Cookie的使用
我們使用cookies來確保流暢的瀏覽體驗。若繼續,我們會認爲你接受使用cookies。
了解更多
import { useState, useEffect, useRef } from "react";
// ─── Design Tokens ────────────────────────────────────────────────────────────
const T = {
bg: "#0A0C10",
surface: "#111418",
card: "#161B22",
border: "#21262D",
accent: "#F97316",
accentDim: "#7C3910",
green: "#22C55E",
red: "#EF4444",
blue: "#3B82F6",
yellow: "#EAB308",
text: "#E6EDF3",
muted: "#7D8590",
subtle: "#30363D",
};
const css = `
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@300;400;500&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: ${T.bg}; color: ${T.text}; font-family: 'DM Sans', sans-serif; }
::-webkit-scrollbar { width: 4px; } ::-webkit-scrollbar-track { background: ${T.surface}; }
::-webkit-scrollbar-thumb { background: ${T.subtle}; border-radius: 2px; }
.app { min-height: 100vh; display: flex; flex-direction: column; }
.topbar { background: ${T.surface}; border-bottom: 1px solid ${T.border}; padding: 0 20px;
display: flex; align-items: center; justify-content: space-between; height: 56px; position: sticky; top: 0; z-index: 100; }
.logo { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 20px; color: ${T.accent};
display: flex; align-items: center; gap: 8px; }
.logo span { color: ${T.text}; }
.tabs { display: flex; gap: 4px; }
.tab { padding: 6px 16px; border-radius: 8px; border: none; cursor: pointer; font-family: 'DM Sans'; font-size: 14px;
font-weight: 500; transition: all .2s; background: transparent; color: ${T.muted}; }
.tab.active { background: ${T.accent}; color: #fff; }
.tab:hover:not(.active) { background: ${T.subtle}; color: ${T.text}; }
/* Auth */
.auth-wrap { flex: 1; display: flex; align-items: center; justify-content: center; padding: 24px; }
.auth-card { background: ${T.card}; border: 1px solid ${T.border}; border-radius: 20px;
padding: 40px; width: 100%; max-width: 440px; }
.auth-title { font-family: 'Syne', sans-serif; font-size: 28px; font-weight: 800; margin-bottom: 6px; }
.auth-sub { color: ${T.muted}; font-size: 14px; margin-bottom: 32px; }
.field { margin-bottom: 16px; }
.field label { display: block; font-size: 12px; font-weight: 500; color: ${T.muted}; margin-bottom: 6px; letter-spacing: .5px; text-transform: uppercase; }
.field input, .field select, .field textarea {
width: 100%; background: ${T.surface}; border: 1px solid ${T.border}; border-radius: 10px;
padding: 10px 14px; color: ${T.text}; font-family: 'DM Sans'; font-size: 14px; outline: none; transition: border .2s; }
.field input:focus, .field select:focus, .field textarea:focus { border-color: ${T.accent}; }
.field select option { background: ${T.surface}; }
.btn { width: 100%; padding: 12px; background: ${T.accent}; color: #fff; border: none; border-radius: 10px;
font-family: 'Syne', sans-serif; font-size: 15px; font-weight: 700; cursor: pointer; transition: opacity .2s; margin-top: 8px; }
.btn:hover { opacity: .88; }
.btn.secondary { background: ${T.subtle}; color: ${T.text}; }
.btn.green { background: ${T.green}; }
.btn.red { background: ${T.red}; }
.btn.sm { width: auto; padding: 7px 16px; font-size: 13px; margin-top: 0; border-radius: 8px; }
.auth-toggle { text-align: center; margin-top: 20px; font-size: 13px; color: ${T.muted}; }
.auth-toggle button { background: none; border: none; color: ${T.accent}; cursor: pointer; font-family: 'DM Sans'; font-size: 13px; text-decoration: underline; }
.role-pick { display: flex; gap: 12px; margin-bottom: 24px; }
.role-btn { flex: 1; padding: 14px; background: ${T.surface}; border: 2px solid ${T.border}; border-radius: 12px;
cursor: pointer; text-align: center; transition: all .2s; color: ${T.text}; }
.role-btn.active { border-color: ${T.accent}; background: rgba(249,115,22,.08); }
.role-btn .icon { font-size: 28px; margin-bottom: 6px; }
.role-btn .label { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 14px; }
/* Layout */
.main { flex: 1; display: grid; grid-template-columns: 260px 1fr; min-height: calc(100vh - 56px); }
.sidebar { background: ${T.surface}; border-right: 1px solid ${T.border}; padding: 20px 0; display: flex; flex-direction: column; gap: 2px; }
.nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 20px; cursor: pointer;
font-size: 14px; color: ${T.muted}; border-radius: 0; transition: all .2s; border-left: 3px solid transparent; }
.nav-item.active { color: ${T.accent}; border-left-color: ${T.accent}; background: rgba(249,115,22,.06); }
.nav-item:hover:not(.active) { color: ${T.text}; background: rgba(255,255,255,.03); }
.nav-icon { font-size: 17px; width: 20px; text-align: center; }
.content { overflow-y: auto; padding: 28px; }
/* Cards */
.page-title { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 26px; margin-bottom: 4px; }
.page-sub { color: ${T.muted}; font-size: 14px; margin-bottom: 28px; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.stat-card { background: ${T.card}; border: 1px solid ${T.border}; border-radius: 16px; padding: 20px; }
.stat-label { font-size: 12px; color: ${T.muted}; letter-spacing: .5px; text-transform: uppercase; margin-bottom: 8px; }
.stat-value { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 28px; }
.stat-value.accent { color: ${T.accent}; }
.stat-value.green { color: ${T.green}; }
.stat-value.blue { color: ${T.blue}; }
.card { background: ${T.card}; border: 1px solid ${T.border}; border-radius: 16px; padding: 20px; margin-bottom: 16px; }
.card-title { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
/* Products */
.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; }
.product-card { background: ${T.card}; border: 1px solid ${T.border}; border-radius: 14px; overflow: hidden; cursor: pointer; transition: all .2s; }
.product-card:hover { border-color: ${T.accent}; transform: translateY(-2px); }
.product-img { width: 100%; height: 120px; object-fit: cover; background: ${T.subtle};
display: flex; align-items: center; justify-content: center; font-size: 40px; }
.product-info { padding: 12px; }
.product-name { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 14px; margin-bottom: 4px; }
.product-price { color: ${T.accent}; font-weight: 700; font-size: 15px; }
.product-desc { color: ${T.muted}; font-size: 12px; margin-top: 4px; }
/* Cart */
.cart-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid ${T.border}; }
.qty-ctrl { display: flex; align-items: center; gap: 8px; }
.qty-btn { width: 28px; height: 28px; border-radius: 8px; border: 1px solid ${T.border}; background: ${T.surface};
color: ${T.text}; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; transition: all .2s; }
.qty-btn:hover { border-color: ${T.accent}; color: ${T.accent}; }
/* Orders */
.order-card { background: ${T.card}; border: 1px solid ${T.border}; border-radius: 14px; padding: 16px; margin-bottom: 12px; }
.order-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.order-id { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 14px; }
.badge { padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; }
.badge.pending { background: rgba(234,179,8,.15); color: ${T.yellow}; }
.badge.active { background: rgba(249,115,22,.15); color: ${T.accent}; }
.badge.delivered { background: rgba(34,197,94,.15); color: ${T.green}; }
.badge.cancelled { background: rgba(239,68,68,.15); color: ${T.red}; }
.order-meta { display: flex; gap: 16px; font-size: 13px; color: ${T.muted}; }
/* Map */
.map-mock { background: ${T.surface}; border: 1px solid ${T.border}; border-radius: 14px; height: 300px;
display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; }
.map-bg { position: absolute; inset: 0; opacity: .08;
background-image: linear-gradient(${T.muted} 1px, transparent 1px), linear-gradient(90deg, ${T.muted} 1px, transparent 1px);
background-size: 40px 40px; }
.map-route { position: absolute; left: 20%; top: 30%; width: 60%; height: 40%;
border: 3px dashed ${T.accent}; border-radius: 50%; opacity: .4; }
.map-pin { position: absolute; font-size: 28px; cursor: pointer; transition: transform .2s; }
.map-pin:hover { transform: scale(1.2); }
.map-pin.start { left: 18%; top: 42%; }
.map-pin.end { left: 70%; top: 30%; }
.map-label { position: absolute; background: ${T.card}; border: 1px solid ${T.border}; border-radius: 8px;
padding: 4px 10px; font-size: 12px; font-weight: 600; }
.map-label.start { left: 22%; top: 35%; }
.map-label.end { left: 74%; top: 22%; }
.courier-dot { position: absolute; left: 38%; top: 48%; width: 18px; height: 18px;
background: ${T.accent}; border-radius: 50%; border: 3px solid #fff;
animation: pulse 1.5s infinite; }
@keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(249,115,22,.4); } 50% { box-shadow: 0 0 0 10px transparent; } }
/* Notification */
.notif { background: ${T.card}; border: 1px solid ${T.border}; border-left: 3px solid ${T.accent};
border-radius: 12px; padding: 14px 16px; margin-bottom: 10px; display: flex; align-items: flex-start; gap: 12px; }
.notif.new { border-left-color: ${T.green}; }
.notif-icon { font-size: 20px; }
.notif-text { flex: 1; }
.notif-title { font-weight: 600; font-size: 14px; margin-bottom: 2px; }
.notif-desc { font-size: 12px; color: ${T.muted}; }
.notif-time { font-size: 11px; color: ${T.muted}; white-space: nowrap; }
/* Fee breakdown */
.fee-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid ${T.border}; font-size: 14px; }
.fee-row.total { border-bottom: none; font-family: 'Syne', sans-serif; font-weight: 800; font-size: 16px; color: ${T.accent}; padding-top: 12px; }
/* Earnings */
.earn-bar { height: 8px; background: ${T.subtle}; border-radius: 4px; margin-top: 6px; overflow: hidden; }
.earn-fill { height: 100%; background: ${T.green}; border-radius: 4px; transition: width .6s; }
/* Tabs */
.inner-tabs { display: flex; gap: 0; border-bottom: 1px solid ${T.border}; margin-bottom: 20px; }
.inner-tab { padding: 8px 18px; cursor: pointer; font-size: 14px; color: ${T.muted};
border-bottom: 2px solid transparent; margin-bottom: -1px; transition: all .2s; }
.inner-tab.active { color: ${T.accent}; border-bottom-color: ${T.accent}; font-weight: 600; }
/* Form row */
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.tag { display: inline-block; padding: 2px 8px; background: ${T.subtle}; border-radius: 4px; font-size: 11px; color: ${T.muted}; margin-right: 4px; }
.divider { border: none; border-top: 1px solid ${T.border}; margin: 16px 0; }
.empty { text-align: center; padding: 40px; color: ${T.muted}; font-size: 14px; }
.empty .icon { font-size: 40px; margin-bottom: 10px; }
.info-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: ${T.muted}; margin-bottom: 6px; }
.highlight { color: ${T.text}; font-weight: 500; }
input[type=range] { accent-color: ${T.accent}; }
`;
// ─── Mock Data ────────────────────────────────────────────────────────────────
const PRODUCTS = [
{ id: 1, name: "滷肉飯", price: 60, emoji: "🍚", desc: "台東招牌滷肉飯,香Q入味", category: "主食" },
{ id: 2, name: "排骨便當", price: 90, emoji: "🍱", desc: "香煎排骨搭配白飯", category: "主食" },
{ id: 3, name: "珍珠奶茶", price: 55, emoji: "🧋", desc: "濃郁奶茶配手工珍珠", category: "飲料" },
{ id: 4, name: "蚵仔煎", price: 70, emoji: "🥘", desc: "新鮮蚵仔現煎", category: "小吃" },
{ id: 5, name: "臭豆腐", price: 65, emoji: "🫕", desc: "酥脆外皮,搭配泡菜", category: "小吃" },
{ id: 6, name: "木瓜牛奶", price: 50, emoji: "🥤", desc: "台東特產木瓜現打", category: "飲料" },
{ id: 7, name: "控肉飯", price: 80, emoji: "🍖", desc: "慢燉三層肉,軟嫩入味", category: "主食" },
{ id: 8, name: "芋圓甜湯", price: 55, emoji: "🍡", desc: "手工芋圓,Q彈有嚼勁", category: "甜點" },
];
const STORES = [
{ id: 1, name: "台東老街小吃", address: "台東市中正路123號", lat: 22.76, lng: 121.14, rating: 4.8, products: [1,2,4,5] },
{ id: 2, name: "卑南鄉飲料店", address: "卑南鄉大南路88號", lat: 22.73, lng: 121.12, rating: 4.6, products: [3,6,8] },
{ id: 3, name: "台東排骨專賣", address: "台東市更生路55號", lat: 22.75, lng: 121.15, rating: 4.9, products: [2,7] },
];
const INITIAL_ORDERS = [
{ id: "ORD-001", storeId: 1, storeName: "台東老街小吃", items: [{id:1,name:"滷肉飯",price:60,qty:2},{id:4,name:"蚵仔煎",price:70,qty:1}], status: "active", address: "台東市中山路99號", total: 240, fee: 40, distance: 2.3, bonus: 12, time: "14:32" },
{ id: "ORD-002", storeId: 2, storeName: "卑南鄉飲料店", items: [{id:3,name:"珍珠奶茶",price:55,qty:2}], status: "pending", address: "卑南鄉初鹿路22號", total: 160, fee: 55, distance: 4.8, bonus: 26, time: "14:45" },
{ id: "ORD-003", storeId: 1, storeName: "台東老街小吃", items: [{id:2,name:"排骨便當",price:90,qty:1}], status: "delivered", address: "台東市光明路15號", total: 140, fee: 35, distance: 1.8, bonus: 8, time: "13:10" },
];
// ─── Haversine distance ───────────────────────────────────────────────────────
function haversine(lat1, lng1, lat2, lng2) {
const R = 6371, dLat = (lat2-lat1)*Math.PI/180, dLng = (lng2-lng1)*Math.PI/180;
const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLng/2)**2;
return +(R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a))).toFixed(1);
}
function calcFee(dist, isRemote = false) {
let fee = dist <= 3 ? 30 : 30 + Math.ceil((dist-3)/0.5)*5;
if (isRemote) fee += 20;
return fee;
}
function calcBonus(dist) {
return dist > 5 ? Math.round(dist * 5.5) : Math.round(dist * 3);
}
// ─── Main App ─────────────────────────────────────────────────────────────────
export default function App() {
const [mode, setMode] = useState("customer"); // customer | courier
const [auth, setAuth] = useState(null); // null = not logged in
const [authView, setAuthView] = useState("login"); // login | register
const [page, setPage] = useState("home");
const [orders, setOrders] = useState(INITIAL_ORDERS);
const [cart, setCart] = useState([]);
const [selectedStore, setSelectedStore] = useState(null);
const [notifications, setNotifications] = useState([
{ id: 1, title: "新訂單!", desc: "ORD-002 卑南鄉,距離 4.8 km", time: "剛剛", type: "new" },
{ id: 2, title: "訂單已取件", desc: "ORD-001 已從台東老街小吃取貨", time: "13分鐘前", type: "info" },
]);
const switchMode = (m) => { setMode(m); setAuth(null); setPage("home"); setCart([]); setSelectedStore(null); };
return (
{!auth
?
: mode === "customer"
?
:
}
);
}
// ─── TopBar ───────────────────────────────────────────────────────────────────
function TopBar({ mode, auth, switchMode }) {
return (
🛵 台東快送
{auth &&
👤 {auth.name}
}
);
}
// ─── Auth ─────────────────────────────────────────────────────────────────────
function AuthScreen({ mode, view, setView, onAuth }) {
const [form, setForm] = useState({ name:"", phone:"", email:"", password:"", address:"", district:"", vehicle:"" });
const set = (k,v) => setForm(f=>({...f,[k]:v}));
const handleSubmit = () => {
if (!form.phone || !form.password) return alert("請填寫必要欄位");
onAuth({ name: form.name || (mode==="customer"?"顧客":"外送員"), phone: form.phone, address: form.address, district: form.district });
};
return (
{view==="login"?"歡迎回來 👋":"建立帳號"}
{mode==="customer" ? "訂餐,輕鬆送到府" : "加入台東快送外送團隊"}
{view==="register" && <>
set("name",e.target.value)} />
{mode==="customer" && <>
set("address",e.target.value)} />
>}
{mode==="courier" && <>
>}
>}
set("phone",e.target.value)} />
set("password",e.target.value)} />
{view==="login"
? <>還沒有帳號?>
: <>已有帳號?>}
{view==="login" &&
💡 測試用:任意輸入手機號碼與密碼即可登入
}
);
}
// ─── Customer App ─────────────────────────────────────────────────────────────
function CustomerApp({ page, setPage, auth, orders, setOrders, cart, setCart, selectedStore, setSelectedStore }) {
const myOrders = orders.filter(o => o.status !== "cancelled");
const nav = [
{ id:"home", icon:"🏠", label:"首頁" },
{ id:"stores", icon:"🏪", label:"餐廳列表" },
{ id:"cart", icon:"🛒", label:`購物車${cart.length>0?` (${cart.reduce((a,c)=>a+c.qty,0)})`:""}`},
{ id:"orders", icon:"📦", label:"我的訂單" },
{ id:"profile", icon:"👤", label:"個人設定" },
];
return (
{nav.map(n => (
setPage(n.id)}>
{n.icon}{n.label}
))}
{page==="home" && }
{page==="stores" && }
{page==="cart" && }
{page==="orders" && }
{page==="profile" && }
{page==="store_detail" && selectedStore && (
)}
);
}
function CustHome({ auth, orders, setPage }) {
const active = orders.filter(o=>o.status==="active");
return (<>
你好,{auth.name} 👋
台東・卑南地區快速外送服務
已送達
{orders.filter(o=>o.status==="delivered").length}
{active.length > 0 &&
🔴 訂單進行中
{active.map(o => (
{o.id}外送中
🏪
📍
{o.storeName}
{o.address.substring(0,8)}
🚴 預計 10-15 分鐘 送達・距離 {o.distance} km
))}
}
setPage("stores")}>
🍽️ 立即點餐
{STORES.map(s=>(
{s.name}
⭐ {s.rating} · {s.address.substring(0,10)}
))}
>);
}
function StoreList({ setPage, setSelectedStore }) {
return (<>
餐廳列表
選擇你想點的餐廳
{STORES.map(s => (
{setSelectedStore(s);setPage("store_detail");}}>
{s.name}
📍 {s.address}
⭐ {s.rating} 外送中{s.products.length}項商品
))}
>);
}
function StoreDetail({ store, cart, setCart, setPage }) {
const storeProducts = PRODUCTS.filter(p => store.products.includes(p.id));
const addToCart = (p) => {
setCart(c => {
const ex = c.find(i=>i.id===p.id&&i.storeId===store.id);
if (ex) return c.map(i=>i.id===p.id&&i.storeId===store.id?{...i,qty:i.qty+1}:i);
return [...c, {...p, storeId: store.id, storeName: store.name, qty:1}];
});
};
const inCart = (id) => cart.find(i=>i.id===id&&i.storeId===store.id)?.qty || 0;
return (<>
{store.name}
📍 {store.address}・⭐ {store.rating}
{storeProducts.map(p => (
{p.emoji}
{p.name}
{p.desc}
NT${p.price}
{inCart(p.id) > 0 && <>
{inCart(p.id)}
>}
))}
{cart.length > 0 && (
)}
>);
}
function CartPage({ auth, cart, setCart, orders, setOrders, setPage }) {
const [address, setAddress] = useState(auth.address || "");
const [district, setDistrict] = useState(auth.district || "台東市");
const [note, setNote] = useState("");
const isRemote = ["卑南鄉","鹿野鄉","關山鎮"].includes(district);
const subtotal = cart.reduce((a,c)=>a+c.price*c.qty,0);
const dist = isRemote ? 5.2 : 2.1;
const fee = calcFee(dist, isRemote);
const platform = Math.round(subtotal * 0.05);
const total = subtotal + fee + platform;
const bonus = calcBonus(dist);
const placeOrder = () => {
if (!address) return alert("請填寫送餐地址");
const id = "ORD-" + String(orders.length + 1).padStart(3, "0");
const storeId = cart[0]?.storeId;
const storeName = cart[0]?.storeName || "餐廳";
const newOrder = { id, storeId, storeName, items: cart.map(c=>({id:c.id,name:c.name,price:c.price,qty:c.qty})),
status: "pending", address, district, total, fee, distance: dist, bonus, time: new Date().toLocaleTimeString("zh-TW",{hour:"2-digit",minute:"2-digit"}) };
setOrders(o=>[...o, newOrder]);
setCart([]);
alert(`✅ 訂單 ${id} 已成立!\n平台維護費:NT$${platform}\n外送費:NT$${fee}`);
setPage("orders");
};
if (cart.length === 0) return (<>
購物車
🛒
購物車是空的
>);
return (<>
確認訂單
確認商品及配送資訊
🛍️ 商品明細
{cart.map(item => (
{PRODUCTS.find(p=>p.id===item.id)?.emoji}
{item.name}
NT${item.price} × {item.qty}
{item.qty}
NT${item.price*item.qty}
))}
📍 配送資訊
setAddress(e.target.value)} placeholder="台東市中正路123號" />
💰 費用明細
商品小計NT${subtotal}
外送費 ({dist} km{isRemote?" · 偏遠地區":""})NT${fee}
平台維護費 (5%)NT${platform}
合計NT${total}
{isRemote &&
⚠️ 偏遠地區加收 NT$20 外送費
}
🗺️ 配送路線預覽
📏 預計距離:{dist} km
⏱️ 預計時間:{Math.round(dist*4+10)} 分鐘
>);
}
function CustOrders({ orders }) {
const [filter, setFilter] = useState("all");
const filtered = filter==="all" ? orders : orders.filter(o=>o.status===filter);
return (<>
我的訂單
{[["all","全部"],["pending","待接單"],["active","外送中"],["delivered","已送達"]].map(([k,l])=>(
setFilter(k)}>{l}
))}
{filtered.length === 0 ?
: filtered.map(o => (
{o.id} · {o.storeName}
{o.status==="pending"?"待接單":o.status==="active"?"外送中":"已送達"}
📍 {o.address}
🕐 {o.time}
💰 NT${o.total}
{o.status==="active" &&
}
))}
>);
}
function CustProfile({ auth }) {
return (<>
個人設定
👤 帳號資訊
姓名:{auth.name}
手機:{auth.phone || "0912-345-678"}
地址:{auth.address || "尚未設定"}
行政區:{auth.district || "台東市"}
💳 付款方式
💵 現金付款 預設
📱 Line Pay
>);
}
// ─── Courier App ──────────────────────────────────────────────────────────────
function CourierApp({ page, setPage, auth, orders, setOrders, notifications }) {
const nav = [
{ id:"home", icon:"🏠", label:"首頁" },
{ id:"orders", icon:"📋", label:"接單列表" },
{ id:"active", icon:"🗺️", label:"進行中" },
{ id:"earnings", icon:"💰", label:"收益" },
{ id:"notif", icon:"🔔", label:`通知${notifications.length>0?` (${notifications.length})`:""}`},
];
return (
{nav.map(n => (
setPage(n.id)}>
{n.icon}{n.label}
))}
{page==="home" &&
}
{page==="orders" &&
}
{page==="active" &&
}
{page==="earnings" &&
}
{page==="notif" &&
}
);
}
function CourierHome({ auth, orders, setPage }) {
const myActive = orders.filter(o=>o.status==="active");
const pending = orders.filter(o=>o.status==="pending");
const totalEarn = orders.filter(o=>o.status==="delivered").reduce((a,c)=>a+c.fee+c.bonus,0);
return (<>
外送員後台
歡迎,{auth.name}・台東卑南路線
{pending.length > 0 &&
🔔 新訂單等待接單
{pending.map(o=>(
{o.id} · {o.storeName}
📍 {o.address}・{o.distance} km
💰 外送費 NT${o.fee} + 里程獎金 NT${o.bonus}
))}
}
{myActive.length > 0 &&
🚴 進行中的配送
}
>);
}
function CourierOrders({ orders, setOrders }) {
const [filter, setFilter] = useState("pending");
const filtered = orders.filter(o => filter==="all" ? true : o.status===filter);
const accept = (id) => setOrders(o=>o.map(x=>x.id===id?{...x,status:"active"}:x));
const complete = (id) => setOrders(o=>o.map(x=>x.id===id?{...x,status:"delivered"}:x));
return (<>
訂單列表
{[["pending","待接單"],["active","外送中"],["delivered","已送達"],["all","全部"]].map(([k,l])=>(
setFilter(k)}>{l}
))}
{filtered.length===0 ?
: filtered.map(o=>(
{o.id}
{o.status==="pending"?"待接單":o.status==="active"?"外送中":"已送達"}
🏪 取貨:{o.storeName}
📍 送達:{o.address}
🕐 下單:{o.time}
📏 距離:{o.distance} km
💰 外送費:NT${o.fee}
🎯 里程獎金:NT${o.bonus}
{o.items.map(i=>{i.name} ×{i.qty})}
{o.status==="pending" && }
{o.status==="active" && }
{o.status==="pending" && }
))}
>);
}
function ActiveDelivery({ orders, setOrders }) {
const active = orders.filter(o=>o.status==="active");
const [current, setCurrent] = useState(active[0] || null);
const [phase, setPhase] = useState("pickup"); // pickup | deliver
useEffect(() => { if (active.length > 0 && !current) setCurrent(active[0]); }, [active]);
if (!current) return (<>
地圖導航
>);
const complete = () => {
setOrders(o=>o.map(x=>x.id===current.id?{...x,status:"delivered"}:x));
setCurrent(null); setPhase("pickup");
};
return (<>
🗺️ 實時導航
{current.id} · {phase==="pickup"?"前往取貨":"前往送餐"}
目的地
{phase==="pickup"?current.storeName:current.address}
{phase==="pickup"
?
: }
📋 訂單資訊
🆔 {current.id}
🏪 取貨:{current.storeName}
📍 送達:{current.address}
{current.items.map(i=>
{PRODUCTS.find(p=>p.id===i.id)?.emoji} {i.name} × {i.qty}
)}
💰 本單收益
外送費NT${current.fee}
里程獎金 ({current.distance} km)+NT${current.bonus}
合計NT${current.fee+current.bonus}
🎯 超過 5km 享有里程加成獎金
📏 配送資料
距離:{current.distance} km
預計時間:{Math.round(current.distance*4+8)} 分鐘
當前狀態:{phase==="pickup"?"前往取貨 🏪":"配送中 🚴"}
>);
}
function Earnings({ orders }) {
const delivered = orders.filter(o=>o.status==="delivered");
const totalFee = delivered.reduce((a,c)=>a+c.fee,0);
const totalBonus = delivered.reduce((a,c)=>a+c.bonus,0);
const totalDist = delivered.reduce((a,c)=>a+c.distance,0).toFixed(1);
const goal = 2000;
return (<>
收益總覽
今日收益與里程統計
🎯 今日目標
NT${totalFee+totalBonus} / NT${goal}
{Math.round((totalFee+totalBonus)/goal*100)}%
📊 里程加成規則
{[[0,3,"標準","NT$3/km"],[3,5,"加成 1.5x","NT$4.5/km"],[5,99,"加成 2x","NT$5.5/km"]].map(([a,b,label,rate])=>(
{a}–{b===99?"∞":b} km {label}
{rate}
))}
📝 配送記錄
{delivered.length===0 ?
: delivered.map(o=>(
{o.id} · {o.distance} km · {o.time}
+NT${o.fee+o.bonus}
))}
>);
}
function Notifications({ notifications }) {
return (<>
通知中心
{notifications.length} 則通知
{notifications.length===0 ?
: notifications.map(n=>(
{n.type==="new"?"🆕":"ℹ️"}
{n.time}
))}
>);
}