招商资讯 > 资讯详情

鲜花配送小程序代码,涵盖首页浏览、商品详情、购物车、下单

2026-09-20 09:51:21

鲜花配送小程序代码,涵盖首页浏览、商品详情、购物车、下单

这是一个可直接运行的鲜花配送小程序代码,涵盖首页浏览、商品详情、购物车、下单和订单管理等核心环节。

html<!-- pages/index/index.wxml -->
<view class="page">
  <!-- 顶部栏 -->
  <view class="topbar">
    <view class="loc">
      <text class="loc-ico">📍</text>
      <text class="loc-txt">杭州 · 西湖区</text>
    </view>
    <view class="search" bindtap="goCategory">
      <text class="s-ico">🔍</text>
      <text class="s-ph">搜索鲜花 / 花束 / 绿植</text>
    </view>
  </view>

  <!-- 轮播 -->
  <swiper class="banner" indicator-dots autoplay circular interval="4000"
          indicator-color="rgba(255,255,255,.45)" indicator-active-color="#ffffff">
    <swiper-item wx:for="{{banners}}" wx:key="id">
      <view class="banner-box" style="background:{{item.bg}}">
        <view class="banner-info">
          <view class="banner-title">{{item.title}}</view>
          <view class="banner-sub">{{item.sub}}</view>
          <view class="banner-btn">立即选购</view>
        </view>
        <text class="banner-emoji">{{item.emoji}}</text>
      </view>
    </swiper-item>
  </swiper>

  <!-- 快捷分类 -->
  <view class="quick">
    <view class="quick-item" wx:for="{{quickCats}}" wx:key="id"
          data-id="{{item.id}}" bindtap="goCategory">
      <view class="quick-icon" style="background:{{item.bg}}">{{item.emoji}}</view>
      <text class="quick-name">{{item.name}}</text>
    </view>
  </view>

  <!-- 服务保障 -->
  <view class="promise">
    <view class="promise-item"><text class="p-ico">🚚</text>3小时送达</view>
    <view class="promise-item"><text class="p-ico">🌿</text>当日鲜花</view>
    <view class="promise-item"><text class="p-ico">💝</text>免费贺卡</view>
  </view>

  <!-- 商品列表 -->
  <view class="section">
    <view class="section-title">
      <text class="st-line"></text>
      <text class="st-text">为你推荐</text>
    </view>

    <view class="goods-grid">
      <view class="goods-card" wx:for="{{goods}}" wx:key="id"
            data-id="{{item.id}}" bindtap="goDetail">
        <view class="goods-img" style="background:{{item.bg}}">
          <text class="goods-emoji">{{item.emoji}}</text>
          <view class="goods-tag" wx:if="{{item.tags[0]}}">{{item.tags[0]}}</view>
        </view>
        <view class="goods-info">
          <view class="goods-name">{{item.name}}</view>
          <view class="goods-sub">{{item.subtitle}}</view>
          <view class="goods-bottom">
            <view class="price">
              <text class="unit">¥</text>{{item.price}}
              <text class="price-from"></text>
            </view>
            <view class="add-btn" catchtap="quickAdd" data-id="{{item.id}}"></view>
          </view>
        </view>
      </view>
    </view>
  </view>

  <view class="footer-tip">—— 愿你被温柔以待 ——</view>
</view>
JavaScript// pages/index/index.js
const { goods, quickCats, banners } = require('../../utils/mock')
const store = require('../../utils/store')

Page({
  data: {
    banners,
    quickCats,
    goods
  },

  onShow() {
    store.syncBadge()
  },

  goDetail(e) {
    wx.navigateTo({ url: `/pages/detail/detail?id=${e.currentTarget.dataset.id}` })
  },

  goCategory() {
    wx.switchTab({ url: '/pages/category/category' })
  },

  quickAdd(e) {
    const id = Number(e.currentTarget.dataset.id)
    const g = goods.find(i => i.id === id)
    if (!g) return
    const spec = g.specs[0]
    store.addToCart(g, spec.name, spec.price, 1)
    wx.showToast({ title: '已加入购物车', icon: 'success' })
  }
})
css/* pages/index/index.wxss */
.page { padding-bottom: 60rpx; }

/* 顶部 */
.topbar {
  display: flex; align-items: center; gap: 20rpx;
  padding: 20rpx 24rpx 12rpx;
  background: #fff;
}
.loc { display: flex; align-items: center; flex-shrink: 0; }
.loc-ico { font-size: 26rpx; margin-right: 4rpx; }
.loc-txt { font-size: 26rpx; color: #333; font-weight: 500; }
.search {
  flex: 1; height: 64rpx; background: #f5f5f7; border-radius: 32rpx;
  display: flex; align-items: center; padding: 0 24rpx;
}
.s-ico { font-size: 24rpx; margin-right: 10rpx; }
.s-ph { font-size: 24rpx; color: #aaa; }

/* 轮播 */
.banner { height: 300rpx; margin: 16rpx 24rpx 0; }
.banner-box {
  height: 100%; border-radius: 24rpx; box-sizing: border-box;
  padding: 0 40rpx; display: flex; align-items: center;
  justify-content: space-between; overflow: hidden;
}
.banner-info { flex: 1; }
.banner-title { font-size: 42rpx; font-weight: 700; color: #fff; letter-spacing: 2rpx; }
.banner-sub { font-size: 24rpx; color: rgba(255,255,255,.9); margin-top: 12rpx; }
.banner-btn {
  display: inline-block; margin-top: 24rpx; padding: 8rpx 26rpx;
  background: rgba(255,255,255,.9); color: #d9578a;
  font-size: 22rpx; border-radius: 24rpx; font-weight: 500;
}
.banner-emoji { font-size: 140rpx; opacity: .95; }

/* 快捷分类 */
.quick {
  display: flex; justify-content: space-between;
  background: #fff; margin: 24rpx; padding: 28rpx 20rpx;
  border-radius: 24rpx;
}
.quick-item { display: flex; flex-direction: column; align-items: center; flex: 1; }
.quick-icon {
  width: 88rpx; height: 88rpx; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 44rpx; margin-bottom: 12rpx;
}
.quick-name { font-size: 22rpx; color: #555; }

/* 服务保障 */
.promise {
  display: flex; justify-content: space-around;
  margin: 0 24rpx 24rpx; padding: 20rpx 0;
  background: #fff6f9; border-radius: 20rpx;
}
.promise-item { font-size: 22rpx; color: #d9578a; display: flex; align-items: center; }
.p-ico { margin-right: 8rpx; font-size: 24rpx; }

/* 商品列表 */
.section { padding: 0 24rpx; }
.section-title { display: flex; align-items: center; margin: 20rpx 0 24rpx; }
.st-line { width: 8rpx; height: 32rpx; background: #e0698c; border-radius: 4rpx; margin-right: 14rpx; }
.st-text { font-size: 32rpx; font-weight: 700; color: #222; }

.goods-grid { display: flex; flex-wrap: wrap; justify-content: space-between; }
.goods-card {
  width: 48.5%; background: #fff; border-radius: 20rpx;
  overflow: hidden; margin-bottom: 24rpx;
  box-shadow: 0 4rpx 20rpx rgba(0,0,0,.04);
}
.goods-img {
  position: relative; height: 300rpx;
  display: flex; align-items: center; justify-content: center;
}
.goods-emoji { font-size: 120rpx; }
.goods-tag {
  position: absolute; left: 0; top: 20rpx;
  background: rgba(224,105,140,.92); color: #fff;
  font-size: 20rpx; padding: 4rpx 14rpx;
  border-radius: 0 20rpx 20rpx 0;
}
.goods-info { padding: 18rpx 20rpx 20rpx; }
.goods-name {
  font-size: 28rpx; font-weight: 600; color: #222;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.goods-sub {
  font-size: 22rpx; color: #aaa; margin-top: 8rpx;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.goods-bottom {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 16rpx;
}
.price { color: #e0698c; font-size: 34rpx; font-weight: 700; }
.unit { font-size: 22rpx; margin-right: 2rpx; }
.price-from { font-size: 20rpx; color: #bbb; font-weight: 400; margin-left: 4rpx; }
.add-btn {
  width: 52rpx; height: 52rpx; border-radius: 50%;
  background: linear-gradient(135deg, #f48fb1, #e0698c);
  color: #fff; font-size: 34rpx; line-height: 50rpx; text-align: center;
}

.footer-tip {
  text-align: center; font-size: 22rpx; color: #ccc;
  margin-top: 20rpx; letter-spacing: 2rpx;
}

2. 分类页

HTML/XML<!-- pages/category/category.wxml -->
<view class="page">
  <view class="search-bar">
    <view class="search">
      <text class="s-ico">🔍</text>
      <input class="s-input" placeholder="搜索鲜花" placeholder-class="ph"
             value="{{keyword}}" bindinput="onInput" confirm-type="search" />
    </view>
  </view>

  <view class="body">
    <!-- 左侧分类 -->
    <scroll-view class="side" scroll-y>
      <view class="side-item {{currentCat === item.id ? 'active' : ''}}"
            wx:for="{{cats}}" wx:key="id"
            data-id="{{item.id}}" bindtap="switchCat">
        <text>{{item.name}}</text>
      </view>
    </scroll-view>

    <!-- 右侧商品 -->
    <scroll-view class="main" scroll-y>
      <view class="cat-banner" style="background:{{currentCatInfo.bg}}">
        <text class="cb-emoji">{{currentCatInfo.emoji}}</text>
        <view class="cb-info">
          <view class="cb-title">{{currentCatInfo.name}}</view>
          <view class="cb-sub">{{currentCatInfo.desc}}</view>
        </view>
      </view>

      <view class="list-item" wx:for="{{list}}" wx:key="id"
            data-id="{{item.id}}" bindtap="goDetail">
        <view class="li-img" style="background:{{item.bg}}">
          <text class="li-emoji">{{item.emoji}}</text>
        </view>
        <view class="li-info">
          <view class="li-name">{{item.name}}</view>
          <view class="li-sub">{{item.subtitle}}</view>
          <view class="li-bottom">
            <view class="li-price"><text class="unit">¥</text>{{item.price}}<text class="qi"></text></view>
            <view class="li-add" catchtap="quickAdd" data-id="{{item.id}}"></view>
          </view>
        </view>
      </view>

      <view class="empty" wx:if="{{list.length === 0}}">
        <text class="empty-ico">🌷</text>
        <text>暂无相关商品</text>
      </view>
      <view style="height:40rpx"></view>
    </scroll-view>
  </view>
</view>
JavaScript// pages/category/category.js
const { goods, categories } = require('../../utils/mock')
const store = require('../../utils/store')

Page({
  data: {
    cats: categories,
    currentCat: categories[0].id,
    currentCatInfo: categories[0],
    list: [],
    keyword: ''
  },

  onLoad() {
    this.filter()
  },

  onShow() {
    store.syncBadge()
  },

  switchCat(e) {
    const id = e.currentTarget.dataset.id
    const info = categories.find(c => c.id === id)
    this.setData({ currentCat: id, currentCatInfo: info }, () => this.filter())
  },

  onInput(e) {
    this.setData({ keyword: e.detail.value }, () => this.filter())
  },

  filter() {
    const { currentCat, keyword } = this.data
    let list = goods
    if (currentCat !== 'all') list = list.filter(g => g.categoryId === currentCat)
    if (keyword.trim()) {
      const k = keyword.trim()
      list = list.filter(g => g.name.indexOf(k) > -1 || g.subtitle.indexOf(k) > -1)
    }
    this.setData({ list })
  },

  goDetail(e) {
    wx.navigateTo({ url: `/pages/detail/detail?id=${e.currentTarget.dataset.id}` })
  },

  quickAdd(e) {
    const id = Number(e.currentTarget.dataset.id)
    const g = goods.find(i => i.id === id)
    if (!g) return
    const spec = g.specs[0]
    store.addToCart(g, spec.name, spec.price, 1)
    wx.showToast({ title: '已加入购物车', icon: 'success' })
  }
})
css/* pages/category/category.wxss */
.page { height: 100vh; display: flex; flex-direction: column; background: #fff; }

.search-bar { padding: 16rpx 24rpx; background: #fff; }
.search {
  height: 68rpx; background: #f5f5f7; border-radius: 34rpx;
  display: flex; align-items: center; padding: 0 24rpx;
}
.s-ico { font-size: 24rpx; margin-right: 10rpx; }
.s-input { flex: 1; font-size: 26rpx; }
.ph { color: #bbb; }

.body { flex: 1; display: flex; overflow: hidden; }

.side { width: 176rpx; background: #f7f7f9; height: 100%; }
.side-item {
  height: 104rpx; display: flex; align-items: center; justify-content: center;
  font-size: 26rpx; color: #666; position: relative;
}
.side-item.active {
  background: #fff; color: #e0698c; font-weight: 600;
}
.side-item.active::before {
  content: ''; position: absolute; left: 0; top: 32rpx;
  width: 6rpx; height: 40rpx; background: #e0698c; border-radius: 0 6rpx 6rpx 0;
}

.main { flex: 1; height: 100%; padding: 20rpx; box-sizing: border-box; }

.cat-banner {
  display: flex; align-items: center; border-radius: 20rpx;
  padding: 24rpx; margin-bottom: 24rpx;
}
.cb-emoji { font-size: 72rpx; margin-right: 20rpx; }
.cb-title { font-size: 30rpx; font-weight: 700; color: #333; }
.cb-sub { font-size: 22rpx; color: #888; margin-top: 8rpx; }

.list-item { display: flex; margin-bottom: 28rpx; }
.li-img {
  width: 180rpx; height: 180rpx; border-radius: 18rpx;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.li-emoji { font-size: 90rpx; }
.li-info { flex: 1; padding-left: 20rpx; display: flex; flex-direction: column; }
.li-name { font-size: 28rpx; font-weight: 600; color: #222; }
.li-sub {
  font-size: 22rpx; color: #aaa; margin-top: 8rpx;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.li-bottom {
  margin-top: auto; display: flex; align-items: center;
  justify-content: space-between;
}
.li-price { color: #e0698c; font-size: 32rpx; font-weight: 700; }
.unit { font-size: 20rpx; }
.qi { font-size: 20rpx; color: #bbb; font-weight: 400; margin-left: 4rpx; }
.li-add {
  width: 48rpx; height: 48rpx; border-radius: 50%;
  background: linear-gradient(135deg, #f48fb1, #e0698c);
  color: #fff; font-size: 30rpx; line-height: 46rpx; text-align: center;
}

.empty {
  padding: 120rpx 0; display: flex; flex-direction: column;
  align-items: center; color: #ccc; font-size: 24rpx;
}
.empty-ico { font-size: 90rpx; margin-bottom: 20rpx; }

3. 商品详情页

HTML/XML<!-- pages/detail/detail.wxml -->
<view class="page" wx:if="{{goods}}">
  <!-- 主图 -->
  <view class="hero" style="background:{{goods.bg}}">
    <text class="hero-emoji">{{goods.emoji}}</text>
  </view>

  <!-- 价格区 -->
  <view class="price-card">
    <view class="pc-row">
      <view class="pc-price">
        <text class="unit">¥</text>{{currentSpec.price}}
      </view>
      <view class="pc-origin" wx:if="{{goods.originPrice}}">¥{{goods.originPrice}}</view>
      <view class="pc-sales">已售 {{goods.sales}}</view>
    </view>
    <view class="pc-name">{{goods.name}}</view>
    <view class="pc-sub">{{goods.subtitle}}</view>
    <view class="pc-tags">
      <text class="pc-tag" wx:for="{{goods.tags}}" wx:key="*this">{{item}}</text>
    </view>
  </view>

  <!-- 规格 -->
  <view class="card">
    <view class="card-title">选择规格</view>
    <view class="spec-list">
      <view class="spec-item {{specIndex === index ? 'active' : ''}}"
            wx:for="{{goods.specs}}" wx:key="name"
            data-index="{{index}}" bindtap="chooseSpec">
        {{item.name}}
      </view>
    </view>
    <view class="count-row">
      <text class="count-label">购买数量</text>
      <view class="stepper">
        <view class="step-btn {{count<=1?'disabled':''}}" bindtap="minus"></view>
        <view class="step-num">{{count}}</view>
        <view class="step-btn" bindtap="plus"></view>
      </view>
    </view>
  </view>

  <!-- 配送 -->
  <view class="card">
    <view class="info-row">
      <text class="ir-label">配送</text>
      <text class="ir-value">杭州城区 3 小时达 · 满 199 免运费</text>
    </view>
    <view class="info-row">
      <text class="ir-label">保障</text>
      <text class="ir-value">鲜花不新鲜 · 无条件退款</text>
    </view>
  </view>

  <!-- 详情 -->
  <view class="card">
    <view class="card-title">商品详情</view>
    <view class="desc-item" wx:for="{{goods.desc}}" wx:key="*this">
      <text class="dot"></text>{{item}}
    </view>
    <view class="detail-img" style="background:{{goods.bg}}">
      <text class="di-emoji">{{goods.emoji}}</text>
      <text class="di-text">{{goods.name}}</text>
    </view>
  </view>

  <view style="height:140rpx"></view>

  <!-- 底部操作栏 -->
  <view class="footer">
    <view class="f-icon" bindtap="goHome">
      <text class="fi">🏠</text>
      <text class="ft">首页</text>
    </view>
    <view class="f-icon" bindtap="goCart">
      <text class="fi">🛒</text>
      <text class="ft">购物车</text>
      <view class="badge" wx:if="{{cartCount > 0}}">{{cartCount > 99 ? '99+' : cartCount}}</view>
    </view>
    <view class="f-btns">
      <view class="btn btn-cart" bindtap="openSpec" data-action="cart">加入购物车</view>
      <view class="btn btn-buy" bindtap="openSpec" data-action="buy">立即购买</view>
    </view>
  </view>

  <!-- 规格弹窗 -->
  <view class="mask" wx:if="{{showSpec}}" bindtap="closeSpec"></view>
  <view class="spec-panel" wx:if="{{showSpec}}">
    <view class="sp-head">
      <view class="sp-img" style="background:{{goods.bg}}">
        <text class="sp-emoji">{{goods.emoji}}</text>
      </view>
      <view class="sp-info">
        <view class="sp-price"><text class="unit">¥</text>{{currentSpec.price}}</view>
        <view class="sp-stock">已选:{{currentSpec.name}} × {{count}}</view>
      </view>
      <view class="sp-close" bindtap="closeSpec"></view>
    </view>

    <scroll-view class="sp-body" scroll-y>
      <view class="sp-label">规格</view>
      <view class="spec-list">
        <view class="spec-item {{specIndex === index ? 'active' : ''}}"
              wx:for="{{goods.specs}}" wx:key="name"
              data-index="{{index}}" bindtap="chooseSpec">
          {{item.name}}
        </view>
      </view>
      <view class="count-row">
        <text class="count-label">购买数量</text>
        <view class="stepper">
          <view class="step-btn {{count<=1?'disabled':''}}" bindtap="minus"></view>
          <view class="step-num">{{count}}</view>
          <view class="step-btn" bindtap="plus"></view>
        </view>
      </view>
    </scroll-view>

    <view class="sp-confirm" bindtap="confirmSpec">
      {{specAction === 'cart' ? '加入购物车' : '立即购买'}}
    </view>
  </view>
</view>
JavaScript// pages/detail/detail.js
const { goods: allGoods } = require('../../utils/mock')
const store = require('../../utils/store')

Page({
  data: {
    goods: null,
    specIndex: 0,
    currentSpec: {},
    count: 1,
    showSpec: false,
    specAction: 'cart',
    cartCount: 0
  },

  onLoad(options) {
    const id = Number(options.id)
    const goods = allGoods.find(g => g.id === id) || allGoods[0]
    this.setData({
      goods,
      currentSpec: goods.specs[0],
      specIndex: 0
    })
  },

  onShow() {
    this.setData({ cartCount: store.totalCount() })
    store.syncBadge()
  },

  chooseSpec(e) {
    const index = Number(e.currentTarget.dataset.index)
    this.setData({
      specIndex: index,
      currentSpec: this.data.goods.specs[index]
    })
  },

  plus() {
    this.setData({ count: this.data.count + 1 })
  },

  minus() {
    if (this.data.count > 1) this.setData({ count: this.data.count - 1 })
  },

  openSpec(e) {
    this.setData({ showSpec: true, specAction: e.currentTarget.dataset.action })
  },

  closeSpec() {
    this.setData({ showSpec: false })
  },

  confirmSpec() {
    const { goods, currentSpec, count, specAction } = this.data

    if (specAction === 'cart') {
      store.addToCart(goods, currentSpec.name, currentSpec.price, count)
      this.setData({
        showSpec: false,
        count: 1,
        cartCount: store.totalCount()
      })
      wx.showToast({ title: '已加入购物车', icon: 'success' })
    } else {
      const items = [{
        key: goods.id + '_' + currentSpec.name,
        goodsId: goods.id,
        name: goods.name,
        emoji: goods.emoji,
        bg: goods.bg,
        price: currentSpec.price,
        specName: currentSpec.name,
        count
      }]
      wx.setStorageSync('CHECKOUT_ITEMS', items)
      wx.setStorageSync('CHECKOUT_FROM_CART', false)
      this.setData({ showSpec: false })
      wx.navigateTo({ url: '/pages/order-confirm/order-confirm' })
    }
  },

  goHome() {
    wx.switchTab({ url: '/pages/index/index' })
  },

  goCart() {
    wx.switchTab({ url: '/pages/cart/cart' })
  }
})
css/* pages/detail/detail.wxss */
.page { min-height: 100vh; background: #f7f7f8; }

.hero {
  height: 620rpx; display: flex; align-items: center; justify-content: center;
}
.hero-emoji { font-size: 300rpx; }

.price-card {
  background: #fff; padding: 28rpx 28rpx 24rpx;
  border-radius: 28rpx 28rpx 0 0; margin-top: -28rpx; position: relative;
}
.pc-row { display: flex; align-items: baseline; }
.pc-price { color: #e0698c; font-size: 52rpx; font-weight: 700; }
.unit { font-size: 28rpx; }
.pc-origin {
  font-size: 24rpx; color: #bbb; text-decoration: line-through;
  margin-left: 16rpx;
}
.pc-sales { margin-left: auto; font-size: 22rpx; color: #999; }
.pc-name { font-size: 36rpx; font-weight: 700; color: #222; margin-top: 16rpx; }
.pc-sub { font-size: 24rpx; color: #999; margin-top: 10rpx; }
.pc-tags { display: flex; flex-wrap: wrap; margin-top: 18rpx; }
.pc-tag {
  font-size: 20rpx; color: #e0698c; background: #fff0f4;
  padding: 6rpx 16rpx; border-radius: 20rpx; margin-right: 12rpx;
}

.card {
  background: #fff; margin: 20rpx 24rpx; border-radius: 20rpx;
  padding: 28rpx;
}
.card-title { font-size: 28rpx; font-weight: 700; color: #222; margin-bottom: 20rpx; }

.spec-list { display: flex; flex-wrap: wrap; }
.spec-item {
  padding: 14rpx 32rpx; background: #f5f5f7; border-radius: 12rpx;
  font-size: 26rpx; color: #555; margin: 0 18rpx 18rpx 0;
  border: 2rpx solid transparent;
}
.spec-item.active {
  background: #fff0f4; color: #e0698c;
  border-color: #e0698c; font-weight: 600;
}

.count-row {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 12rpx;
}
.count-label { font-size: 26rpx; color: #555; }
.stepper { display: flex; align-items: center; }
.step-btn {
  width: 56rpx; height: 56rpx; background: #f5f5f7; border-radius: 12rpx;
  display: flex; align-items: center; justify-content: center;
  font-size: 30rpx; color: #333;
}
.step-btn.disabled { color: #ccc; }
.step-num {
  width: 80rpx; text-align: center; font-size: 28rpx; font-weight: 600;
}

.info-row { display: flex; margin-bottom: 16rpx; }
.info-row:last-child { margin-bottom: 0; }
.ir-label { font-size: 26rpx; color: #999; width: 100rpx; flex-shrink: 0; }
.ir-value { font-size: 26rpx; color: #333; flex: 1; }

.desc-item {
  font-size: 26rpx; color: #555; line-height: 48rpx;
  display: flex;
}
.dot { color: #e0698c; margin-right: 12rpx; }

.detail-img {
  margin-top: 24rpx; height: 420rpx; border-radius: 16rpx;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
}
.di-emoji { font-size: 180rpx; }
.di-text { font-size: 26rpx; color: #666; margin-top: 20rpx; }

/* 底部栏 */
.footer {
  position: fixed; left: 0; right: 0; bottom: 0;
  height: 110rpx; background: #fff; display: flex; align-items: center;
  padding: 0 24rpx calc(0rpx + env(safe-area-inset-bottom));
  box-shadow: 0 -4rpx 20rpx rgba(0,0,0,.05);
}
.f-icon {
  width: 100rpx; display: flex; flex-direction: column;
  align-items: center; justify-content: center; position: relative;
}
.fi { font-size: 34rpx; }
.ft { font-size: 20rpx; color: #888; margin-top: 2rpx; }
.badge {
  position: absolute; top: -6rpx; right: 14rpx;
  min-width: 32rpx; height: 32rpx; padding: 0 8rpx;
  background: #e0698c; color: #fff; font-size: 20rpx;
  border-radius: 16rpx; display: flex;
  align-items: center; justify-content: center;
  box-sizing: border-box;
}
.f-btns { flex: 1; display: flex; margin-left: 16rpx; }
.btn {
  flex: 1; height: 80rpx; display: flex; align-items: center;
  justify-content: center; font-size: 28rpx; color: #fff;
}
.btn-cart { background: #ffb74d; border-radius: 40rpx 0 0 40rpx; }
.btn-buy { background: linear-gradient(135deg, #f48fb1, #e0698c); border-radius: 0 40rpx 40rpx 0; }

/* 弹窗 */
.mask {
  position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 100;
}
.spec-panel {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 101;
  background: #fff; border-radius: 28rpx 28rpx 0 0;
  padding-bottom: env(safe-area-inset-bottom);
  animation: slideUp .25s ease;
}
@keyframes slideUp {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}
.sp-head {
  display: flex; align-items: center; padding: 30rpx 28rpx;
  border-bottom: 2rpx solid #f5f5f7; position: relative;
}
.sp-img {
  width: 160rpx; height: 160rpx; border-radius: 16rpx;
  display: flex; align-items: center; justify-content: center;
  margin-top: -50rpx;
}
.sp-emoji { font-size: 90rpx; }
.sp-info { flex: 1; margin-left: 24rpx; }
.sp-price { color: #e0698c; font-size: 42rpx; font-weight: 700; }
.sp-stock { font-size: 24rpx; color: #999; margin-top: 12rpx; }
.sp-close {
  position: absolute; right: 28rpx; top: 24rpx;
  font-size: 32rpx; color: #bbb; padding: 10rpx;
}
.sp-body { max-height: 600rpx; padding: 28rpx; }
.sp-label { font-size: 26rpx; color: #333; font-weight: 600; margin-bottom: 20rpx; }
.sp-confirm {
  margin: 20rpx 28rpx 28rpx; height: 88rpx;
  background: linear-gradient(135deg, #f48fb1, #e0698c);
  color: #fff; border-radius: 44rpx; display: flex;
  align-items: center; justify-content: center;
  font-size: 30rpx; font-weight: 600;
}

4. 购物车页

HTML/XML<!-- pages/cart/cart.wxml -->
<view class="page">
  <view class="cart-header" wx:if="{{cart.length > 0}}">
    <text class="ch-title">购物车</text>
    <text class="ch-count">共 {{totalCount}} 件</text>
  </view>

  <block wx:if="{{cart.length > 0}}">
    <view class="cart-list">
      <view class="cart-item" wx:for="{{cart}}" wx:key="key">
        <view class="check {{item.checked ? 'checked' : ''}}"
              data-key="{{item.key}}" bindtap="toggleCheck">
          <text wx:if="{{item.checked}}"></text>
        </view>

        <view class="ci-img" style="background:{{item.bg}}" data-key="{{item.key}}" bindtap="goDetail">
          <text class="ci-emoji">{{item.emoji}}</text>
        </view>

        <view class="ci-info">
          <view class="ci-name">{{item.name}}</view>
          <view class="ci-spec">{{item.specName}}</view>
          <view class="ci-bottom">
            <view class="ci-price"><text class="unit">¥</text>{{item.price}}</view>
            <view class="stepper">
              <view class="step-btn {{item.count<=1?'disabled':''}}"
                    data-key="{{item.key}}" bindtap="minus"></view>
              <view class="step-num">{{item.count}}</view>
              <view class="step-btn" data-key="{{item.key}}" bindtap="plus"></view>
            </view>
          </view>
        </view>

        <view class="ci-del" data-key="{{item.key}}" bindtap="removeItem"></view>
      </view>
    </view>

    <view class="cart-tip">🚚 满 199 元免配送费,满 299 元立减 30 元</view>
    <view style="height:160rpx"></view>

    <!-- 结算栏 -->
    <view class="checkout-bar">
      <view class="cb-check" bindtap="toggleAll">
        <view class="check {{allChecked ? 'checked' : ''}}">
          <text wx:if="{{allChecked}}"></text>
        </view>
        <text class="cb-text">全选</text>
      </view>
      <view class="cb-total">
        <text class="cb-label">合计</text>
        <text class="cb-price"><text class="unit">¥</text>{{checkedTotal}}</text>
      </view>
      <view class="cb-btn {{checkedCount ? '' : 'disabled'}}" bindtap="checkout">
        结算({{checkedCount}})
      </view>
    </view>
  </block>

  <!-- 空状态 -->
  <view class="empty" wx:else>
    <text class="empty-ico">🛒</text>
    <text class="empty-text">购物车还是空的</text>
    <view class="empty-btn" bindtap="goShopping">去逛逛</view>
  </view>
</view>
JavaScript// pages/cart/cart.js
const store = require('../../utils/store')

Page({
  data: {
    cart: [],
    allChecked: false,
    checkedCount: 0,
    checkedTotal: '0.00',
    totalCount: 0
  },

  onShow() {
    this.refresh()
    store.syncBadge()
  },

  refresh() {
    const cart = store.getCart()
    const checked = cart.filter(i => i.checked)
    const total = checked.reduce((s, i) => s + i.price * i.count, 0)
    const count = checked.reduce((s, i) => s + i.count, 0)

    this.setData({
      cart,
      allChecked: cart.length > 0 && checked.length === cart.length,
      checkedCount: count,
      checkedTotal: (Math.round(total * 100) / 100).toFixed(2),
      totalCount: cart.reduce((s, i) => s + i.count, 0)
    })
  },

  toggleCheck(e) {
    const key = e.currentTarget.dataset.key
    const item = store.getCart().find(i => i.key === key)
    store.toggleCheck(key, !item.checked)
    this.refresh()
  },

  toggleAll() {
    store.toggleAll(!this.data.allChecked)
    this.refresh()
  },

  plus(e) {
    const key = e.currentTarget.dataset.key
    const item = store.getCart().find(i => i.key === key)
    store.updateCount(key, item.count + 1)
    this.refresh()
  },

  minus(e) {
    const key = e.currentTarget.dataset.key
    const item = store.getCart().find(i => i.key === key)
    if (item.count <= 1) return
    store.updateCount(key, item.count - 1)
    this.refresh()
  },

  removeItem(e) {
    const key = e.currentTarget.dataset.key
    wx.showModal({
      title: '提示',
      content: '确定要删除这件商品吗?',
      confirmColor: '#e0698c',
      success: (res) => {
        if (res.confirm) {
          store.removeItem(key)
          this.refresh()
        }
      }
    })
  },

  goDetail(e) {
    const key = e.currentTarget.dataset.key
    const item = store.getCart().find(i => i.key === key)
    if (item) wx.navigateTo({ url: `/pages/detail/detail?id=${item.goodsId}` })
  },

  goShopping() {
    wx.switchTab({ url: '/pages/index/index' })
  },

  checkout() {
    const items = store.getChecked()
    if (!items.length) {
      wx.showToast({ title: '请先选择商品', icon: 'none' })
      return
    }
    wx.setStorageSync('CHECKOUT_ITEMS', items)
    wx.setStorageSync('CHECKOUT_FROM_CART', true)
    wx.navigateTo({ url: '/pages/order-confirm/order-confirm' })
  }
})
css/* pages/cart/cart.wxss */
.page { min-height: 100vh; background: #f7f7f8; }

.cart-header {
  display: flex; align-items: baseline;
  padding: 28rpx 32rpx 16rpx;
}
.ch-title { font-size: 38rpx; font-weight: 700; color: #222; }
.ch-count { font-size: 24rpx; color: #999; margin-left: 16rpx; }

.cart-list { padding: 0 24rpx; }

.cart-item {
  display: flex; align-items: center; background: #fff;
  border-radius: 20rpx; padding: 26rpx 22rpx; margin-bottom: 20rpx;
  position: relative;
}
.check {
  width: 40rpx; height: 40rpx; border-radius: 50%;
  border: 2rpx solid #ddd; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 24rpx; color: #fff; box-sizing: border-box;
}
.check.checked { background: #e0698c; border-color: #e0698c; }

.ci-img {
  width: 150rpx; height: 150rpx; border-radius: 16rpx;
  display: flex; align-items: center; justify-content: center;
  margin-left: 20rpx; flex-shrink: 0;
}
.ci-emoji { font-size: 80rpx; }

.ci-info {
  flex: 1; margin-left: 20rpx; height: 150rpx;
  display: flex; flex-direction: column; justify-content: space-between;
}
.ci-name { font-size: 28rpx; font-weight: 600; color: #222; }
.ci-spec {
  font-size: 22rpx; color: #999; background: #f7f7f9;
  padding: 4rpx 14rpx; border-radius: 8rpx; align-self: flex-start;
}
.ci-bottom { display: flex; align-items: center; justify-content: space-between; }
.ci-price { color: #e0698c; font-size: 32rpx; font-weight: 700; }
.unit { font-size: 20rpx; }

.stepper { display: flex; align-items: center; }
.step-btn {
  width: 48rpx; height: 48rpx; background: #f5f5f7; border-radius: 10rpx;
  display: flex; align-items: center; justify-content: center;
  font-size: 26rpx; color: #333;
}
.step-btn.disabled { color: #ddd; }
.step-num { width: 66rpx; text-align: center; font-size: 26rpx; font-weight: 600; }

.ci-del {
  position: absolute; right: 22rpx; top: 18rpx;
  font-size: 24rpx; color: #ccc; padding: 8rpx;
}

.cart-tip {
  margin: 8rpx 24rpx; padding: 20rpx; background: #fff6f9;
  border-radius: 16rpx; font-size: 22rpx; color: #d9578a;
}

/* 结算栏 */
.checkout-bar {
  position: fixed; left: 0; right: 0; bottom: 0;
  height: 110rpx; background: #fff; display: flex; align-items: center;
  padding: 0 24rpx calc(0rpx + env(safe-area-inset-bottom));
  box-shadow: 0 -4rpx 20rpx rgba(0,0,0,.05);
}
.cb-check { display: flex; align-items: center; }
.cb-text { font-size: 26rpx; color: #555; margin-left: 12rpx; }
.cb-total { flex: 1; text-align: right; margin-right: 20rpx; }
.cb-label { font-size: 24rpx; color: #666; }
.cb-price { color: #e0698c; font-size: 38rpx; font-weight: 700; margin-left: 8rpx; }
.cb-btn {
  height: 76rpx; padding: 0 48rpx;
  background: linear-gradient(135deg, #f48fb1, #e0698c);
  color: #fff; border-radius: 38rpx; font-size: 28rpx;
  font-weight: 600; display: flex; align-items: center; justify-content: center;
}
.cb-btn.disabled { background: #ddd; }

/* 空状态 */
.empty {
  padding-top: 220rpx; display: flex; flex-direction: column;
  align-items: center;
}
.empty-ico { font-size: 140rpx; opacity: .4; }
.empty-text { font-size: 26rpx; color: #bbb; margin: 24rpx 0 44rpx; }
.empty-btn {
  padding: 18rpx 64rpx; background: linear-gradient(135deg, #f48fb1, #e0698c);
  color: #fff; border-radius: 40rpx; font-size: 28rpx;
}

5. 订单确认页

HTML/XML<!-- pages/order-confirm/order-confirm.wxml -->
<view class="page">
  <!-- 地址 -->
  <view class="card addr-card" bindtap="editAddress">
    <block wx:if="{{address}}">
      <view class="addr-top">
        <text class="addr-name">{{address.name}}</text>
        <text class="addr-phone">{{address.phone}}</text>
        <text class="addr-default" wx:if="{{address.isDefault}}">默认</text>
      </view>
      <view class="addr-detail">{{address.region}} {{address.detail}}</view>
    </block>
    <block wx:else>
      <view class="addr-empty">
        <text class="ae-ico">📍</text>
        <text>请添加收货地址</text>
      </view>
    </block>
    <text class="arrow"></text>
  </view>

  <!-- 配送时间 -->
  <view class="card">
    <picker mode="selector" range="{{timeSlots}}" value="{{timeIndex}}" bindchange="onTimeChange">
      <view class="row">
        <text class="row-label">配送时间</text>
        <view class="row-value">
          <text>{{timeSlots[timeIndex]}}</text>
          <text class="arrow"></text>
        </view>
      </view>
    </picker>

    <view class="row">
      <text class="row-label">贺卡留言</text>
      <input class="row-input" placeholder="写上你的祝福(选填)"
             placeholder-class="ph" value="{{blessing}}" bindinput="onBlessing" maxlength="40" />
    </view>

    <view class="row">
      <text class="row-label">订单备注</text>
      <input class="row-input" placeholder="如:请提前电话联系(选填)"
             placeholder-class="ph" value="{{remark}}" bindinput="onRemark" maxlength="50" />
    </view>
  </view>

  <!-- 商品清单 -->
  <view class="card">
    <view class="card-title">商品清单</view>
    <view class="oi-item" wx:for="{{items}}" wx:key="key">
      <view class="oi-img" style="background:{{item.bg}}">
        <text class="oi-emoji">{{item.emoji}}</text>
      </view>
      <view class="oi-info">
        <view class="oi-name">{{item.name}}</view>
        <view class="oi-spec">{{item.specName}}</view>
      </view>
      <view class="oi-right">
        <view class="oi-price">¥{{item.price}}</view>
        <view class="oi-count">×{{item.count}}</view>
      </view>
    </view>
  </view>

  <!-- 金额 -->
  <view class="card">
    <view class="fee-row">
      <text>商品金额</text>
      <text>¥{{goodsTotal}}</text>
    </view>
    <view class="fee-row">
      <text>配送费</text>
      <text class="{{deliveryFee == '0.00' ? 'free' : ''}}">
        {{deliveryFee == '0.00' ? '免配送费' : '¥' + deliveryFee}}
      </text>
    </view>
    <view class="fee-row" wx:if="{{discount !== '0.00'}}">
      <text>满减优惠</text>
      <text class="discount">-¥{{discount}}</text>
    </view>
    <view class="fee-row total">
      <text>合计</text>
      <text class="total-price"><text class="unit">¥</text>{{total}}</text>
    </view>
  </view>

  <view style="height:160rpx"></view>

  <!-- 提交栏 -->
  <view class="submit-bar">
    <view class="sb-total">
      <text class="sb-label">实付</text>
      <text class="sb-price"><text class="unit">¥</text>{{total}}</text>
    </view>
    <view class="sb-btn" bindtap="submitOrder">提交订单</view>
  </view>
</view>
JavaScript// pages/order-confirm/order-confirm.js
const store = require('../../utils/store')
const { formatTime } = require('../../utils/util')

const TIME_SLOTS = [
  '尽快送达(2小时内)',
  '今天 09:00-12:00',
  '今天 12:00-15:00',
  '今天 15:00-18:00',
  '今天 18:00-21:00',
  '明天 09:00-12:00',
  '明天 12:00-15:00',
  '明天 15:00-18:00'
]

Page({
  data: {
    items: [],
    address: null,
    timeSlots: TIME_SLOTS,
    timeIndex: 0,
    blessing: '',
    remark: '',
    goodsTotal: '0.00',
    deliveryFee: '0.00',
    discount: '0.00',
    total: '0.00'
  },

  onLoad() {
    const items = wx.getStorageSync('CHECKOUT_ITEMS') || []
    this.setData({ items }, () => this.calc())
  },

  onShow() {
    const addr = wx.getStorageSync('FLOWER_ADDRESS')
    if (addr) this.setData({ address: addr })
  },

  calc() {
    const { items } = this.data
    const goodsTotal = items.reduce((s, i) => s + i.price * i.count, 0)

    let deliveryFee = goodsTotal >= 199 ? 0 : 15
    let discount = goodsTotal >= 299 ? 30 : 0
    const total = Math.max(0, goodsTotal + deliveryFee - discount)

    const f = n => (Math.round(n * 100) / 100).toFixed(2)
    this.setData({
      goodsTotal: f(goodsTotal),
      deliveryFee: f(deliveryFee),
      discount: f(discount),
      total: f(total)
    })
  },

  onTimeChange(e) {
    this.setData({ timeIndex: Number(e.detail.value) })
  },

  onBlessing(e) {
    this.setData({ blessing: e.detail.value })
  },

  onRemark(e) {
    this.setData({ remark: e.detail.value })
  },

  editAddress() {
    wx.navigateTo({ url: '/pages/address/address' })
  },

  submitOrder() {
    const { items, address, timeSlots, timeIndex, blessing, remark } = this.data

    if (!items.length) {
      wx.showToast({ title: '没有可结算的商品', icon: 'none' })
      return
    }
    if (!address) {
      wx.showToast({ title: '请先填写收货地址', icon: 'none' })
      return
    }

    wx.showLoading({ title: '提交中...' })

    setTimeout(() => {
      const order = {
        id: 'FL' + Date.now(),
        items,
        address,
        deliveryTime: timeSlots[timeIndex],
        blessing,
        remark,
        goodsTotal: this.data.goodsTotal,
        deliveryFee: this.data.deliveryFee,
        discount: this.data.discount,
        total: this.data.total,
        status: 'pending',
        createTime: formatTime()
      }

      const orders = wx.getStorageSync('FLOWER_ORDERS') || []
      orders.unshift(order)
      wx.setStorageSync('FLOWER_ORDERS', orders)

      if (wx.getStorageSync('CHECKOUT_FROM_CART')) {
        store.clearChecked()
      }
      wx.removeStorageSync('CHECKOUT_ITEMS')
      wx.removeStorageSync('CHECKOUT_FROM_CART')

      wx.hideLoading()
      wx.showToast({ title: '下单成功', icon: 'success' })

      setTimeout(() => {
        wx.redirectTo({ url: '/pages/order-list/order-list' })
      }, 800)
    }, 600)
  }
})
css/* pages/order-confirm/order-confirm.wxss */
.page { min-height: 100vh; background: #f7f7f8; padding-top: 20rpx; }

.card {
  background: #fff; margin: 0 24rpx 20rpx; border-radius: 20rpx;
  padding: 28rpx; position: relative;
}
.card-title { font-size: 28rpx; font-weight: 700; color: #222; margin-bottom: 24rpx; }

/* 地址 */
.addr-card { padding-right: 60rpx; }
.addr-top { display: flex; align-items: center; margin-bottom: 12rpx; }
.addr-name { font-size: 30rpx; font-weight: 700; color: #222; }
.addr-phone { font-size: 26rpx; color: #666; margin-left: 20rpx; }
.addr-default {
  font-size: 20rpx; color: #e0698c; background: #fff0f4;
  padding: 2rpx 12rpx; border-radius: 6rpx; margin-left: 16rpx;
}
.addr-detail { font-size: 25rpx; color: #666; line-height: 38rpx; }
.addr-empty {
  display: flex; align-items: center; font-size: 28rpx;
  color: #e0698c; padding: 16rpx 0;
}
.ae-ico { margin-right: 12rpx; font-size: 32rpx; }

/* 通用行 */
.row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 22rpx 0; border-bottom: 2rpx solid #f5f5f7;
}
.row:last-child { border-bottom: none; }
.row-label { font-size: 27rpx; color: #333; flex-shrink: 0; }
.row-value { font-size: 26rpx; color: #666; display: flex; align-items: center; }
.row-input { flex: 1; text-align: right; font-size: 26rpx; color: #333; }
.ph { color: #c8c8c8; }
.arrow { font-size: 34rpx; color: #ccc; margin-left: 8rpx; }

/* 商品 */
.oi-item { display: flex; align-items: center; margin-bottom: 24rpx; }
.oi-item:last-child { margin-bottom: 0; }
.oi-img {
  width: 120rpx; height: 120rpx; border-radius: 14rpx;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.oi-emoji { font-size: 64rpx; }
.oi-info { flex: 1; margin-left: 20rpx; }
.oi-name { font-size: 27rpx; font-weight: 600; color: #222; }
.oi-spec { font-size: 22rpx; color: #999; margin-top: 10rpx; }
.oi-right { text-align: right; }
.oi-price { font-size: 27rpx; color: #333; font-weight: 600; }
.oi-count { font-size: 22rpx; color: #999; margin-top: 8rpx; }

/* 费用 */
.fee-row {
  display: flex; justify-content: space-between;
  font-size: 26rpx; color: #666; padding: 14rpx 0;
}
.fee-row .free { color: #4caf50; }
.fee-row .discount { color: #e0698c; }
.fee-row.total {
  border-top: 2rpx solid #f5f5f7;
  margin-top: 12rpx; padding-top: 24rpx;
  font-size: 28rpx; color: #222; font-weight: 600;
}
.total-price { color: #e0698c; font-size: 38rpx; font-weight: 700; }
.unit { font-size: 22rpx; }

/* 提交栏 */
.submit-bar {
  position: fixed; left: 0; right: 0; bottom: 0;
  height: 110rpx; background: #fff; display: flex; align-items: center;
  padding: 0 24rpx calc(0rpx + env(safe-area-inset-bottom));
  box-shadow: 0 -4rpx 20rpx rgba(0,0,0,.05);
}
.sb-total { flex: 1; display: flex; align-items: baseline; }
.sb-label { font-size: 26rpx; color: #666; }
.sb-price { color: #e0698c; font-size: 40rpx; font-weight: 700; margin-left: 10rpx; }
.sb-btn {
  height: 80rpx; padding: 0 56rpx;
  background: linear-gradient(135deg, #f48fb1, #e0698c);
  color: #fff; border-radius: 40rpx; font-size: 30rpx;
  font-weight: 600; display: flex; align-items: center; justify-content: center;
}

6. 订单列表页

HTML/XML<!-- pages/order-list/order-list.wxml -->
<view class="page">
  <view class="tabs">
    <view class="tab {{currentTab === item.key ? 'active' : ''}}"
          wx:for="{{tabs}}" wx:key="key"
          data-key="{{item.key}}" bindtap="switchTab">
      <text>{{item.name}}</text>
      <view class="tab-line" wx:if="{{currentTab === item.key}}"></view>
    </view>
  </view>

  <view class="list" wx:if="{{list.length > 0}}">
    <view class="order-card" wx:for="{{list}}" wx:key="id"
          data-id="{{item.id}}" bindtap="goDetail">
      <view class="oc-head">
        <text class="oc-id">订单号:{{item.id}}</text>
        <text class="oc-status">{{item.statusText}}</text>
      </view>

      <view class="oc-goods">
        <view class="og-item" wx:for="{{item.items}}" wx:for-item="g" wx:key="key">
          <view class="og-img" style="background:{{g.bg}}">
            <text class="og-emoji">{{g.emoji}}</text>
          </view>
          <view class="og-info">
            <view class="og-name">{{g.name}}</view>
            <view class="og-spec">{{g.specName}}</view>
          </view>
          <view class="og-right">
            <view class="og-price">¥{{g.price}}</view>
            <view class="og-count">×{{g.count}}</view>
          </view>
        </view>
      </view>

      <view class="oc-foot">
        <text class="oc-time">{{item.createTime}}</text>
        <text class="oc-total">共 {{item.totalCount}} 件 合计 <text class="oc-price">¥{{item.total}}</text></text>
      </view>

      <view class="oc-actions">
        <view class="oc-btn" catchtap="contact" data-id="{{item.id}}">联系客服</view>
        <view class="oc-btn primary" catchtap="urge" data-id="{{item.id}}">催一催</view>
      </view>
    </view>
  </view>

  <view class="empty" wx:else>
    <text class="empty-ico">📦</text>
    <text class="empty-text">暂无相关订单</text>
  </view>
</view>
JavaScript// pages/order-list/order-list.js
const STATUS_MAP = {
  pending: '待配送',
  delivering: '配送中',
  done: '已完成'
}

Page({
  data: {
    tabs: [
      { key: 'all', name: '全部' },
      { key: 'pending', name: '待配送' },
      { key: 'delivering', name: '配送中' },
      { key: 'done', name: '已完成' }
    ],
    currentTab: 'all',
    list: []
  },

  onShow() {
    this.load()
  },

  switchTab(e) {
    this.setData({ currentTab: e.currentTarget.dataset.key }, () => this.load())
  },

  load() {
    const orders = wx.getStorageSync('FLOWER_ORDERS') || []
    const { currentTab } = this.data

    let list = orders
    if (currentTab !== 'all') list = orders.filter(o => o.status === currentTab)

    list = list.map(o => ({
      ...o,
      statusText: STATUS_MAP[o.status] || '待配送',
      totalCount: o.items.reduce((s, i) => s + i.count, 0)
    }))

    this.setData({ list })
  },

  goDetail(e) {
    wx.navigateTo({ url: `/pages/order-detail/order-detail?id=${e.currentTarget.dataset.id}` })
  },

  contact() {
    wx.showToast({ title: '客服电话:400-888-8888', icon: 'none' })
  },

  urge() {
    wx.showToast({ title: '已帮您催单~', icon: 'none' })
  }
})
css/* pages/order-list/order-list.wxss */
.page { min-height: 100vh; background: #f7f7f8; }

.tabs {
  display: flex; background: #fff; position: sticky; top: 0; z-index: 10;
}
.tab {
  flex: 1; height: 88rpx; display: flex; align-items: center;
  justify-content: center; font-size: 27rpx; color: #666;
  position: relative;
}
.tab.active { color: #e0698c; font-weight: 700; }
.tab-line {
  position: absolute; bottom: 8rpx; width: 48rpx; height: 6rpx;
  background: #e0698c; border-radius: 3rpx;
}

.list { padding: 24rpx; }
.order-card {
  background: #fff; border-radius: 20rpx;
  padding: 26rpx; margin-bottom: 24rpx;
}
.oc-head {
  display: flex; justify-content: space-between; align-items: center;
  padding-bottom: 20rpx; border-bottom: 2rpx solid #f7f7f9;
}
.oc-id { font-size: 22rpx; color: #999; }
.oc-status { font-size: 26rpx; color: #e0698c; font-weight: 600; }

.oc-goods { padding: 20rpx 0; }
.og-item { display: flex; align-items: center; margin-bottom: 20rpx; }
.og-item:last-child { margin-bottom: 0; }
.og-img {
  width: 110rpx; height: 110rpx; border-radius: 14rpx;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.og-emoji { font-size: 58rpx; }
.og-info { flex: 1; margin-left: 18rpx; }
.og-name { font-size: 26rpx; font-weight: 600; color: #222; }
.og-spec { font-size: 22rpx; color: #999; margin-top: 8rpx; }
.og-right { text-align: right; }
.og-price { font-size: 26rpx; color: #333; }
.og-count { font-size: 22rpx; color: #999; margin-top: 8rpx; }

.oc-foot {
  display: flex; justify-content: space-between; align-items: center;
  padding-top: 20rpx; border-top: 2rpx solid #f7f7f9;
  font-size: 22rpx; color: #999;
}
.oc-total { font-size: 24rpx; color: #666; }
.oc-price { color: #e0698c; font-size: 30rpx; font-weight: 700; }

.oc-actions {
  display: flex; justify-content: flex-end; margin-top: 24rpx;
}
.oc-btn {
  padding: 12rpx 36rpx; border: 2rpx solid #ddd;
  border-radius: 32rpx; font-size: 24rpx; color: #666;
  margin-left: 20rpx;
}
.oc-btn.primary { border-color: #e0698c; color: #e0698c; }

.empty {
  padding-top: 220rpx; display: flex; flex-direction: column;
  align-items: center;
}
.empty-ico { font-size: 130rpx; opacity: .35; }
.empty-text { font-size: 26rpx; color: #bbb; margin-top: 24rpx; }

7. 订单详情页

HTML/XML<!-- pages/order-detail/order-detail.wxml -->
<view class="page" wx:if="{{order}}">
  <view class="status-bar">
    <text class="sb-ico">{{statusIcon}}</text>
    <view class="sb-info">
      <view class="sb-title">{{order.statusText}}</view>
      <view class="sb-sub">{{statusDesc}}</view>
    </view>
  </view>

  <view class="card">
    <view class="addr-top">
      <text class="addr-name">{{order.address.name}}</text>
      <text class="addr-phone">{{order.address.phone}}</text>
    </view>
    <view class="addr-detail">{{order.address.region}} {{order.address.detail}}</view>
  </view>

  <view class="card">
    <view class="card-title">配送信息</view>
    <view class="info-row"><text class="ir-label">配送时间</text><text class="ir-value">{{order.deliveryTime}}</text></view>
    <view class="info-row" wx:if="{{order.blessing}}"><text class="ir-label">贺卡留言</text><text class="ir-value">{{order.blessing}}</text></view>
    <view class="info-row" wx:if="{{order.remark}}"><text class="ir-label">订单备注</text><text class="ir-value">{{order.remark}}</text></view>
  </view>

  <view class="card">
    <view class="card-title">商品清单</view>
    <view class="oi-item" wx:for="{{order.items}}" wx:key="key">
      <view class="oi-img" style="background:{{item.bg}}">
        <text class="oi-emoji">{{item.emoji}}</text>
      </view>
      <view class="oi-info">
        <view class="oi-name">{{item.name}}</view>
        <view class="oi-spec">{{item.specName}}</view>
      </view>
      <view class="oi-right">
        <view class="oi-price">¥{{item.price}}</view>
        <view class="oi-count">×{{item.count}}</view>
      </view>
    </view>
  </view>

  <view class="card">
    <view class="fee-row"><text>商品金额</text><text>¥{{order.goodsTotal}}</text></view>
    <view class="fee-row"><text>配送费</text><text>{{order.deliveryFee == '0.00' ? '免配送费' : '¥' + order.deliveryFee}}</text></view>
    <view class="fee-row" wx:if="{{order.discount !== '0.00'}}">
      <text>满减优惠</text><text class="discount">-¥{{order.discount}}</text>
    </view>
    <view class="fee-row total">
      <text>实付金额</text>
      <text class="total-price"><text class="unit">¥</text>{{order.total}}</text>
    </view>
  </view>

  <view class="card">
    <view class="info-row"><text class="ir-label">订单编号</text><text class="ir-value">{{order.id}}</text></view>
    <view class="info-row"><text class="ir-label">下单时间</text><text class="ir-value">{{order.createTime}}</text></view>
  </view>

  <view style="height:60rpx"></view>
</view>
JavaScript// pages/order-detail/order-detail.js
const STATUS_MAP = {
  pending: { text: '待配送', icon: '🌸', desc: '花艺师正在精心准备中' },
  delivering: { text: '配送中', icon: '🛵', desc: '骑手正在飞奔,请保持电话畅通' },
  done: { text: '已完成', icon: '💐', desc: '感谢您的惠顾,期待再次相见' }
}

Page({
  data: {
    order: null,
    statusIcon: '🌸',
    statusDesc: ''
  },

  onLoad(options) {
    const orders = wx.getStorageSync('FLOWER_ORDERS') || []
    const order = orders.find(o => o.id === options.id)
    if (!order) {
      wx.showToast({ title: '订单不存在', icon: 'none' })
      setTimeout(() => wx.navigateBack(), 800)
      return
    }
    const st = STATUS_MAP[order.status] || STATUS_MAP.pending
    this.setData({
      order: { ...order, statusText: st.text },
      statusIcon: st.icon,
      statusDesc: st.desc
    })
  }
})
css/* pages/order-detail/order-detail.wxss */
.page { min-height: 100vh; background: #f7f7f8; padding-bottom: 40rpx; }

.status-bar {
  display: flex; align-items: center;
  background: linear-gradient(135deg, #f48fb1, #e0698c);
  padding: 44rpx 40rpx; color: #fff;
}
.sb-ico { font-size: 80rpx; margin-right: 26rpx; }
.sb-title { font-size: 38rpx; font-weight: 700; }
.sb-sub { font-size: 24rpx; opacity: .9; margin-top: 12rpx; }

.card {
  background: #fff; margin: 20rpx 24rpx; border-radius: 20rpx; padding: 28rpx;
}
.card-title { font-size: 28rpx; font-weight: 700; color: #222; margin-bottom: 24rpx; }

.addr-top { display: flex; align-items: center; margin-bottom: 12rpx; }
.addr-name { font-size: 30rpx; font-weight: 700; color: #222; }
.addr-phone { font-size: 26rpx; color: #666; margin-left: 20rpx; }
.addr-detail { font-size: 25rpx; color: #666; line-height: 38rpx; }

.info-row { display: flex; margin-bottom: 18rpx; }
.info-row:last-child { margin-bottom: 0; }
.ir-label { font-size: 25rpx; color: #999; width: 140rpx; flex-shrink: 0; }
.ir-value { font-size: 25rpx; color: #333; flex: 1; }

.oi-item { display: flex; align-items: center; margin-bottom: 24rpx; }
.oi-item:last-child { margin-bottom: 0; }
.oi-img {
  width: 120rpx; height: 120rpx; border-radius: 14rpx;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.oi-emoji { font-size: 64rpx; }
.oi-info { flex: 1; margin-left: 20rpx; }
.oi-name { font-size: 27rpx; font-weight: 600; color: #222; }
.oi-spec { font-size: 22rpx; color: #999; margin-top: 10rpx; }
.oi-right { text-align: right; }
.oi-price { font-size: 27rpx; color: #333; font-weight: 600; }
.oi-count { font-size: 22rpx; color: #999; margin-top: 8rpx; }

.fee-row {
  display: flex; justify-content: space-between;
  font-size: 26rpx; color: #666; padding: 14rpx 0;
}
.fee-row .discount { color: #e0698c; }
.fee-row.total {
  border-top: 2rpx solid #f5f5f7; margin-top: 12rpx;
  padding-top: 24rpx; font-size: 28rpx; color: #222; font-weight: 600;
}
.total-price { color: #e0698c; font-size: 38rpx; font-weight: 700; }
.unit { font-size: 22rpx; }

8. 我的

HTML/XML<!-- pages/mine/mine.wxml -->
<view class="page">
  <view class="header">
    <view class="avatar">🌷</view>
    <view class="user-info">
      <view class="nickname">花间集用户</view>
      <view class="uid">让每一束花,都替我拥抱你</view>
    </view>
  </view>

  <view class="order-panel">
    <view class="op-head" bindtap="goOrders" data-status="all">
      <text class="op-title">我的订单</text>
      <text class="op-more">全部订单 ›</text>
    </view>
    <view class="op-tabs">
      <view class="op-tab" wx:for="{{orderTabs}}" wx:key="key"
            data-status="{{item.key}}" bindtap="goOrders">
        <view class="op-ico">{{item.emoji}}</view>
        <text class="op-name">{{item.name}}</text>
        <view class="op-badge" wx:if="{{item.count > 0}}">{{item.count}}</view>
      </view>
    </view>
  </view>

  <view class="menu">
    <view class="menu-item" bindtap="goAddress">
      <text class="mi-ico">📍</text>
      <text class="mi-text">收货地址</text>
      <text class="mi-arrow"></text>
    </view>
    <view class="menu-item" bindtap="contact">
      <text class="mi-ico">📞</text>
      <text class="mi-text">联系客服</text>
      <text class="mi-arrow"></text>
    </view>
    <view class="menu-item" bindtap="about">
      <text class="mi-ico">🌸</text>
      <text class="mi-text">关于花间集</text>
      <text class="mi-arrow"></text>
    </view>
  </view>

  <view class="foot-tip">花间集 · 鲜花配送 v1.0.0</view>
</view>
JavaScript// pages/mine/mine.js
Page({
  data: {
    orderTabs: [
      { key: 'pending', name: '待配送', emoji: '🌸', count: 0 },
      { key: 'delivering', name: '配送中', emoji: '🛵', count: 0 },
      { key: 'done', name: '已完成', emoji: '💐', count: 0 }
    ]
  },

  onShow() {
    const orders = wx.getStorageSync('FLOWER_ORDERS') || []
    const tabs = this.data.orderTabs.map(t => ({
      ...t,
      count: orders.filter(o => o.status === t.key).length
    }))
    this.setData({ orderTabs: tabs })
  },

  goOrders(e) {
    const status = e.currentTarget.dataset.status || 'all'
    wx.setStorageSync('ORDER_TAB', status)
    wx.navigateTo({ url: '/pages/order-list/order-list' })
  },

  goAddress() {
    wx.navigateTo({ url: '/pages/address/address' })
  },

  contact() {
    wx.showModal({
      title: '联系客服',
      content: '客服热线:400-888-8888\n服务时间:08:00 - 22:00',
      showCancel: false,
      confirmColor: '#e0698c'
    })
  },

  about() {
    wx.showModal({
      title: '关于花间集',
      content: '专注鲜花同城配送,3小时送达,只为把最新鲜的美好送给你。',
      showCancel: false,
      confirmColor: '#e0698c'
    })
  }
})
css/* pages/mine/mine.wxss */
.page { min-height: 100vh; background: #f7f7f8; }

.header {
  background: linear-gradient(135deg, #f48fb1, #e0698c);
  padding: 70rpx 44rpx 100rpx;
  display: flex; align-items: center;
}
.avatar {
  width: 130rpx; height: 130rpx; border-radius: 50%;
  background: rgba(255,255,255,.25);
  display: flex; align-items: center; justify-content: center;
  font-size: 68rpx; margin-right: 28rpx;
}
.nickname { font-size: 36rpx; font-weight: 700; color: #fff; }
.uid { font-size: 23rpx; color: rgba(255,255,255,.85); margin-top: 14rpx; }

.order-panel {
  background: #fff; margin: -60rpx 24rpx 0;
  border-radius: 24rpx; padding: 30rpx 20rpx;
  box-shadow: 0 8rpx 30rpx rgba(224,105,140,.12);
}
.op-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0 12rpx 24rpx; border-bottom: 2rpx solid #f7f7f9;
}
.op-title { font-size: 30rpx; font-weight: 700; color: #222; }
.op-more { font-size: 24rpx; color: #999; }

.op-tabs { display: flex; padding-top: 28rpx; }
.op-tab {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; position: relative;
}
.op-ico { font-size: 52rpx; }
.op-name { font-size: 23rpx; color: #666; margin-top: 12rpx; }
.op-badge {
  position: absolute; top: -8rpx; right: 34rpx;
  min-width: 32rpx; height: 32rpx; padding: 0 8rpx;
  background: #e0698c; color: #fff; font-size: 20rpx;
  border-radius: 16rpx; display: flex;
  align-items: center; justify-content: center; box-sizing: border-box;
}

.menu {
  background: #fff; margin: 24rpx; border-radius: 24rpx;
  padding: 0 28rpx;
}
.menu-item {
  display: flex; align-items: center; height: 108rpx;
  border-bottom: 2rpx solid #f7f7f9;
}
.menu-item:last-child { border-bottom: none; }
.mi-ico { font-size: 36rpx; margin-right: 22rpx; }
.mi-text { flex: 1; font-size: 28rpx; color: #333; }
.mi-arrow { font-size: 34rpx; color: #ccc; }

.foot-tip {
  text-align: center; font-size: 22rpx; color: #ccc;
  margin-top: 60rpx;
}

9. 地址页

HTML/XML<!-- pages/address/address.wxml -->
<view class="page">
  <view class="form">
    <view class="form-row">
      <text class="fr-label">收货人</text>
      <input class="fr-input" placeholder="请输入姓名" placeholder-class="ph"
             value="{{form.name}}" data-field="name" bindinput="onInput" />
    </view>
    <view class="form-row">
      <text class="fr-label">手机号</text>
      <input class="fr-input" type="number" maxlength="11"
             placeholder="请输入手机号" placeholder-class="ph"
             value="{{form.phone}}" data-field="phone" bindinput="onInput" />
    </view>
    <view class="form-row">
      <text class="fr-label">所在地区</text>
      <input class="fr-input" placeholder="如:浙江省杭州市西湖区" placeholder-class="ph"
             value="{{form.region}}" data-field="region" bindinput="onInput" />
    </view>
    <view class="form-row">
      <text class="fr-label">详细地址</text>
      <input class="fr-input" placeholder="街道、门牌号" placeholder-class="ph"
             value="{{form.detail}}" data-field="detail" bindinput="onInput" />
    </view>
    <view class="form-row no-border">
      <text class="fr-label">设为默认</text>
      <switch class="fr-switch" checked="{{form.isDefault}}"
              color="#e0698c" bindchange="onSwitch" />
    </view>
  </view>

  <view class="save-btn" bindtap="save">保存地址</view>
</view>
JavaScript// pages/address/address.js
Page({
  data: {
    form: {
      name: '',
      phone: '',
      region: '',
      detail: '',
      isDefault: true
    }
  },

  onLoad() {
    const saved = wx.getStorageSync('FLOWER_ADDRESS')
    if (saved) this.setData({ form: saved })
  },

  onInput(e) {
    const field = e.currentTarget.dataset.field
    this.setData({ [`form.${field}`]: e.detail.value })
  },

  onSwitch(e) {
    this.setData({ 'form.isDefault': e.detail.value })
  },

  save() {
    const { name, phone, region, detail } = this.data.form

    if (!name.trim()) return wx.showToast({ title: '请填写收货人', icon: 'none' })
    if (!/^1\d{10}$/.test(phone)) return wx.showToast({ title: '手机号格式不正确', icon: 'none' })
    if (!region.trim()) return wx.showToast({ title: '请填写所在地区', icon: 'none' })
    if (!detail.trim()) return wx.showToast({ title: '请填写详细地址', icon: 'none' })

    wx.setStorageSync('FLOWER_ADDRESS', this.data.form)
    wx.showToast({ title: '保存成功', icon: 'success' })
    setTimeout(() => wx.navigateBack(), 700)
  }
})
css/* pages/address/address.wxss */
.page { min-height: 100vh; background: #f7f7f8; padding-top: 24rpx; }

.form { background: #fff; margin: 0 24rpx; border-radius: 20rpx; padding: 0 28rpx; }
.form-row {
  display: flex; align-items: center; height: 110rpx;
  border-bottom: 2rpx solid #f7f7f9;
}
.form-row.no-border { border-bottom: none; }
.fr-label { width: 170rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
.fr-input { flex: 1; font-size: 28rpx; color: #222; }
.ph { color: #c8c8c8; font-size: 27rpx; }
.fr-switch { transform: scale(.85); }

.save-btn {
  margin: 60rpx 24rpx 0; height: 92rpx;
  background: linear-gradient(135deg, #f48fb1, #e0698c);
  color: #fff; border-radius: 46rpx; font-size: 31rpx;
  font-weight: 600; display: flex; align-items: center; justify-content: center;
}

10. 全局配置文件

json// app.json
{
  "pages": [
    "pages/index/index",
    "pages/category/category",
    "pages/cart/cart",
    "pages/mine/mine",
    "pages/detail/detail",
    "pages/order-confirm/order-confirm",
    "pages/order-list/order-list",
    "pages/order-detail/order-detail",
    "pages/address/address"
  ],
  "window": {
    "backgroundTextStyle": "dark",
    "navigationBarBackgroundColor": "#ffffff",
    "navigationBarTitleText": "花间集",
    "navigationBarTextStyle": "black",
    "backgroundColor": "#f7f7f8"
  },
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#e0698c",
    "backgroundColor": "#ffffff",
    "borderStyle": "white",
    "list": [
      { "pagePath": "pages/index/index", "text": "首页" },
      { "pagePath": "pages/category/category", "text": "分类" },
      { "pagePath": "pages/cart/cart", "text": "购物车" },
      { "pagePath": "pages/mine/mine", "text": "我的" }
    ]
  },
  "style": "v2",
  "sitemapLocation": "sitemap.json"
}
JavaScript// app.js
const store = require('./utils/store')

App({
  onLaunch() {
    // 首次启动初始化
    if (!wx.getStorageSync('FLOWER_ORDERS')) {
      wx.setStorageSync('FLOWER_ORDERS', [])
    }
  },

  onShow() {
    // 延迟同步购物车角标,等待 tabBar 渲染完成
    setTimeout(() => store.syncBadge(), 300)
  },

  globalData: {
    version: '1.0.0'
  }
})
css/* app.wxss */
page {
  background: #f7f7f8;
  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC",
               "Helvetica Neue", Helvetica, sans-serif;
  color: #333;
  font-size: 28rpx;
  -webkit-font-smoothing: antialiased;
}

view, text, input, button {
  box-sizing: border-box;
}

image { display: block; }
JavaScript// utils/util.js
function pad(n) {
  return n < 10 ? '0' + n : '' + n
}

function formatTime(date) {
  const d = date || new Date()
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
         `${pad(d.getHours())}:${pad(d.getMinutes())}`
}

function money(n) {
  return (Math.round(Number(n) * 100) / 100).toFixed(2)
}

module.exports = { formatTime, money }
JavaScript// utils/store.js
const CART_KEY = 'FLOWER_CART'

/* ---------- 基础读写 ---------- */
function getCart() {
  try {
    return wx.getStorageSync(CART_KEY) || []
  } catch (e) {
    return []
  }
}

function saveCart(cart) {
  try {
    wx.setStorageSync(CART_KEY, cart)
  } catch (e) {}
  syncBadge(cart)
}

/* ---------- 角标 ---------- */
function syncBadge(cart) {
  const list = cart || getCart()
  const count = list.reduce((s, i) => s + (i.count || 0), 0)

  if (count > 0) {
    wx.setTabBarBadge({
      index: 2,
      text: count > 99 ? '99+' : String(count),
      fail() {}
    })
  } else {
    wx.removeTabBarBadge({ index: 2, fail() {} })
  }
}

function totalCount() {
  return getCart().reduce((s, i) => s + (i.count || 0), 0)
}

/* ---------- 增删改 ---------- */
function addToCart(goods, specName, price, count) {
  const cart = getCart()
  const key = goods.id + '_' + specName
  const idx = cart.findIndex(i => i.key === key)

  if (idx > -1) {
    cart[idx].count += count
  } else {
    cart.push({
      key,
      goodsId: goods.id,
      name: goods.name,
      emoji: goods.emoji,
      bg: goods.bg,
      price,
      specName,
      count,
      checked: true
    })
  }
  saveCart(cart)
  return cart
}

function updateCount(key, count) {
  const cart = getCart()
  const idx = cart.findIndex(i => i.key === key)
  if (idx > -1) {
    if (count <= 0) cart.splice(idx, 1)
    else cart[idx].count = count
  }
  saveCart(cart)
  return cart
}

function removeItem(key) {
  const cart = getCart().filter(i => i.key !== key)
  saveCart(cart)
  return cart
}

function toggleCheck(key, checked) {
  const cart = getCart()
  const idx = cart.findIndex(i => i.key === key)
  if (idx > -1) cart[idx].checked = checked
  saveCart(cart)
  return cart
}

function toggleAll(checked) {
  const cart = getCart().map(i => ({ ...i, checked }))
  saveCart(cart)
  return cart
}

function getChecked() {
  return getCart().filter(i => i.checked)
}

function clearChecked() {
  const cart = getCart().filter(i => !i.checked)
  saveCart(cart)
  return cart
}

function clearCart() {
  saveCart([])
}

module.exports = {
  getCart,
  saveCart,
  syncBadge,
  totalCount,
  addToCart,
  updateCount,
  removeItem,
  toggleCheck,
  toggleAll,
  getChecked,
  clearChecked,
  clearCart
}
JavaScript// utils/mock.js
/* 分类 */
const categories = [
  { id: 'all',     name: '全部',     emoji: '💐', bg: 'linear-gradient(135deg,#ffe0e6,#ffc2d1)', desc: '把最美的都给你' },
  { id: 'rose',    name: '玫瑰',     emoji: '🌹', bg: 'linear-gradient(135deg,#ffd6e0,#ffb3c6)', desc: '热烈而深沉的爱' },
  { id: 'lily',    name: '百合',     emoji: '🌼', bg: 'linear-gradient(135deg,#fff3d6,#ffe0a3)', desc: '百年好合,纯洁祝福' },
  { id: 'mixed',   name: '混搭花束', emoji: '💐', bg: 'linear-gradient(135deg,#e0f7fa,#b2ebf2)', desc: '五彩斑斓的心情' },
  { id: 'eternal', name: '永生花',   emoji: '🌸', bg: 'linear-gradient(135deg,#f3e5f5,#e1bee7)', desc: '永不凋零的浪漫' },
  { id: 'basket',  name: '花篮',     emoji: '🧺', bg: 'linear-gradient(135deg,#e8f5e9,#c8e6c9)', desc: '开业庆典首选' },
  { id: 'plant',   name: '绿植',     emoji: '🪴', bg: 'linear-gradient(135deg,#e8f5e9,#a5d6a7)', desc: '给家添一抹生机' }
]

/* 首页快捷入口 */
const quickCats = categories.filter(c => c.id !== 'all').slice(0, 5)

/* 轮播 */
const banners = [
  { id: 1, title: '春日花礼季', sub: '全场满 299 减 30', emoji: '💐', bg: 'linear-gradient(135deg,#ff9a9e,#fecfef)' },
  { id: 2, title: '挚爱红玫瑰', sub: '11 支仅需 ¥199',   emoji: '🌹', bg: 'linear-gradient(135deg,#f78ca0,#f9748f)' },
  { id: 3, title: '永生花系列', sub: '把浪漫定格成永恒', emoji: '🌸', bg: 'linear-gradient(135deg,#a18cd1,#fbc2eb)' }
]

/* 商品原始数据 */
const rawGoods = [
  {
    id: 1, name: '挚爱红玫瑰', subtitle: '11支红玫瑰 · 满天星点缀',
    originPrice: 259, emoji: '🌹',
    bg: 'linear-gradient(135deg,#ffe0e6,#ffc2d1)',
    tags: ['热销', '送恋人'], categoryId: 'rose', sales: 1286,
    specs: [
      { name: '11支', price: 199 },
      { name: '19支', price: 299 },
      { name: '33支', price: 499 }
    ],
    desc: ['精选云南A级红玫瑰,花头饱满', '搭配满天星与尤加利叶', '当日清晨采摘,冷链直送']
  },
  {
    id: 2, name: '初恋粉玫瑰', subtitle: '19支粉玫瑰 · 粉色包装',
    originPrice: 329, emoji: '🌷',
    bg: 'linear-gradient(135deg,#ffeef2,#ffd9e3)',
    tags: ['新品'], categoryId: 'rose', sales: 862,
    specs: [
      { name: '19支', price: 269 },
      { name: '33支', price: 429 }
    ],
    desc: ['厄瓜多尔进口粉玫瑰', '韩式雾面纸包装', '附赠手写贺卡']
  },
  {
    id: 3, name: '白月光百合', subtitle: '6支香水百合 · 清新淡雅',
    originPrice: 268, emoji: '🌼',
    bg: 'linear-gradient(135deg,#fff8e1,#ffe9a8)',
    tags: ['清香'], categoryId: 'lily', sales: 534,
    specs: [
      { name: '6支', price: 218 },
      { name: '10支', price: 328 }
    ],
    desc: ['香气馥郁的香水百合', '适合探望与祝福', '花苞饱满,可绽放7天以上']
  },
  {
    id: 4, name: '春日限定混搭', subtitle: '玫瑰+洋桔梗+尤加利',
    originPrice: 358, emoji: '💐',
    bg: 'linear-gradient(135deg,#e0f7fa,#b2ebf2)',
    tags: ['限定', '设计师款'], categoryId: 'mixed', sales: 743,
    specs: [
      { name: '标准款', price: 298 },
      { name: '豪华款', price: 428 }
    ],
    desc: ['花艺师手工搭配', '当季新鲜花材', '自然风牛皮纸包装']
  },
  {
    id: 5, name: '星空永生花盒', subtitle: '永生玫瑰 · 玻璃罩礼盒',
    originPrice: 468, emoji: '🌸',
    bg: 'linear-gradient(135deg,#f3e5f5,#e1bee7)',
    tags: ['送女友', '长久保存'], categoryId: 'eternal', sales: 421,
    specs: [
      { name: '小号', price: 388 },
      { name: '大号', price: 588 }
    ],
    desc: ['进口永生玫瑰,可保存3年', '附LED灯串,夜晚更浪漫', '精美礼盒包装,可直接送礼']
  },
  {
    id: 6, name: '开业庆典花篮', subtitle: '双层花篮 · 含贺幅',
    originPrice: 598, emoji: '🧺',
    bg: 'linear-gradient(135deg,#e8f5e9,#c8e6c9)',
    tags: ['商务'], categoryId: 'basket', sales: 268,
    specs: [
      { name: '单层', price: 398 },
      { name: '双层', price: 598 },
      { name: '三层', price: 898 }
    ],
    desc: ['含红色贺幅一条(20字内)', '同城3小时送达', '可指定送达时间段']
  },
  {
    id: 7, name: '多肉组合盆栽', subtitle: '5款多肉 · 陶瓷花盆',
    originPrice: 168, emoji: '🪴',
    bg: 'linear-gradient(135deg,#e8f5e9,#a5d6a7)',
    tags: ['好养活'], categoryId: 'plant', sales: 655,
    specs: [
      { name: '5款组合', price: 128 },
      { name: '8款组合', price: 188 }
    ],
    desc: ['随机搭配5种多肉', '附赠陶粒与营养土', '办公室桌面好伴侣']
  },
  {
    id: 8, name: '香槟玫瑰礼盒', subtitle: '21支香槟玫瑰 · 抽屉礼盒',
    originPrice: 428, emoji: '🥂',
    bg: 'linear-gradient(135deg,#fff8e1,#ffe082)',
    tags: ['高级感'], categoryId: 'rose', sales: 392,
    specs: [
      { name: '21支', price: 368 },
      { name: '33支', price: 528 }
    ],
    desc: ['香槟色玫瑰,温柔高级', '双层抽屉式礼盒', '可放置巧克力与饰品']
  },
  {
    id: 9, name: '向日葵阳光花束', subtitle: '5支向日葵 · 元气满满',
    originPrice: 218, emoji: '🌻',
    bg: 'linear-gradient(135deg,#fff9c4,#ffe082)',
    tags: ['毕业季'], categoryId: 'mixed', sales: 578,
    specs: [
      { name: '5支', price: 168 },
      { name: '9支', price: 268 }
    ],
    desc: ['新鲜向日葵,向阳而生', '适合毕业、升职、探望', '搭配满天星更显饱满']
  },
  {
    id: 10, name: '蓝色妖姬', subtitle: '11支染色蓝玫瑰',
    originPrice: 388, emoji: '💙',
    bg: 'linear-gradient(135deg,#e3f2fd,#bbdefb)',
    tags: ['稀有'], categoryId: 'rose', sales: 216,
    specs: [
      { name: '11支', price: 328 },
      { name: '19支', price: 488 }
    ],
    desc: ['神秘蓝玫瑰,独一无二', '花语:奇迹与不可能实现的事', '黑色雾面纸包装']
  },
  {
    id: 11, name: '永生花玻璃球', subtitle: '迷你款 · 桌面摆件',
    originPrice: 158, emoji: '🔮',
    bg: 'linear-gradient(135deg,#fce4ec,#f8bbd0)',
    tags: ['小预算'], categoryId: 'eternal', sales: 934,
    specs: [
      { name: '粉色', price: 118 },
      { name: '蓝色', price: 118 },
      { name: '紫色', price: 128 }
    ],
    desc: ['永生玫瑰+满天星', '高透玻璃球罩', '可作夜灯摆件']
  },
  {
    id: 12, name: '百合混搭花束', subtitle: '百合+玫瑰+桔梗',
    originPrice: 328, emoji: '💮',
    bg: 'linear-gradient(135deg,#fff3e0,#ffe0b2)',
    tags: ['探望'], categoryId: 'lily', sales: 367,
    specs: [
      { name: '标准款', price: 268 },
      { name: '加大款', price: 388 }
    ],
    desc: ['百合为主花,寓意美好', '色彩柔和,适合长辈', '附赠祝福贺卡']
  }
]

/* 补充 price 字段(取最低规格价) */
const goods = rawGoods.map(g => ({
  ...g,
  price: g.specs[0].price
}))

module.exports = { categories, quickCats, banners, goods }
json// sitemap.json
{
  "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
  "rules": [
    {
      "action": "allow",
      "page": "*"
    }
  ]
}
json// project.config.json
{
  "description": "花间集 · 鲜花配送小程序",
  "packOptions": {
    "ignore": [],
    "include": []
  },
  "setting": {
    "coverView": true,
    "es6": true,
    "enhance": true,
    "postcss": true,
    "minified": true,
    "urlCheck": false
  },
  "compileType": "miniprogram",
  "libVersion": "3.0.0",
  "appid": "touristappid",
  "projectname": "flower-shop",
  "miniprogramRoot": "./"
}

使用说明

  1. 创建项目:微信开发者工具 → 新建小程序项目 → 选择空目录 → 把上述文件按目录结构放好。AppID 可先用测试号。
  2. 目录结构
纯文本flower-shop/
├── app.js / app.json / app.wxss / sitemap.json / project.config.json
├── utils/
│   ├── mock.js     商品与分类数据
│   ├── store.js    购物车状态管理(Storage 持久化)
│   └── util.js     时间 / 金额格式化
└── pages/
    ├── index/          首页
    ├── category/       分类
    ├── detail/         商品详情
    ├── cart/           购物车
    ├── order-confirm/  订单确认
    ├── order-list/     订单列表
    ├── order-detail/   订单详情
    ├── mine/           个人中心
    └── address/        收货地址
  1. 无需图片资源:所有商品图用「渐变背景 + Emoji」呈现,避免配置合法域名,克隆即可跑通。
  2. 接入真实后端:把 utils/mock.js 的数据请求换成 wx.request,把 utils/store.js 里的 Storage 读写换成服务端接口即可,页面逻辑基本不用改。
  3. 功能亮点
    • 购物车全选/单选、数量增减、实时金额计算
    • TabBar 购物车角标自动同步
    • 满 199 免运费、满 299 减 30 的营销规则
    • 配送时间段选择 + 贺卡留言
    • 订单状态流转与筛选

优化建议: 商品数据和配送时段目前放在 utils/mock.js 里写死,您可以把它换成后端接口返回的数据,后续维护会更方便。

DISCUSSION

评论

登录后参与
请使用网站前台用户账号登录后发表评论。

正在加载评论...


合作热线
18865460927
公司地址
山东省东营市垦利区兴隆路8-5号
产品咨询
产品咨询
Copyright © 2014-2026 东营码良软件开发 Inc. 版权所有鲁ICP备2025202136号-2 | 经营许可证编号:豫B2-20190103豫公网安备41019602002340