这是一个可直接运行的鲜花配送小程序代码,涵盖首页浏览、商品详情、购物车、下单和订单管理等核心环节。
<!-- 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>// 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' })
}
})/* 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. 分类页
<!-- 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>// 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' })
}
})/* 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. 商品详情页
<!-- 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>// 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' })
}
})/* 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. 购物车页
<!-- 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>// 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' })
}
})/* 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. 订单确认页
<!-- 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>// 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)
}
})/* 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. 订单列表页
<!-- 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>// 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' })
}
})/* 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. 订单详情页
<!-- 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>// 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
})
}
})/* 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. 我的
<!-- 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>// 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'
})
}
})/* 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. 地址页
<!-- 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>// 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)
}
})/* 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. 全局配置文件
// 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"
}// 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'
}
})/* 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; }// 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 }// 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
}// 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 }// sitemap.json
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [
{
"action": "allow",
"page": "*"
}
]
}// 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": "./"
}使用说明
- 创建项目:微信开发者工具 → 新建小程序项目 → 选择空目录 → 把上述文件按目录结构放好。AppID 可先用测试号。
- 目录结构:
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/ 收货地址- 无需图片资源:所有商品图用「渐变背景 + Emoji」呈现,避免配置合法域名,克隆即可跑通。
- 接入真实后端:把
utils/mock.js的数据请求换成wx.request,把utils/store.js里的 Storage 读写换成服务端接口即可,页面逻辑基本不用改。 - 功能亮点:
- 购物车全选/单选、数量增减、实时金额计算
- TabBar 购物车角标自动同步
- 满 199 免运费、满 299 减 30 的营销规则
- 配送时间段选择 + 贺卡留言
- 订单状态流转与筛选
优化建议: 商品数据和配送时段目前放在 utils/mock.js 里写死,您可以把它换成后端接口返回的数据,后续维护会更方便。



