feat(bidding): add initial auction control and display pages
This commit introduces a lightweight, browser-based auction system. It consists of two main components that communicate in real-time using the `BroadcastChannel` API, enabling a serverless front-end experience: - `display.html`: The public-facing screen for attendees. It shows the current item, price with smooth animation, and deal announcements. It automatically generates a unique session ID. - `control.html`: The auctioneer's control panel. It uses the session ID from the display page to connect. It allows for managing the auction flow, including loading items from a CSV, starting bids, updating prices, and finalizing sales.
This commit is contained in:
189
bidding/display.html
Normal file
189
bidding/display.html
Normal file
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Auction Display</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.splash {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.item img,
|
||||
.item video {
|
||||
max-width: 60vw;
|
||||
max-height: 40vh;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.price {
|
||||
font-size: 3rem;
|
||||
color: #ffd700;
|
||||
margin: 1rem 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.deal {
|
||||
font-size: 2.5rem;
|
||||
color: #00ff88;
|
||||
}
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 初次加载提示 -->
|
||||
<div id="guide" class="center">
|
||||
<p>请将此窗口拖曳到大屏幕并按下 <b>F11</b> 进行展示</p>
|
||||
<p>控制端链接: <span id="controlLink"></span></p>
|
||||
</div>
|
||||
|
||||
<!-- Splash Screen -->
|
||||
<div id="splash" class="center hidden">
|
||||
<div class="splash">🚀 Studio Name</div>
|
||||
<div><img src="logo.png" alt="Logo" style="max-width: 200px" /></div>
|
||||
</div>
|
||||
|
||||
<!-- 标品展示 -->
|
||||
<div id="itemView" class="center hidden">
|
||||
<div class="itemMedia"></div>
|
||||
<div class="name"></div>
|
||||
<div class="remark"></div>
|
||||
<div class="price">¥0</div>
|
||||
</div>
|
||||
|
||||
<!-- 成交界面 -->
|
||||
<div id="dealView" class="center hidden">
|
||||
<div class="deal">🎉 成交!</div>
|
||||
<div class="finalPrice"></div>
|
||||
<div class="winner"></div>
|
||||
</div>
|
||||
|
||||
<!-- 总金额 -->
|
||||
<div class="footer">总金额: <span id="totalAmount">0</span> 元</div>
|
||||
|
||||
<script>
|
||||
// 获取或生成频道 ID
|
||||
function getOrCreateDisplayId() {
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
let id = urlParams.get("display");
|
||||
if (!id) {
|
||||
id =
|
||||
Math.random().toString(36).slice(2, 12) + Date.now().toString(36);
|
||||
location.href = location.pathname + "?display=" + id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
const displayId = getOrCreateDisplayId();
|
||||
const channel = new BroadcastChannel("auction-" + displayId);
|
||||
|
||||
// DOM 元素
|
||||
const guide = document.getElementById("guide");
|
||||
const controlLink = document.getElementById("controlLink");
|
||||
const splash = document.getElementById("splash");
|
||||
const itemView = document.getElementById("itemView");
|
||||
const dealView = document.getElementById("dealView");
|
||||
const totalAmountEl = document.getElementById("totalAmount");
|
||||
|
||||
const itemMedia = itemView.querySelector(".itemMedia");
|
||||
const itemName = itemView.querySelector(".name");
|
||||
const itemRemark = itemView.querySelector(".remark");
|
||||
const itemPrice = itemView.querySelector(".price");
|
||||
|
||||
const dealPrice = dealView.querySelector(".finalPrice");
|
||||
const dealWinner = dealView.querySelector(".winner");
|
||||
|
||||
let currentPrice = 0;
|
||||
let targetPrice = 0;
|
||||
let totalAmount = 0;
|
||||
|
||||
// 设置控制端链接
|
||||
controlLink.innerText =
|
||||
location.origin + "/control.html?display=" + displayId;
|
||||
|
||||
// 展示 Splash
|
||||
setTimeout(() => {
|
||||
guide.classList.add("hidden");
|
||||
splash.classList.remove("hidden");
|
||||
setTimeout(() => {
|
||||
splash.classList.add("hidden");
|
||||
}, 2000);
|
||||
}, 3000);
|
||||
|
||||
// 动态价格更新 (Lerp)
|
||||
function animatePrice() {
|
||||
if (Math.abs(targetPrice - currentPrice) > 1) {
|
||||
currentPrice += (targetPrice - currentPrice) * 0.1;
|
||||
itemPrice.textContent = "¥" + Math.round(currentPrice);
|
||||
requestAnimationFrame(animatePrice);
|
||||
} else {
|
||||
currentPrice = targetPrice;
|
||||
itemPrice.textContent = "¥" + currentPrice;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听消息
|
||||
channel.onmessage = (ev) => {
|
||||
const msg = ev.data;
|
||||
if (msg.type === "showItem") {
|
||||
splash.classList.add("hidden");
|
||||
dealView.classList.add("hidden");
|
||||
itemView.classList.remove("hidden");
|
||||
|
||||
// 更新标品信息
|
||||
itemName.textContent = msg.name;
|
||||
itemRemark.textContent = msg.remark || "";
|
||||
if (msg.media) {
|
||||
if (msg.media.endsWith(".mp4")) {
|
||||
itemMedia.innerHTML = `<video src="${msg.media}" autoplay loop></video>`;
|
||||
} else {
|
||||
itemMedia.innerHTML = `<img src="${msg.media}" alt="">`;
|
||||
}
|
||||
} else {
|
||||
itemMedia.innerHTML = "";
|
||||
}
|
||||
|
||||
currentPrice = 0;
|
||||
targetPrice = msg.basePrice || 0;
|
||||
animatePrice();
|
||||
}
|
||||
|
||||
if (msg.type === "updatePrice") {
|
||||
targetPrice = msg.price;
|
||||
animatePrice();
|
||||
}
|
||||
|
||||
if (msg.type === "deal") {
|
||||
itemView.classList.add("hidden");
|
||||
dealView.classList.remove("hidden");
|
||||
dealPrice.textContent = "成交价: ¥" + msg.price;
|
||||
dealWinner.textContent = "得标者: " + (msg.winner || "兴 旺 发");
|
||||
totalAmount += msg.price;
|
||||
totalAmountEl.textContent = totalAmount;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user