From 80913d3478b5b5dcca2cedbe5f05fd99be0c1a1a Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 8 Oct 2025 19:56:23 +0800 Subject: [PATCH] feat(web): add menu item image upload and image-based ordering grid; compress images client-side and persist in menu definition --- web/src/pages/MenuMVP.tsx | 92 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/web/src/pages/MenuMVP.tsx b/web/src/pages/MenuMVP.tsx index 9223c970e..8df0dcfe4 100644 --- a/web/src/pages/MenuMVP.tsx +++ b/web/src/pages/MenuMVP.tsx @@ -9,7 +9,7 @@ import { Visibility } from "@/types/proto/api/v1/memo_service"; import { toast } from "react-hot-toast"; import MenuOrdersView from "@/components/MenuOrdersView"; -type MenuItem = { id: string; name: string; price?: number }; +type MenuItem = { id: string; name: string; price?: number; image?: string }; type Menu = { id: string; name: string; items: MenuItem[] }; const STORAGE_KEY = "memos.menu.mvp"; @@ -96,6 +96,51 @@ const MenuMVP = () => { saveMenus(next); }; + // 图片读取/压缩与设置 + const fileToDataUrl = (file: File) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result)); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + + const resizeImage = (src: string, max: number, quality = 0.8) => + new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + let w = img.width; + let h = img.height; + const scale = Math.min(1, max / Math.max(w, h)); + w = Math.round(w * scale); + h = Math.round(h * scale); + const canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d"); + if (ctx) { + ctx.drawImage(img, 0, 0, w, h); + resolve(canvas.toDataURL("image/jpeg", quality)); + } else { + resolve(src); + } + }; + img.onerror = () => resolve(src); + img.src = src; + }); + + const handleUploadImage = async (itemId: string, file?: File) => { + if (!file) return; + try { + const dataUrl = await fileToDataUrl(file); + const resized = await resizeImage(dataUrl, 640, 0.8); + updateItem(itemId, { image: resized }); + } catch (e) { + console.error(e); + toast.error("图片处理失败"); + } + }; + const deleteItem = (itemId: string) => { if (!selectedMenu) return; const next = menus.map((m) => @@ -245,7 +290,7 @@ const MenuMVP = () => { merged.push({ id, name: im.name || id, - items: (im.items || []).map((it: any) => ({ id: it.id || slugify(it.name || "item"), name: it.name || "", price: it.price })) + items: (im.items || []).map((it: any) => ({ id: it.id || slugify(it.name || "item"), name: it.name || "", price: it.price, image: it.image })) }); } setMenus(merged); @@ -350,6 +395,49 @@ const MenuMVP = () => { setNote(e.target.value)} /> + {/* 图片选单(点击图片快速加购;可在卡片内上传/替换图片) */} + {selectedMenu.items.length > 0 && ( +
+
图片选单
+
+ {selectedMenu.items.map((it) => ( +
+ +
+ + +1 点图 +
+
+ ))} +
+
+ )}