001package polaris.sdk.ui.samples; 002 003import polaris.sdk.ui.*; 004import polaris.sdk.ui.controls.*; 005import java.util.*; 006import static polaris.sdk.ui.samples.SceneSupport.*; 007 008/** Local delivery ordering workflow built out of library controls; no payment or network. */ 009public final class FoodScene extends SceneSupport.AppScene { 010 public static final String[] NAMES={"照烧鸡腿饭","香煎牛肉饭","南瓜浓汤","田园轻食碗","冰柠檬茶","燕麦拿铁"}; 011 private static final int[] PRICES={2400,3200,1200,1800,800,1600}; 012 private static final int[] CATEGORIES={1,1,2,2,3,3}; 013 /** Integer-cent model for deterministic totals and repeatable example tests. */ 014 public static final class Cart { 015 private final int[] quantities=new int[NAMES.length];private int fulfillment;private boolean coupon; 016 public int quantity(int index){return quantities[index];} 017 public void quantity(int index,int value){quantities[index]=Math.max(0,Math.min(20,value));} 018 public int count(){int n=0;for(int q:quantities)n+=q;return n;} 019 public int subtotal(){int n=0;for(int i=0;i<quantities.length;i++)n+=quantities[i]*PRICES[i];return n;} 020 public int fee(){return count()==0||fulfillment==1?0:300;} 021 public int discount(){return coupon&&subtotal()>=3000?400:0;} 022 public int total(){return subtotal()+fee()-discount();} 023 public void fulfillment(int value){fulfillment=value==1?1:0;} 024 public void coupon(boolean value){coupon=value;} 025 public void clear(){Arrays.fill(quantities,0);} 026 } 027 private final Cart cart=new Cart();private final Panel menu=new Panel(),bag=new Panel(); 028 private final SearchField search=new SearchField();private final ButtonGroup categories=new ButtonGroup("全部","主食","轻食","饮品"); 029 private final Product[] products=new Product[NAMES.length];private final ScrollPane cartScroll=new ScrollPane(); 030 private final Label bagTitle=title("购物袋"),empty=caption("还没有商品,点一下「加入」。"),sum=title("¥0.00"),details=caption(""); 031 private final ButtonGroup fulfillment=new ButtonGroup("配送","到店自取"); 032 private final CheckBox coupon=new CheckBox("满 30 减 4 · 演示优惠");private final TextField note=new TextField(); 033 private final Button checkout=new Button("模拟下单").primary(true);private final CartLine[] lines=new CartLine[NAMES.length]; 034 private final Label orderState=caption("不连接商家,不收款,不产生真实订单。");private int category,orders; 035 private String lastOrder=""; 036 public FoodScene(){super("食光小馆","外卖点餐 / 菜单筛选、购物袋、价格计算与确认订单");id("scene.food"); 037 mount(this,menu,"food.menu");mount(this,bag,"food.bag"); 038 mount(menu,search,"food.search");search.placeholder("搜索餐品名称");search.onChange(v->filter()); 039 mount(menu,categories,"food.categories");categories.select(0);categories.onChange(v->{category=categories.selectedIndex();filter();}); 040 for(int i=0;i<products.length;i++){products[i]=new Product(i);menu.add(products[i]);} 041 mount(bag,bagTitle,null);mount(bag,cartScroll,"food.cart");cartScroll.glass(false).padding(0).gap(8).layout(Panel.Layout.COLUMN); 042 cartScroll.add(empty);empty.bounds(0,0,240,32); 043 for(int i=0;i<lines.length;i++){lines[i]=new CartLine(i);cartScroll.add(lines[i]);} 044 mount(bag,fulfillment,"food.fulfillment");fulfillment.select(0);fulfillment.onChange(v->{cart.fulfillment(fulfillment.selectedIndex());refresh();}); 045 mount(bag,coupon,"food.coupon");coupon.onChange(v->{cart.coupon(coupon.selected());refresh();}); 046 mount(bag,note,"food.note");note.placeholder("备注:少冰、不要香菜等");note.maxLength(160); 047 mount(bag,details,"food.details");mount(bag,sum,"food.total");mount(bag,checkout,"food.checkout");checkout.onClick(v->confirm()); 048 mount(this,orderState,"food.orderState");refresh(); 049 } 050 public Cart cart(){return cart;} 051 public int orderCount(){return orders;} 052 public String lastOrder(){return lastOrder;} 053 public int visibleProducts(){int n=0;for(Product p:products)if(p.visible())n++;return n;} 054 public void addProduct(int index){cart.quantity(index,cart.quantity(index)+1);refresh();} 055 private void filter(){String query=search.value().trim().toLowerCase(Locale.ROOT);for(int i=0;i<products.length;i++)products[i].visible((category==0||CATEGORIES[i]==category)&&NAMES[i].toLowerCase(Locale.ROOT).contains(query));} 056 private void refresh(){ 057 empty.visible(cart.count()==0);bagTitle.text("购物袋 / "+cart.count()); 058 for(int i=0;i<lines.length;i++){lines[i].visible(cart.quantity(i)>0);if(!lines[i].quantity.focused())lines[i].quantity.number(cart.quantity(i));} 059 sum.text(money(cart.total()));details.text("商品 "+money(cart.subtotal())+" / 运费 "+money(cart.fee())+(cart.discount()>0?"\n优惠 −"+money(cart.discount()):"")); 060 checkout.enabled(cart.count()>0);checkout.text(cart.count()>0?"模拟下单 · "+cart.count()+" 件":"先选择餐品"); 061 } 062 private void confirm(){if(ui==null||cart.count()==0)return; 063 final int price=cart.total(),count=cart.count();final String memo=note.value(); 064 Modal dialog=new Modal("确认演示订单");dialog.id("food.confirm");dialog.bounds(0,0,458,258); 065 Label receipt=new Label("共 "+count+" 件餐品 / "+money(price)+"\n"+(fulfillment.selectedIndex()==0?"配送至演示地址":"到店自取")+"\n这是本地交互演示,不会付款或下单。").size(14); 066 place(receipt,16,72,426,84);dialog.add(receipt); 067 Button cancel=new Button("返回修改"),send=new Button("确认模拟").primary(true);cancel.id("food.cancel");send.id("food.commit"); 068 place(cancel,16,199,184,40);place(send,212,199,230,40);dialog.add(cancel);dialog.add(send); 069 cancel.onClick(v->dialog.close());send.onClick(v->{orders++;lastOrder="LG-"+String.format(Locale.ROOT,"%03d",orders)+" · "+count+" 件 · "+money(price); 070 if(!memo.trim().isEmpty())lastOrder+=" · "+memo;cart.clear();refresh();orderState.text("已创建演示订单 "+lastOrder);dialog.close();notice(this,"模拟订单已确认","仅保存在当前窗口,不涉及真实交易。");}); 071 dialog.open(ui); 072 } 073 @Override public float preferredHeight(float width){float mw=width<800?width:width-298;int cols=mw<500?1:2;float mh=Math.max(458,112+((visibleProducts()+cols-1)/cols)*110+6);return (width<800?76+mh+12+458:76+Math.max(mh,458))+44;} 074 @Override protected void layoutChildren(){header();boolean stacked=w<800;float bw=stacked?w:286,mw=stacked?w:w-bw-GAP; 075 place(menu,0,76,mw,stacked?474:458);place(bag,stacked?0:mw+GAP,stacked?562:76,bw,458); 076 place(search,16,16,mw-32,34);place(categories,16,62,mw-32,38); 077 int colCount=mw<500?1:2,visible=0;float cardWidth=(mw-32-(colCount-1)*GAP)/colCount; 078 for(Product p:products)if(p.visible()){place(p,16+(visible%colCount)*(cardWidth+GAP),112+(visible/colCount)*110,cardWidth,98);visible++;} 079 // A narrow menu becomes a tall, naturally scrolling page rather than clipping its products. 080 float mh=Math.max(458,112+((visible+colCount-1)/colCount)*110+6);menu.bounds(menu.x(),menu.y(),mw,mh); 081 if(stacked)bag.bounds(0,76+mh+GAP,bw,458); 082 place(bagTitle,16,16,bw-32,28);place(cartScroll,16,56,bw-32,153); 083 place(fulfillment,16,221,bw-32,36);place(coupon,16,269,bw-32,28);place(note,16,309,bw-32,34); 084 place(details,16,355,bw-140,35);place(sum,bw-122,361,106,28);place(checkout,16,403,bw-32,39); 085 float bottom=Math.max(menu.y()+menu.height(),bag.y()+bag.height());place(orderState,0,bottom+16,w,24);h=bottom+44; 086 } 087 private final class Product extends Panel { 088 final int index;final Artwork art;final Label name,description,price;final Button add=new Button("加入 +"); 089 Product(int index){this.index=index;id("food.product."+index);art=new Artwork(CATEGORIES[index]==3?"cup":"bowl"); 090 name=new Label(NAMES[index]).size(14).weight(500);description=caption(index==0?"经典热食 · 单人份":index==1?"现煎牛肉 · 单人份":index==2?"暖胃浓汤 · 一人份":index==3?"蔬菜谷物 · 轻负担":index==4?"清爽柠檬 · 450ml":"燕麦咖啡 · 350ml");price=new Label(money(PRICES[index])).size(14).weight(600); 091 mount(this,art,null);mount(this,name,null);mount(this,description,null);mount(this,price,null);mount(this,add,"food.add."+index);add.onClick(v->addProduct(index));} 092 @Override protected void layoutChildren(){place(art,12,12,58,58);place(name,82,12,w-94,24);place(description,82,38,w-94,20);place(price,12,75,w-108,20);place(add,w-88,64,76,28);} 093 } 094 private final class CartLine extends Panel { 095 final Spinner quantity=new Spinner();final Label name,price; 096 CartLine(final int index){glass(false);padding(0);h=45;name=new Label(NAMES[index]).size(12);price=caption(money(PRICES[index])); 097 quantity.range(0,20).step(1).decimals(0);mount(this,name,null);mount(this,price,null);mount(this,quantity,"food.qty."+index); 098 quantity.onChange(v->{cart.quantity(index,(int)Math.round(quantity.number()));refresh();});} 099 @Override protected void layoutChildren(){place(name,0,2,w-118,20);place(price,0,23,w-118,18);place(quantity,w-114,7,114,32);} 100 } 101}