import { useState, useMemo } from "react"; import "./App.css"; interface Package { id: string; type: "single" | "dual"; photos: number; price: number; postProcessing: string; note: string; available: boolean; isAddon: boolean; } const packages: Package[] = [ { id: "s1", type: "single", photos: 1, price: 10, postProcessing: "调色简修", note: "一张不接,附加项", available: true, isAddon: true }, { id: "s4", type: "single", photos: 4, price: 40, postProcessing: "调色简修", note: "", available: true, isAddon: false }, { id: "s6", type: "single", photos: 6, price: 55, postProcessing: "调色简修", note: "", available: true, isAddon: false }, { id: "s9", type: "single", photos: 9, price: 0, postProcessing: "", note: "暂不接", available: false, isAddon: false }, { id: "d1", type: "dual", photos: 1, price: 15, postProcessing: "调色简修", note: "一张不接,附加项", available: true, isAddon: true }, { id: "d2", type: "dual", photos: 2, price: 30, postProcessing: "调色简修", note: "", available: true, isAddon: false }, { id: "d4", type: "dual", photos: 4, price: 65, postProcessing: "调色简修", note: "", available: true, isAddon: false }, { id: "d6", type: "dual", photos: 6, price: 0, postProcessing: "", note: "暂不接", available: false, isAddon: false }, ]; const COMBO_DISCOUNT = 5; const DEPOSIT_RATE = 0.5; /** 定金封顶:50% 超过该值时只收该金额,避免大额订单定金过高 */ const DEPOSIT_CAP = 50; function formatMoney(n: number): string { return n % 1 === 0 ? String(n) : n.toFixed(1); } /** 定金:默认应付的 50%;若 50% 超过 50 元则封顶 50 元(且不超过订单总额) */ function computeDeposit(finalPrice: number): { amount: number; isCapped: boolean; half: number } { if (finalPrice <= 0) return { amount: 0, isCapped: false, half: 0 }; const half = finalPrice * DEPOSIT_RATE; if (half > DEPOSIT_CAP) { return { amount: Math.min(DEPOSIT_CAP, finalPrice), isCapped: true, half }; } return { amount: half, isCapped: false, half }; } function App() { const [quantities, setQuantities] = useState>({}); const getQty = (id: string) => quantities[id] || 0; const setQty = (id: string, val: number) => { setQuantities((prev) => ({ ...prev, [id]: Math.max(0, val) })); }; const selectedSingle = useMemo( () => packages.filter((p) => p.type === "single" && getQty(p.id) > 0), // eslint-disable-next-line react-hooks/exhaustive-deps [quantities] ); const selectedDual = useMemo( () => packages.filter((p) => p.type === "dual" && getQty(p.id) > 0), // eslint-disable-next-line react-hooks/exhaustive-deps [quantities] ); const singleSubtotal = selectedSingle.reduce((sum, p) => sum + p.price * getQty(p.id), 0); const dualSubtotal = selectedDual.reduce((sum, p) => sum + p.price * getQty(p.id), 0); const originalTotal = singleSubtotal + dualSubtotal; const hasCombo = selectedSingle.length > 0 && selectedDual.length > 0; const finalPrice = hasCombo ? originalTotal - COMBO_DISCOUNT : originalTotal; const { amount: deposit, isCapped, half: depositHalf } = computeDeposit(finalPrice); const balance = finalPrice - deposit; const totalSinglePhotos = selectedSingle.reduce((sum, p) => sum + p.photos * getQty(p.id), 0); const totalDualPhotos = selectedDual.reduce((sum, p) => sum + p.photos * getQty(p.id), 0); const totalPhotos = totalSinglePhotos + totalDualPhotos; const hasSelection = originalTotal > 0; const singlePackages = packages.filter((p) => p.type === "single"); const dualPackages = packages.filter((p) => p.type === "dual"); return (
{/* Header */}
📷

晓螺

摄影套餐价格计算器

按价目表原价;同时选单人与双人可再减 5 元

{/* Package Selection */}

🎨 套餐自选

👤 独行者

{singlePackages.map((pkg) => ( setQty(pkg.id, val)} /> ))}

👥 双人成行

{dualPackages.map((pkg) => ( setQty(pkg.id, val)} /> ))}
{/* Selected Items Summary */}

📋 已选套餐 {totalPhotos > 0 && 共 {totalPhotos} 张}

{!hasSelection ? (
🛒

还没有选择任何套餐,请在上方添加

) : (

👤 独行者

{selectedSingle.length === 0 ? (

未选择

) : ( <> {selectedSingle.map((pkg) => (
单人 {pkg.photos} 张 × {getQty(pkg.id)} ¥{pkg.price * getQty(pkg.id)}
))}
小计 ({totalSinglePhotos} 张) ¥{singleSubtotal}
)}

👥 双人成行

{selectedDual.length === 0 ? (

未选择

) : ( <> {selectedDual.map((pkg) => (
双人 {pkg.photos} 张 × {getQty(pkg.id)} ¥{pkg.price * getQty(pkg.id)}
))}
小计 ({totalDualPhotos} 张) ¥{dualSubtotal}
)}
)}
{/* Price Calculation */} {hasSelection && (

💰 价格计算

套餐小计 ¥{formatMoney(originalTotal)}
{hasCombo && (
组合套餐优惠 -¥{formatMoney(COMBO_DISCOUNT)}
)}
应付金额 ¥{formatMoney(finalPrice)}
定金 {isCapped ? ( ({DEPOSIT_RATE * 100}% 为 {formatMoney(depositHalf)} 元,超过 ¥{DEPOSIT_CAP} 时封顶 ¥{DEPOSIT_CAP}) ) : ( (应付×{DEPOSIT_RATE * 100}%) )} ¥{formatMoney(deposit)}
尾款 ¥{formatMoney(balance)}
{hasCombo && (
已选单人 + 双人,减免 ¥{formatMoney(COMBO_DISCOUNT)}
)}

订单: {hasCombo ? ( {formatMoney(originalTotal)} − {formatMoney(COMBO_DISCOUNT)} = {formatMoney(finalPrice)} ) : ( {formatMoney(finalPrice)} )}

定金: {isCapped ? ( 应付 {formatMoney(finalPrice)} × {DEPOSIT_RATE} = {formatMoney(depositHalf)} 元 > ¥{DEPOSIT_CAP},封顶收{" "} ¥{formatMoney(deposit)};尾款 ¥{formatMoney(balance)} ) : finalPrice > 0 ? ( 应付 {formatMoney(finalPrice)} × {DEPOSIT_RATE} = ¥{formatMoney(deposit)};尾款 ¥{formatMoney(balance)} ) : null}

)} {/* Footer */}

💡 单人 1 张 / 双人 1 张 为附加项,不单独接单

💡 同时选择单人 + 双人套餐可享组合优惠 ¥{formatMoney(COMBO_DISCOUNT)}

💡 定金默认应付的 {DEPOSIT_RATE * 100}%;当 {DEPOSIT_RATE * 100}% 超过 ¥{DEPOSIT_CAP} 时,定金封顶为 ¥{DEPOSIT_CAP}

); } function PackageCard({ pkg, qty, onQtyChange, }: { pkg: Package; qty: number; onQtyChange: (val: number) => void; }) { const typeLabel = pkg.type === "single" ? "单人" : "双人"; return (
0 ? "active" : ""}`}> {pkg.isAddon &&
附加项
} {!pkg.available &&
暂不接
}
{typeLabel} {pkg.photos} 张
{pkg.available ? ( <>
¥{pkg.price}
{pkg.postProcessing}
) : (
暂不接单
)} {pkg.available && (
{qty}
)}
); } export default App;