跟我一直做基于 RSBUILD+VANT4 的移动端点单页

37 阅读3分钟

需求说明

客户提供了菜品清单(图片+价格,如图1所示),想要做一个移动端的点单功能,安排😄。 图1

实施方案

  1. 技术栈选择:pnpm、rsbuildvue3vant4
  2. 页面布局参考了某茶的点单页面,样式则使用 UI 库默认值😄 图2

开工大吉🍊

项目初始化

然后安装相关依赖:pnpm i vant,最后的目录结构及index.js代码如下:

创建页面

<template>
    <div style="height:100%;display:flex; flex-direction:column;">
        <van-config-provider style="flex:1;overflow-y:auto;" :theme-vars="themeVars">
            <van-row class="h-full">
                <van-col span="5" class="h-full" style="overflow-y: auto; border-right: 1px solid white;">
                    <van-sidebar class="w-full" v-model="active" @change="changeType">
                        <van-sidebar-item v-for="item in indexList" :title="item" />
                    </van-sidebar>
                </van-col>
                <van-col span="19" class="h-full" style="overflow-y: auto;">
                    <van-index-bar ref="bar" :index-list="[]">
                        <template v-for="(menu, key) in menus">
                            <van-index-anchor :index="key">{{key}}</van-index-anchor>
                            <template v-for="item in menu" >
                                <van-card v-if="onlyCart?item.num:true" :price="item.price" :title="item.name" :thumb="item.thumb">
                                    <template #num>
                                        <van-stepper v-model="item.num" @change="onChange" min="0" max="99" theme="round" button-size="22"/>
                                    </template>
                                    <template #tags>
                                        <van-space style="margin: 6px 0px 6px 0px;">
                                            <van-tag plain v-if="item.tag" type="primary">{{item.tag}}</van-tag>
                                            <van-tag plain v-if="item.unit" type="primary">{{item.unit}}</van-tag>
                                        </van-space>
                                    </template>
                                </van-card>
                            </template>
                        </template>
                    </van-index-bar>
                </van-col>
            </van-row>
        </van-config-provider>

        <div style="height:55px;background-color:white;padding:0px">
            <van-action-bar>
                <van-action-bar-icon icon="cart-o" :badge="cart.count" text="购物车" :color="onlyCart?'#ff5000':'#323233'" @click="onlyCart=!onlyCart" />
                <div style="width: 200px;padding-left: 10px;"><span style="padding:0 10px 0px 0px;font-weight: bold;font-size: 1.1rem;">
                        {{cart.price}}
                        <template v-if="cart.unkonw">
                            + {{cart.unkonw}}个时价菜
                        </template>
                    </span>
                </div>
                <van-action-bar-button type="primary" size="large" text="提交预订" @click="toSave" />
            </van-action-bar>
        </div>
    </div>

    <van-popup v-model:show="cart.show" round position="bottom" :style="{ padding: '40px 10px 0px 10px', height:'540px' }" closeable>
        <van-cell-group style="height: 400px; overflow-y: auto;">
            <van-cell v-for="item in carts" center :title="item.name" :label="`单价:`+item.price" :value="'x'+item.num+' '+item.unit" />
        </van-cell-group>
        <van-button style="margin-top: 20px;" size="large" type="primary" round @click="saveDo">确认预订</van-button>
        <div style="text-align: center;">金额:{{cart.price}}<template v-if="cart.unkonw">+ {{cart.unkonw}}个时价菜</template></div>
    </van-popup>
</template>

<script setup>
    import { ref, computed, reactive } from 'vue'
    import menus from "../menus.json"

    const indexList = Object.keys(menus)
    const themeVars = { cardThumbSize:"64px", cardPriceIntegerFontSize:"1.5em",cardFontSize:"16px" }

    let bar = ref()
    let active = ref(0)
    let keyword = ref()
    let onlyCart = ref(false)       //只显示加入购物车的菜品
    let carts = ref([])
    let cart = reactive({price:0, count:0, unkonw:0, show:false})

    const total = computed(()=> carts.value.reduce((sum,v)=> sum+=v.price||0, 0))
    const changeType = i=>bar.value.scrollTo(indexList[i])
    const onChange = (v,d)=>{
        let dd = {price:0, count:0, unkonw:0}
        //计算总金额
        Object.keys(menus).forEach(k=>{
            let items = menus[k]
            items.forEach(m=>{
                if(m.num>0){
                    dd.count ++
                    if(isNaN(m.price))
                        dd.unkonw ++
                    else
                        dd.price += m.num*m.price
                }
            })
        })
        console.debug(dd)
        Object.assign(cart, dd)
    }
    const onSearch = ()=>{}
    const onCancel = ()=>{}
    const toSave = ()=>{
        if(cart.count<=0)   return M.notice.warn(`请先选择菜品`)
        let selects = []
        Object.keys(menus).forEach(k=>{
            selects.push(...menus[k].filter(m=>m.num).map(m=>({num:m.num, price:m.price, name:m.name, unit:m.unit})))
        })
        carts.value = selects
        cart.show = true
    }
    const saveDo = ()=>{
        let order = { count: cart.count, price:cart.price, unkonw:cart.unkonw, items:carts.value }
        console.debug(`订单信息:`, order)
    }
</script>

先来一波预览😄。大体的框架有了,下一步我们需要准备全部的菜单数据,Let's GO!

准备菜单数据

客户提供的数据源是 xlsx 文件,我准备写个解析器,将源文件的数据清洗到 JSON 格式(参考上一步骤的结构)

客户给的xlsx排版有点混乱,我们先把它整理下,符合标准化要求:

  1. 每个sheet为一个品类
  2. 每行相邻的两个单元格视为一个菜品(第一节是名称、价格),第二列为图片
  3. 图片不能超出所在单元格的边框(否则程序无法识别图片)
  4. 菜名数据格式如下图

数据整理完毕后,就可以进入清洗环节。这里使用`exceljs´ 读取表格文件,解析菜品及对应的图片后,写入到JSON文件。

/*
 * 菜单数据解析工具
 * @Author: 集成显卡
 * @Date: 2024-12-09 17:04:25
 * @Last Modified by: 集成显卡
 * @Last Modified time: 2024-12-10 14:56:54
 */
import chalk from 'chalk'
import ExcelJS from 'exceljs'
import sharp from 'sharp'
import { writeFileSync } from 'node:fs'

const originFile = "E:/菜单明细表价格(带图片).xlsx"

const workbook = new ExcelJS.Workbook()

let time = Date.now()
await workbook.xlsx.readFile(originFile)
console.debug(`读取 ${chalk.magenta(originFile)}(耗时 ${(Date.now()-time)}ms)`)

/**
 * 转换为 webp 格式的 base64 图片
 * @param {Buffer} buf
 * @returns {ExcelJS.Image}
 */
const toWebp = async buf=>{
    let newBuf = await sharp(buf).resize({width:80, withoutEnlargement:true}).toFormat('webp', {quality: 60, }).toBuffer()
    return newBuf.toString('base64')
}

const menus = {}

/**
 *
 * @param {ExcelJS.Worksheet} sheet
 */
const dealWithSheet = async sheet=>{
    console.group(chalk.magenta(`${sheet.name}(行数=${sheet.rowCount})`))
    let tag = undefined
    const images =  sheet.getImages()
    const items = []
    /**
     * @param {ExcelJS.Cell} cell
     */
    const _findImg = cell=>{
        let { col, row } = cell.fullAddress
        return images.find(img=> Math.ceil(img.range.br.col) == col && Math.ceil(img.range.br.row)==row )
    }

    for(let rowI=1;rowI<=sheet.rowCount;rowI++){
        let row = sheet.getRow(rowI)
        let cell = row.getCell(1)
        if(cell.isMerged){
            //对于合并单元格,视为大标签
            tag = cell.value
            continue
        }

        //处理菜品,两列为一个
        for(let cellI=1;cellI<=row.cellCount;cellI+=2){
            let name = row.getCell(cellI).value
            if(name == null)    continue
            //凉瓜排骨炖海螺汤/68(位),分别是菜名、价格、单位
            let match = name.trim().match(/(.+)\/(.+)((.+))/)
            if(match == null){
                console.debug(chalk.red(`菜名 ${name} 与正则不匹配...`))
                continue
            }
            //获取图片
            let img = _findImg(row.getCell(cellI+1))
            if(img){
                items.push({
                    name: match[1],
                    price: isNaN(match[2])?match[2]:parseInt(match[2]),
                    unit: match[3],
                    num:0,
                    tag,
                    thumb: `data:image/webp;base64,`+await toWebp(workbook.getImage(img.imageId).buffer)
                })
                console.debug(chalk.green(`解析菜名:${match[0]}`))
            }
            else{
                console.debug(chalk.yellow(`获取菜名 ${match[0]},但无对应的图片...`))
            }
        }
    }
    if(items.length)
        menus[sheet.name] = items
    console.groupEnd()
}

const sheets = workbook.worksheets
for (let i = 0; i < sheets.length; i++) {
    await dealWithSheet(sheets[i])
}

//写入文件
writeFileSync(`menus.json`, JSON.stringify(menus), {encoding:'utf-8'})
console.debug(`写入文件${chalk.magenta("成功")} :)`)

程序运行结果:

成果展示

结语

此文只是一个简单的例子,仅供参考😄,文中提及的代码完整版未来会发布到 GITHUB,如急需可私聊。