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}