<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>我们的星星瓶</title>
<link href="https://fonts.googleapis.com/css2?family=Zhi+Mang+Xing&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<style>
* { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html, body { height:100%; overflow:hidden; }
body {
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
background: #1a3a5c;
background-image:
radial-gradient(ellipse at 20% 30%, rgba(70,130,180,0.55) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(40,90,140,0.6) 0%, transparent 45%),
radial-gradient(ellipse at 50% 80%, rgba(30,70,110,0.7) 0%, transparent 55%),
radial-gradient(ellipse at 90% 70%, rgba(60,110,160,0.4) 0%, transparent 40%),
linear-gradient(160deg, #1e4266 0%, #153252 40%, #0f2440 70%, #1a3a5c 100%);
background-attachment: fixed;
color:#fff;
user-select:none;
position:relative;
}
body::before {
content:''; position:fixed; inset:0; pointer-events:none;
background:
radial-gradient(circle at 15% 25%, rgba(120,170,220,0.12) 0%, transparent 30%),
radial-gradient(circle at 75% 65%, rgba(80,140,200,0.1) 0%, transparent 35%);
animation: drift 20s ease-in-out infinite alternate;
}
@keyframes drift { from { transform:translate(0,0) scale(1); } to { transform:translate(2%,1%) scale(1.05); } }
.screen { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:24px; transition:opacity .4s, visibility .4s; }
.screen.hidden { opacity:0; visibility:hidden; pointer-events:none; }
/* 入场 */
.entry-box { width:100%; max-width:340px; text-align:center; }
.entry-title { font-size:26px; font-weight:600; margin-bottom:8px; letter-spacing:2px; text-shadow:0 2px 12px rgba(0,0,0,0.4); }
.entry-sub { font-size:13px; opacity:.65; margin-bottom:32px; letter-spacing:1px; }
.input-field {
width:100%; padding:15px 18px; border-radius:14px;
border:1px solid rgba(180,210,240,0.3);
background:rgba(255,255,255,0.08);
backdrop-filter:blur(10px);
color:#fff; font-size:17px; text-align:center; letter-spacing:6px;
outline:none; transition:.3s;
}
.input-field::placeholder { letter-spacing:2px; color:rgba(255,255,255,0.35); font-size:14px; }
.input-field:focus { border-color:rgba(160,210,255,0.6); background:rgba(255,255,255,0.13); }
.btn-primary {
width:100%; margin-top:18px; padding:15px; border:none; border-radius:14px;
background:linear-gradient(135deg, rgba(120,180,240,0.85), rgba(80,140,210,0.85));
color:#fff; font-size:16px; font-weight:600; letter-spacing:2px;
cursor:pointer; transition:.25s; box-shadow:0 6px 20px rgba(0,0,0,0.25);
}
.btn-primary:active { transform:scale(0.97); }
.btn-primary:disabled { opacity:.4; cursor:not-allowed; }
.error-msg { color:#ff9a9a; font-size:13px; margin-top:12px; min-height:18px; }
.identity-btns { display:flex; gap:16px; width:100%; max-width:340px; }
.identity-btn {
flex:1; padding:28px 12px; border-radius:18px;
border:1px solid rgba(180,210,240,0.25);
background:rgba(255,255,255,0.07);
backdrop-filter:blur(10px);
color:#fff; font-size:17px; font-weight:600; letter-spacing:1px;
cursor:pointer; transition:.25s;
}
.identity-btn:active { transform:scale(0.96); }
.identity-btn:hover { background:rgba(255,255,255,0.14); border-color:rgba(180,210,240,0.5); }
/* 主页面 */
#mainScreen { justify-content:flex-start; padding-top:20px; }
.top-bar {
position:fixed; top:16px; right:16px; z-index:30;
display:flex; gap:10px;
}
.btn-add-star {
padding:11px 18px; border-radius:12px; border:1px solid rgba(180,210,240,0.3);
background:rgba(255,255,255,0.12); backdrop-filter:blur(12px);
color:#fff; font-size:13px; font-weight:600; letter-spacing:1px;
cursor:pointer; transition:.25s;
}
.btn-add-star:active { transform:scale(0.95); }
.current-user { position:fixed; top:16px; left:16px; font-size:12px; opacity:.6; letter-spacing:1px; z-index:30; }
.bottles-wrap {
display:flex; gap:clamp(20px, 6vw, 70px);
align-items:flex-end; justify-content:center;
margin-top:auto; margin-bottom:auto;
width:100%; max-width:820px;
padding:0 10px;
}
.bottle-col { display:flex; flex-direction:column; align-items:center; gap:14px; flex:1; max-width:300px; }
.bottle-label { font-size:14px; letter-spacing:2px; opacity:.8; font-weight:600; text-shadow:0 2px 8px rgba(0,0,0,0.4); }
/* 玻璃杯伪3D */
.glass {
position:relative;
width:clamp(120px, 30vw, 200px);
height:clamp(180px, 44vw, 290px);
cursor:pointer;
transition:transform .3s;
perspective:800px;
}
.glass:active { transform:scale(0.96); }
.glass-body {
position:absolute; inset:0;
border-radius:12% 12% 22% 22% / 8% 8% 18% 18%;
background:
linear-gradient(100deg, rgba(255,255,255,0.28) 0%, rgba(255,255,255,0.05) 18%, rgba(255,255,255,0.02) 45%, rgba(255,255,255,0.1) 78%, rgba(255,255,255,0.3) 100%),
linear-gradient(to top, rgba(150,200,240,0.22) 0%, rgba(180,220,250,0.14) 40%, rgba(200,230,255,0.06) 100%);
border:1px solid rgba(220,240,255,0.35);
box-shadow:
inset 0 0 30px rgba(200,230,255,0.18),
inset -8px 0 20px rgba(150,200,240,0.15),
inset 8px 0 20px rgba(255,255,255,0.12),
0 10px 30px rgba(0,0,0,0.3);
overflow:hidden;
backdrop-filter:blur(2px);
}
/* 浮雕纹路 */
.glass-body::before {
content:''; position:absolute; inset:12% 18% 28% 18%;
border-radius:8px;
background:
repeating-linear-gradient(90deg, transparent 0 10px, rgba(255,255,255,0.055) 10px 12px),
radial-gradient(ellipse at 50% 35%, rgba(255,255,255,0.07) 0%, transparent 65%);
box-shadow: inset 0 0 20px rgba(200,230,255,0.08);
pointer-events:none;
}
/* 高光 */
.glass-body::after {
content:''; position:absolute; top:6%; left:12%; width:16%; height:60%;
border-radius:50%;
background:linear-gradient(to bottom, rgba(255,255,255,0.5), rgba(255,255,255,0.05));
filter:blur(2px); opacity:.7; pointer-events:none;
}
/* 杯口 */
.glass-rim {
position:absolute; top:-3px; left:6%; right:6%; height:14px;
border-radius:50%;
border:1.5px solid rgba(230,245,255,0.55);
background:linear-gradient(to bottom, rgba(255,255,255,0.2), rgba(255,255,255,0.02));
box-shadow:0 2px 8px rgba(0,0,0,0.15), inset 0 1px 3px rgba(255,255,255,0.4);
z-index:3;
}
/* 把手 */
.glass-handle {
position:absolute; top:22%; right:-30%; width:42%; height:44%;
border:clamp(6px,1.6vw,10px) solid rgba(200,230,255,0.32);
border-left:none;
border-radius:0 50% 50% 0 / 0 50% 50% 0;
box-shadow:
0 0 0 1px rgba(255,255,255,0.12),
inset 0 0 0 1px rgba(255,255,255,0.18),
4px 4px 14px rgba(0,0,0,0.2);
background:linear-gradient(120deg, rgba(255,255,255,0.12), rgba(255,255,255,0.02));
z-index:-1;
}
/* 底部厚度 */
.glass-base {
position:absolute; bottom:0; left:5%; right:5%; height:12px;
border-radius:50%;
background:linear-gradient(to top, rgba(180,220,250,0.35), transparent);
filter:blur(1px);
}
/* 星星 */
.stars-container { position:absolute; bottom:8%; left:8%; right:8%; height:55%; z-index:2; }
.star {
position:absolute; width:22px; height:22px;
cursor:pointer;
transition:transform .2s, filter .2s;
filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}
.star:hover { transform:scale(1.25) rotate(8deg); filter:drop-shadow(0 3px 8px rgba(255,255,255,0.4)); }
.star:active { transform:scale(1.1); }
.star svg { width:100%; height:100%; display:block; }
/* 弹窗 */
.modal-overlay {
position:fixed; inset:0; z-index:100;
background:rgba(8,20,38,0.75);
backdrop-filter:blur(8px);
display:flex; align-items:center; justify-content:center;
padding:20px;
opacity:0; visibility:hidden; transition:.3s;
}
.modal-overlay.show { opacity:1; visibility:visible; }
.letter {
width:100%; max-width:380px; max-height:78vh;
background:linear-gradient(160deg, #fdf8ee 0%, #f5efe0 100%);
color:#3a3226;
border-radius:6px;
padding:32px 26px;
box-shadow:0 20px 60px rgba(0,0,0,0.5);
overflow-y:auto;
position:relative;
transform:scale(0.9); transition:.3s;
background-image:
repeating-linear-gradient(0deg, transparent, transparent 27px, rgba(180,160,120,0.12) 27px, rgba(180,160,120,0.12) 28px);
}
.modal-overlay.show .letter { transform:scale(1); }
.letter-from { font-size:12px; opacity:.55; margin-bottom:16px; letter-spacing:1px; }
.letter-content {
font-family:'Zhi Mang Xing', cursive;
font-size:22px; line-height:28px; white-space:pre-wrap;
word-break:break-word; min-height:60px;
}
.letter-actions { display:flex; gap:12px; margin-top:24px; }
.letter-actions button {
flex:1; padding:12px; border-radius:10px; border:none; font-size:14px; cursor:pointer; transition:.2s;
}
.btn-close { background:#e0d5c0; color:#5a4a30; }
.btn-close:active { transform:scale(0.97); }
.btn-danger { background:#f0c8c0; color:#8a3a2a; }
.btn-danger:active { transform:scale(0.97); }
.btn-edit { background:#c8dce8; color:#2a4a5a; }
.btn-edit:active { transform:scale(0.97); }
/* 写星星弹窗 */
.write-panel {
width:100%; max-width:380px;
background:rgba(30,55,85,0.95);
backdrop-filter:blur(20px);
border:1px solid rgba(180,210,240,0.25);
border-radius:20px; padding:26px;
box-shadow:0 20px 60px rgba(0,0,0,0.5);
transform:scale(0.9); transition:.3s;
}
.modal-overlay.show .write-panel { transform:scale(1); }
.write-title { font-size:17px; font-weight:600; margin-bottom:18px; text-align:center; letter-spacing:1px; }
.write-textarea {
width:100%; min-height:150px; padding:14px; border-radius:12px;
border:1px solid rgba(180,210,240,0.3);
background:rgba(255,255,255,0.08);
color:#fff; font-size:15px; line-height:1.6; resize:vertical;
outline:none; font-family:inherit;
}
.write-textarea::placeholder { color:rgba(255,255,255,0.3); }
.color-picker { display:flex; gap:12px; justify-content:center; margin:18px 0; }
.color-dot {
width:36px; height:36px; border-radius:50%; cursor:pointer;
border:2px solid transparent; transition:.2s;
box-shadow:0 2px 8px rgba(0,0,0,0.2);
}
.color-dot.selected { border-color:#fff; transform:scale(1.15); box-shadow:0 0 0 3px rgba(255,255,255,0.25); }
/* 空瓶提示 */
.empty-hint { position:absolute; bottom:30%; left:0; right:0; text-align:center; font-size:11px; opacity:.4; letter-spacing:1px; }
/* 加载 */
.loading { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; z-index:200; background:rgba(26,58,92,0.9); }
.loading.hidden { display:none; }
.spinner { width:36px; height:36px; border:3px solid rgba(255,255,255,0.2); border-top-color:#fff; border-radius:50%; animation:spin .8s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
/* 小屏优化 */
@media (max-width:400px) {
.glass-handle { right:-26%; }
.top-bar { top:12px; right:12px; }
.btn-add-star { padding:9px 13px; font-size:12px; }
}
</style>
</head>
<body>
<div class="loading" id="loading"><div class="spinner"></div></div>
<!-- 密码 -->
<div class="screen" id="passwordScreen">
<div class="entry-box">
<div class="entry-title">我们的星星瓶</div>
<div class="entry-sub">请输入房间密码</div>
<input class="input-field" id="passwordInput" type="password" inputmode="numeric" placeholder="····" maxlength="8" autocomplete="off">
<button class="btn-primary" id="passwordBtn">进入</button>
<div class="error-msg" id="passwordError"></div>
</div>
</div>
<!-- 选身份 -->
<div class="screen hidden" id="identityScreen">
<div class="entry-box">
<div class="entry-title">你是?</div>
<div class="entry-sub" style="margin-bottom:28px;">选择你的身份</div>
<div class="identity-btns">
<button class="identity-btn" data-user="Steven">Steven</button>
<button class="identity-btn" data-user="Holly">Holly</button>
</div>
</div>
</div>
<!-- 主页 -->
<div class="screen hidden" id="mainScreen">
<div class="current-user" id="currentUser"></div>
<div class="top-bar">
<button class="btn-add-star" id="addStarBtn">✦ 折一颗新星星</button>
</div>
<div class="bottles-wrap">
<div class="bottle-col">
<div class="bottle-label">to Steven</div>
<div class="glass" data-bottle="to Steven">
<div class="glass-handle"></div>
<div class="glass-body">
<div class="stars-container" id="starsToSteven"></div>
</div>
<div class="glass-rim"></div>
<div class="glass-base"></div>
</div>
</div>
<div class="bottle-col">
<div class="bottle-label">to Holly</div>
<div class="glass" data-bottle="to Holly">
<div class="glass-handle"></div>
<div class="glass-body">
<div class="stars-container" id="starsToHolly"></div>
</div>
<div class="glass-rim"></div>
<div class="glass-base"></div>
</div>
</div>
</div>
</div>
<!-- 读信弹窗 -->
<div class="modal-overlay" id="letterModal">
<div class="letter">
<div class="letter-from" id="letterFrom"></div>
<div class="letter-content" id="letterContent"></div>
<div class="letter-actions">
<button class="btn-edit" id="editStarBtn">修改</button>
<button class="btn-danger" id="deleteStarBtn">删除</button>
<button class="btn-close" id="closeLetterBtn">折回去</button>
</div>
</div>
</div>
<!-- 写星星弹窗 -->
<div class="modal-overlay" id="writeModal">
<div class="write-panel">
<div class="write-title" id="writeTitle">折一颗新星星</div>
<textarea class="write-textarea" id="writeText" placeholder="写点什么..."></textarea>
<div class="color-picker" id="colorPicker"></div>
<div class="letter-actions">
<button class="btn-close" id="cancelWriteBtn">取消</button>
<button class="btn-primary" style="margin:0; flex:1;" id="saveStarBtn">放进瓶子</button>
</div>
</div>
</div>
<script>
const SUPABASE_URL = 'https://hknlfxuwjwxturfbdbkg.supabase.co';
const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhrbmxmeHV3and4dHVyZmJkYmtnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3OTIyMDgzNjYsImV4cCI6MjEwNzc4NDM2Nn0.sHvNHL0VZ1wIdT6oMaTNfYlBUItwhZ6i0FSpBXpCXQc';
const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
const PASSWORDS = ['1215', '1124'];
const COLORS = [
{ name:'淡蓝', value:'#a8d4f0' },
{ name:'淡粉', value:'#f5b8d0' },
{ name:'淡紫', value:'#c8b8e8' },
{ name:'淡黄', value:'#f5e0a0' },
{ name:'淡绿', value:'#b0e0b8' },
];
let currentUser = null;
let allStars = [];
let activeStarId = null;
let editingStarId = null;
let selectedColor = COLORS[0].value;
const $ = id => document.getElementById(id);
// ===== 入口流程 =====
$('passwordBtn').onclick = () => {
const pw = $('passwordInput').value.trim();
if (PASSWORDS.includes(pw)) {
$('passwordError').textContent = '';
$('passwordScreen').classList.add('hidden');
$('identityScreen').classList.remove('hidden');
} else {
$('passwordError').textContent = '密码不对,再想想~';
$('passwordInput').value = '';
}
};
$('passwordInput').onkeydown = e => { if (e.key === 'Enter') $('passwordBtn').click(); };
document.querySelectorAll('.identity-btn').forEach(btn => {
btn.onclick = () => {
currentUser = btn.dataset.user;
$('identityScreen').classList.add('hidden');
$('mainScreen').classList.remove('hidden');
$('currentUser').textContent = '你好,' + currentUser;
loadStars();
subscribeRealtime();
};
});
// ===== 数据 =====
async function loadStars() {
$('loading').classList.remove('hidden');
const { data, error } = await supabase.from('stars').select('*').order('created_at', { ascending: true });
$('loading').classList.add('hidden');
if (error) { console.error(error); alert('加载失败:' + error.message); return; }
allStars = data || [];
renderStars();
}
function subscribeRealtime() {
supabase.channel('stars-channel')
.on('postgres_changes', { event: '*', schema: 'public', table: 'stars' }, () => {
loadStars();
})
.subscribe();
}
// ===== 渲染星星 =====
function starSvg(color) {
return `<svg viewBox="0 0 24 24"><path d="M12 2 L14.5 9 L22 9.5 L16 14 L18 21.5 L12 17.5 L6 21.5 L8 14 L2 9.5 L9.5 9 Z" fill="${color}" stroke="rgba(255,255,255,0.5)" stroke-width="0.8"/></svg>`;
}
function renderStars() {
const toSteven = allStars.filter(s => s.bottle === 'to Steven');
const toHolly = allStars.filter(s => s.bottle === 'to Holly');
paintBottle('starsToSteven', toSteven);
paintBottle('starsToHolly', toHolly);
}
function paintBottle(containerId, stars) {
const box = $(containerId);
box.innerHTML = '';
if (stars.length === 0) {
const hint = document.createElement('div');
hint.className = 'empty-hint';
hint.textContent = '还空着…';
box.appendChild(hint);
return;
}
stars.forEach((star, i) => {
const el = document.createElement('div');
el.className = 'star';
el.innerHTML = starSvg(star.color);
// 堆叠布局:底部密集,随机微散
const total = stars.length;
const perRow = 4;
const row = Math.floor(i / perRow);
const col = i % perRow;
const xBase = 8 + col * 22 + (row % 2 ? 10 : 0);
const yBase = 88 - row * 18;
const jitterX = (Math.random() - 0.5) * 10;
const jitterY = (Math.random() - 0.5) * 8;
el.style.left = Math.min(Math.max(xBase + jitterX, 0), 82) + '%';
el.style.bottom = Math.min(Math.max(yBase + jitterY, 0), 88) + '%';
el.style.transform = `rotate(${(Math.random()-0.5)*60}deg)`;
el.onclick = (e) => { e.stopPropagation(); openStar(star); };
box.appendChild(el);
});
}
// ===== 读星星 =====
function openStar(star) {
activeStarId = star.id;
$('letterFrom').textContent = star.author + ' 写于 ' + formatDate(star.created_at);
$('letterContent').textContent = star.content;
$('editStarBtn').style.display = star.author === currentUser ? 'block' : 'none';
$('deleteStarBtn').style.display = star.author === currentUser ? 'block' : 'none';
$('letterModal').classList.add('show');
}
function formatDate(ts) {
const d = new Date(ts);
return `${d.getFullYear()}.${String(d.getMonth()+1).padStart(2,'0')}.${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
}
$('closeLetterBtn').onclick = () => { $('letterModal').classList.remove('show'); activeStarId = null; };
$('deleteStarBtn').onclick = async () => {
if (!activeStarId) return;
if (!confirm('确定要撕掉这颗星星吗?')) return;
const { error } = await supabase.from('stars').delete().eq('id', activeStarId);
if (error) { alert('删除失败:' + error.message); return; }
$('letterModal').classList.remove('show');
activeStarId = null;
loadStars();
};
$('editStarBtn').onclick = () => {
const star = allStars.find(s => s.id === activeStarId);
if (!star) return;
$('letterModal').classList.remove('show');
openWritePanel(star);
};
// ===== 写星星 =====
$('addStarBtn').onclick = () => openWritePanel(null);
function openWritePanel(star) {
editingStarId = star ? star.id : null;
$('writeTitle').textContent = star ? '修改这颗星星' : '折一颗新星星';
$('writeText').value = star ? star.content : '';
selectedColor = star ? star.color : COLORS[0].value;
renderColorPicker();
$('writeModal').classList.add('show');
}
function renderColorPicker() {
const picker = $('colorPicker');
picker.innerHTML = '';
COLORS.forEach(c => {
const dot = document.createElement('div');
dot.className = 'color-dot' + (c.value === selectedColor ? ' selected' : '');
dot.style.background = c.value;
dot.title = c.name;
dot.onclick = () => { selectedColor = c.value; renderColorPicker(); };
picker.appendChild(dot);
});
}
$('cancelWriteBtn').onclick = () => {
$('writeModal').classList.remove('show');
editingStarId = null;
};
$('saveStarBtn').onclick = async () => {
const content = $('writeText').value.trim();
if (!content) { alert('写点什么再放进去吧~'); return; }
if (editingStarId) {
const { error } = await supabase.from('stars')
.update({ content, color: selectedColor })
.eq('id', editingStarId);
if (error) { alert('保存失败:' + error.message); return; }
} else {
// 检查上限
const targetBottle = currentUser === 'Holly' ? 'to Steven' : 'to Holly';
const count = allStars.filter(s => s.bottle === targetBottle).length;
if (count >= 30) { alert('这个瓶子已经装满 30 颗星星啦~'); return; }
const { error } = await supabase.from('stars').insert({
bottle: targetBottle,
author: currentUser,
content,
color: selectedColor,
});
if (error) { alert('保存失败:' + error.message); return; }
}
$('writeModal').classList.remove('show');
editingStarId = null;
loadStars();
};
// 点遮罩关闭
$('letterModal').onclick = e => { if (e.target === $('letterModal')) $('closeLetterBtn').click(); };
$('writeModal').onclick = e => { if (e.target === $('writeModal')) $('cancelWriteBtn').click(); };
// 瓶子点击放大(可选交互:点瓶身打开最近一颗)
document.querySelectorAll('.glass').forEach(g => {
g.onclick = () => {
const bottleName = g.dataset.bottle;
const stars = allStars.filter(s => s.bottle === bottleName);
if (stars.length === 0) return;
// 随机打开一颗
const random = stars[Math.floor(Math.random() * stars.length)];
openStar(random);
};
});
</script>
</body>
</html>