【Vue H5项目实战】从0到1的肯德基点餐系统—— 提交订单设计(Vue3.2 + Vite + TS + Vant + Pinia + Nodejs + MongoDB)

前言

本系列将以肯德基自助点餐页面为模板,搭建一款自助点餐系统,第一次开发移动端h5项目,免不了有所差错和不足,欢迎各位大佬指正。在上一章我们已经完成了商品页面、购物车弹出层、导航栏双向联动等功能,但是提交订单按钮点击后还是空的,那么在这章,我们将要继续设计点击提交订单按钮后,修改或新增地址、确认订单、提交订单的功能。

一、路由设计

在之前的设计中,我们只在一个整体布局中进行操作,没有使用嵌套路由,只有一个<router-view>,但如果要涉及提交订单,就得设计新的订单页面,因此我们要把之前的路由改一改,改成嵌套路由的模式。

1.1、更改项目结构

首先新建一个pages文件夹放父页面,文件夹中创建一个Home.vue,将原本App.vue中的内容转移到Home.vue中,然后将App.vue改为:

<template>
  <router-view> </router-view>
</template>

<script setup lang="ts"></script>
<style lang="less"></style>

这里的App.vue中的<router-view> </router-view>放置父级路由(即page中的页面)。

Home.vue的内容如下:

<template>
  <!-- swipe轮播图 -->
  <Swipe></Swipe>
  <!-- header标题栏 -->
  <van-sticky
    ><Header></Header>
    <!-- navigation导航页 -->
    <Nav></Nav
  ></van-sticky>
  <!-- content内容页 包括侧边导航和主体 -->
  <router-view v-slot="{ Component }">
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </router-view>
</template>
<script setup lang="ts">
import Swipe from "@/components/swipe/Swipe.vue";
import Header from "@/components/header/Header.vue";
import Nav from "@/components/nav/Nav.vue";
</script>
<style lang="less"></style>

这里的Home.vue中的<router-view> </router-view>放置子级路由(即component)。

1.2、嵌套路由

修改router文件夹中index.ts为嵌套路由模式:

/**
 * createRouter 这个为创建路由的方法
 * createWebHashHistory 这个就是vue2中路由的模式,
 *                      这里的是hash模式,这个还可以是createWebHistory等
 * RouteRecordRaw 这个为要添加的路由记录,也可以说是routes的ts类型
 */
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
// 路由记录,这个跟vue2中用法一致,就不做过多解释了
const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "Home",
    component: () => import("@/pages/Home.vue"),
    alias: "/home",
    meta: {
      title: "点单页面",
    },
    children: [
      {
        path: "",
        name: "Goods",
        component: () => import("@/components/goods/Goods.vue"),
        alias: "/goods",
        meta: {
          title: "商品页面",
        },
      },
      {
        path: "kitchen",
        name: "Kitchen",
        component: () => import("@/components/kitchen/Kitchen.vue"),
        alias: "/kitchen",
        meta: {
          title: "自在厨房",
        },
      },
      {
        path: "about",
        name: "About",
        component: () => import("@/components/about/About.vue"),
        alias: "/about",
        meta: {
          title: "关于我们",
        },
      },
    ],
  },
  {
    path: "/order",
    name: "Order",
    component: () => import("@/pages/Order.vue"),
    alias: "/order",
    meta: {
      title: "订单页面",
    },
  },
];

const router = createRouter({
  history: createWebHashHistory(),
  routes,
});
export default router;

即将Goods.vueKitchen.vueAbout.vue作为home下的子路由,另一个父级路由为/order,pages文件夹下新建一个Order.vue,内容就写如下代码,进行测试

<template>
  <div>这是订单提交页面</div>
</template>
<script setup lang="ts"></script>
<style lang="less"></style>

然后我们可以在地址栏输入http://127.0.0.1:5173/#/order查看,如果成功跳转显示如下页面,则代表路由配置成功:

1.3、订单提交的跳转

这一步需要给前面商品和购物车弹出层的“提交订单”按钮增加点击事件sumbitCart(),点击后通过router.push来跳转路由,我们不采用路由传参的方式,因为购物车的store已经保存了提交订单所需的完整状态。

const sumbitCart = () => {
  router.push({ path: "/order", name: "Order" });
};

二、订单设计

2.1、订单类型设计

与商品的设计方法类似,我们在utils/interface/index.ts中同样设计一个shoppingOrder接口类型,其结构如下,包括店铺名、店铺地址、订单类型、取餐时间、订单列表、是否需要餐具和订单备注。

export interface shoppingOrder {
  // 店铺名
  shopName: string;
  // 店铺地址
  shopAddress: string;
  // 订单类型:外带或堂食
  orderType: string;
  // 取餐时间
  pickupTime: string;
  // 订单列表
  myCart: shoppingCart;
  // 是否需要餐具
  tableware: boolean;
  // 订单备注
  note: string;
}

然后同样在store/modules/modules.ts中设计useorderStore,如下:

export const useorderStore = defineStore("order", {
  state: (): shoppingOrder => {
    return {
      // 店铺名
      shopName: "",
      // 店铺地址
      shopAddress: "",
      // 订单类型:外带或堂食
      orderType: "堂食",
      // 取餐时间
      pickupTime: "",
      // 订单列表
      myCart: { carts: [], totalPrice: 0 },
      // 是否需要餐具
      tableware: false,
      // 订单备注
      note: "",
    };
  },
  /* 
        类似于组件的computed,用来封装计算属性,有缓存的功能
    */
  getters: {},
  /* 
        类似于methods,封装业务逻辑,修改state
    */
  actions: {
    // 清空订单
    clearOrder() {
      this.orderType = "";
      this.pickupTime = "";
      this.myCart = { carts: [], totalPrice: 0 };
      this.note = "";
    },
  },
});

2.2、订单布局分析

分析一下肯德基的提交订单的页面,主要可以分为,顶端的确认订单以及返回商品选择(固定),头部的说明(店铺、取餐时间和堂食/外带选项),中间的订单(包括购物车列表、优惠券信息),下部的选项(餐具、备注、开票)和最底部的确认提交订单。

2.3、订单布局设计

2.3.1、顶部说明

顶部很简单,包括左边的箭头用于回退到上一页面,中部的确认订单用于提示,需要注意的是头部需要使用粘性,这里可以采用<van-sticky>组件将其包裹起来,代码如下:

    <van-sticky>
          <div class="order-top">
        <van-icon name="arrow-left" size="20" @click="returnGoods()" />
        <div class="order-top-text">确认订单</div>
      </div></van-sticky
    >

2.3.2、底部确认提交订单

底部确认与前面的类似也是使用<van-sticky>组件,采用吸底方法:

<!-- 底部提交 -->
    <van-sticky position="bottom" offset-bottom="4vw"
      ><div class="cart">
        <div class="cart-content">
          <van-icon class="cart-content-icon" size="5vh" name="shopping-cart" />
          <div class="cart-content-num">
            <!-- 未选购商品 -->
            <span v-if="cartStore.totalPrice == 0">还未选购商品</span>
            <!-- 已选购商品 -->
            <span v-else>合计:¥{{ cartStore.totalPrice }}</span>
          </div>
          <van-button class="cart-content-button" @click="sumbitOrder()"
            >提交订单</van-button
          >
        </div>
      </div>
    </van-sticky>

2.3.3、头部信息

头部信息主要由店铺、取餐时间和堂食/外带选项组成,新建order文件夹,在order文件夹中新建一个header.vue用于写头部组件,在堂食和外带选项中,我们使用伪类元素::after和::before设计了选中进行标记,主要代码如下,不熟悉的同学可以看这篇文章:解决方案:实现Vue3.2+Vant点击选中按钮,右下角显示三角形勾选 + 破碎图片占位-CSDN博客

<template>
  <div class="order-header">
    <div class="order-header-shop">
      <van-icon name="hot" color="#ee0a24" size="10vw" />
      <div>
        <div class="shopname">{{ orderStore.shopName }}</div>
        <div class="shopaddress">{{ orderStore.shopAddress }}</div>
      </div>
    </div>

    <div class="order-header-type">
      <div class="type">
        <div class="type-content">
          <van-button
            type="default"
            size="large"
            :class="orderStore.orderType === '堂食' ? 'select' : ''"
            @click="clickType('堂食')"
          >
            <!-- <template #icon><van-icon name="shop-o" /> </template> -->
            <span class="type-content-text">堂食</span></van-button
          >
          <van-button
            type="default"
            size="large"
            :class="orderStore.orderType === '外带' ? 'select' : ''"
            @click="clickType('外带')"
            >外带</van-button
          >
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useorderStore } from "@/store/modules/module.js";

const orderStore = useorderStore();
const clickType = (str: string) => {
  orderStore.orderType = str;
};
</script>

<style lang="less" scoped>
.order-header {
  height: 10vh;
  background-color: #ffffff;
  border-radius: 4vw;

  .order-header-shop {
    display: flex;
    align-items: center;
    // text-align: center;
    .shopname {
      font-size: 5vw;
      font-weight: 400;
    }
    .shopaddress {
      font-size: 2vw;
    }
  }
  .order-header-type {
    .type {
      display: flex;
      .type-content {
        // align-items: center;
        // justify-content: center;
        display: flex;
        width: 100vw;
        height: 5vh;
        .select {
          position: relative;
          text-align: left;
          color: #00aaff;
          border: 0.5vw solid #ff335f;
          overflow: hidden;
          ::after {
            content: "";
            position: absolute;
            right: -3vw;
            bottom: -3vw;
            width: 6vw;
            height: 6vw;
            background-color: #ff335f;
            transform: rotate(45deg);
          }
          ::before {
            content: "";
            width: 1vw;
            height: 4vw;
            position: absolute;
            right: 0vw;
            bottom: 0vw;
            border: 0.5vw solid #fff;
            border-top-color: transparent;
            border-left-color: transparent;
            transform: rotate(45deg);
            z-index: 999;
          }
        }
      }
    }
  }
}
</style>

效果如下:

2.3.4、中部订单信息

首先来分析一下肯德基的中部订单内容:

可以看到,中部订单信息是这个页面的主体,其包括标题栏餐品详情字样、餐品列表、餐品推荐、下方的商品小计、卡券优惠和合计金额。餐品列表我们可以用循环+van-card来做,而原始的vant-card比较丑,我们用插槽的方法来重写一些样式和内容:

<div
        class="middle-cart"
        v-for="(item, index) in cartStore.carts"
        :key="index"
      >
        <van-card style="height: 8vh; margin-bottom: 2vw">
          <template #title>
            <div
              style="
                padding-left: 15vw;
                padding-top: 1vw;
                font-size: 3.5vw;
                font-weight: 600;
              "
            >
              {{ item.good.name }}
            </div>
          </template>
          <template #desc>
            <div
              style="
                padding-left: 15vw;
                padding-top: 1vw;
                font-size: 1vw;
                color: #868080;
              "
            >
              {{ item.good.description }}
            </div>
          </template>
          <template #num>
            <div>×{{ item.quantity }}</div>
          </template>
          <template #price>
            <div style="padding-left: 15vw; padding-top: 1vw; font-size: 3.5vw">
              ¥{{ item.good.price }}
            </div>
          </template>
          <template #thumb>
            <van-image
              width="25vw"
              style="max-height: 7vh"
              :src="item.good.image_path"
              alt="1"
            />
          </template>
        </van-card>
      </div>

餐品推荐我们暂时放一放,后面再来做,到本步,效果如下:

2.3.5、下部选项

下部订单选项主要都是一些信息的选择项,我们使用输入框field配合弹出层popup实现,即点击输入框后将editTableware置为true,弹出以van-picker为方法的选项栏,点击选项栏或者点击无关区域时将editTableware置为false。

在备注部分中,额外使用了button做为标签按钮,为他绑定事件,当点击对应标签后,自动加入备注:

这一部分完整的代码如下

<!-- src/components/order/Footer.vue -->
<template>
  <div class="order-footer">
    <div class="footer">
      <div class="footer-text">订单选项</div>
      <van-field
        v-model="time"
        is-link
        readonly
        label="取餐时间"
        placeholder=""
        @click="editTime = true"
      >
      </van-field>
      <van-field
        v-model="tableware"
        is-link
        readonly
        label="餐具选项"
        placeholder=""
        @click="editTableware = true"
      />
      <van-cell
        title="备注"
        :value="orderStore.note"
        is-link
        arrow-direction="right"
        @click="editNote = true"
      ></van-cell>

      <van-popup v-model:show="editTime" round position="bottom">
        <van-picker
          :columns="timeColumns"
          @cancel="editTime = false"
          @confirm="timeConfirm"
        />
      </van-popup>

      <van-popup v-model:show="editTableware" round position="bottom">
        <van-picker
          :columns="tablewareColumns"
          @cancel="editTableware = false"
          @confirm="tablewareConfirm"
        />
      </van-popup>
      <van-popup
        v-model:show="editNote"
        round
        closeable
        position="bottom"
        teleport="body"
        :style="{ height: '30%' }"
      >
        <van-field
          class="note"
          v-model="orderStore.note"
          style="background-color: #f7f7f7"
          rows="7"
          autosize
          label="备注"
          type="textarea"
          maxlength="100"
          placeholder="请输入备注"
          show-word-limit
        />
        <van-button
          style="margin-right: 2vw"
          color="#16A085"
          @click="addNote('不要香菜')"
          >不要香菜</van-button
        >
        <van-button
          style="color: #112af4; margin-right: 2vw"
          plain
          @click="addNote('不要辣')"
          >不要辣</van-button
        >
        <van-button
          style="margin-right: 2vw"
          color="#D68910"
          @click="addNote('微辣')"
          >微辣</van-button
        >
        <van-button
          style="margin-right: 2vw"
          color="linear-gradient(to right, #ff6034, #ee0a24)"
          @click="addNote('多放点辣')"
        >
          多放点辣
        </van-button>
        <van-button
          style="margin-right: 2vw"
          color="#cb4335"
          @click="addNote('放桌子上')"
          >放桌子上</van-button
        >
      </van-popup>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { useorderStore } from "@/store/modules/module.js";
const orderStore = useorderStore();
const editTime = ref(false);
const editTableware = ref(false);
const editNote = ref(false);
const timeColumns = [
  { text: "尽快取餐", value: "now" },
  { text: "15分钟内", value: "15min" },
  { text: "半小时内", value: "30min" },
  { text: "一小时内", value: "1hour" },
  { text: "一小时后", value: "later" },
];
const tablewareColumns = [
  { text: "无需餐具", value: "not-need" },
  { text: "需要餐具", value: "need" },
];

const time = ref("尽快取餐");
const tableware = ref("无需餐具");

const timeConfirm = ({ selectedOptions }: any) => {
  editTime.value = false;
  time.value = selectedOptions[0].text;
  orderStore.pickupTime = selectedOptions[0].text;
};

const tablewareConfirm = ({ selectedOptions }: any) => {
  editTableware.value = false;
  tableware.value = selectedOptions[0].text;
  orderStore.tableware = selectedOptions[0].text;
};

const addNote = (note: string) => {
  orderStore.note = orderStore.note + "#" + note + " ";
};
</script>

<style lang="less" scoped>
.order-footer {
  margin-left: 4vw;
  margin-right: 4vw;
  margin-top: 2vw;
  padding: 1vw;
  border-radius: 4vw;
  background-color: #ffffff;
  .footer {
    max-height: 40vh;
    overflow-y: scroll;
    .footer-text {
      font-size: 4vw;
      font-weight: 800;
      padding: 2vw 2vw;
    }
    van-contact-card {
      height: 20vh;
    }
  }
}
</style>
<style>
:root:root {
  --van-contact-card-title-line-height: 4vw;
  --van-dialog-message-line-height: 6vw;
  --van-field-label-width: 15vw;
}
</style>

到此,我们就已经成功设计好了订单页面的基本框架,最终订单的布局设计效果为:

  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

中杯可乐多加冰

请我喝杯可乐吧,我会多加冰!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值