需求说明
客户提供了菜品清单(图片+价格,如图1所示),想要做一个移动端的点单功能,安排😄。
实施方案
开工大吉🍊
项目初始化
然后安装相关依赖:
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排版有点混乱,我们先把它整理下,符合标准化要求:
- 每个sheet为一个
品类
- 每行相邻的
两个单元格
视为一个菜品(第一节是名称、价格),第二列为图片 - 图片不能
超出所在单元格的边框
(否则程序无法识别图片) - 菜名数据格式如下图
数据整理完毕后,就可以进入清洗环节。这里使用`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,如急需可私聊。