源码介绍
一款解决“吃什么”难题的趣味点餐助手。通过随机转盘形式,为您推荐美食选择,并支持自定义菜单,让点餐过程变得轻松有趣,是朋友聚餐与日常用餐的决策好帮手。
源码:
演示:https://tool.jetmast.com/chi/
<!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>美食大转盘 - 点餐助手 - 墨桅博客jetmast.com</title>
<style>
/*
============================================
样式表说明 - 现代素雅版
============================================
设计理念:
- 保留原有的简约、清晰、易用风格
- 加入现代玻璃拟态和深度设计元素
- 优化移动端触摸体验和视觉效果
- 保持功能完整性和代码可维护性
============================================
*/
:root {
--primary-color: #4a90e2;
--primary-light: #6aa8f7;
--primary-dark: #357abd;
--secondary-color: #50c878;
--accent-color: #ff8c42;
--danger-color: #ff6b6b;
--warning-color: #ffa726;
--text-primary: #2d3748;
--text-secondary: #718096;
--text-light: #a0aec0;
--bg-primary: #ffffff;
--bg-secondary: #f7fafc;
--bg-overlay: rgba(255, 255, 255, 0.85);
--border-color: #e2e8f0;
--shadow-light: rgba(0, 0, 0, 0.04);
--shadow-medium: rgba(0, 0, 0, 0.1);
--shadow-heavy: rgba(0, 0, 0, 0.15);
--glass-blur: 12px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html {
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-attachment: fixed;
padding: 1rem;
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.header h1 {
font-size: clamp(1.8rem, 5vw, 2.8rem);
font-weight: 800;
color: white;
margin: 0.5rem 0;
letter-spacing: -0.5px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header p {
color: rgba(255, 255, 255, 0.9);
font-size: 1.1rem;
font-weight: 400;
margin-top: 0.5rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.shortcut-hint {
display: inline-block;
background: rgba(255, 255, 255, 0.15);
color: white;
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.9rem;
margin-top: 1rem;
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(var(--glass-blur));
}
.main-grid {
display: grid;
grid-template-columns: 350px 1fr;
gap: 2rem;
align-items: start;
}
@media (max-width: 900px) {
.main-grid {
grid-template-columns: 1fr;
}
.list-panel { order: 2; }
.wheel-panel { order: 1; }
}
.panel {
background: var(--bg-overlay);
border-radius: 20px;
padding: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px var(--shadow-medium);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(var(--glass-blur));
}
.panel:hover {
box-shadow: 0 12px 40px var(--shadow-heavy);
transform: translateY(-2px);
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
flex-wrap: wrap;
gap: 0.75rem;
}
.panel-header h2 {
font-size: 1.4rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.count-badge {
font-size: 0.85rem;
font-weight: 600;
color: white;
background: var(--primary-color);
padding: 0.3rem 0.7rem;
border-radius: 20px;
box-shadow: 0 2px 4px rgba(74, 144, 226, 0.3);
}
.input-group {
margin-bottom: 1.5rem;
}
.input-label {
color: var(--text-primary);
margin-bottom: 0.5rem;
display: block;
font-weight: 600;
font-size: 1rem;
}
.input-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
input, textarea {
background: white;
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 0.85rem 1rem;
color: var(--text-primary);
outline: none;
transition: all 0.2s;
width: 100%;
font-size: 1rem;
font-weight: 400;
box-shadow: 0 2px 4px var(--shadow-light);
}
input::placeholder, textarea::placeholder {
color: var(--text-light);
font-weight: 400;
}
input:focus, textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.15);
}
.input-row input {
flex: 1;
min-width: 150px;
}
.btn {
padding: 0.85rem 1.25rem;
border: none;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
text-decoration: none;
user-select: none;
white-space: nowrap;
font-size: 0.95rem;
position: relative;
box-shadow: 0 4px 6px var(--shadow-light);
touch-action: manipulation;
}
.btn::after {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 2px solid var(--primary-color);
border-radius: 14px;
opacity: 0;
transition: opacity 0.2s;
}
.btn.highlight::after {
opacity: 1;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
.btn:active:not(:disabled) {
transform: scale(0.96);
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 100%);
box-shadow: 0 6px 12px rgba(74, 144, 226, 0.3);
transform: translateY(-1px);
}
.btn-success {
background: linear-gradient(135deg, var(--secondary-color) 0%, #66d98a 100%);
color: white;
}
.btn-success:hover:not(:disabled) {
background: linear-gradient(135deg, #40b368 0%, var(--secondary-color) 100%);
box-shadow: 0 6px 12px rgba(80, 200, 120, 0.3);
transform: translateY(-1px);
}
.btn-secondary {
background: white;
color: var(--text-primary);
border: 2px solid var(--border-color);
}
.btn-secondary:hover:not(:disabled) {
background: var(--bg-secondary);
border-color: var(--primary-color);
box-shadow: 0 4px 8px var(--shadow-light);
transform: translateY(-1px);
}
.btn-danger {
background: linear-gradient(135deg, var(--danger-color) 0%, #ff8787 100%);
color: white;
}
.btn-danger:hover:not(:disabled) {
background: linear-gradient(135deg, #ff5252 0%, var(--danger-color) 100%);
box-shadow: 0 6px 12px rgba(255, 107, 107, 0.3);
transform: translateY(-1px);
}
.btn-warning {
background: linear-gradient(135deg, var(--warning-color) 0%, #ffb74d 100%);
color: white;
}
.btn-warning:hover:not(:disabled) {
background: linear-gradient(135deg, #ff9800 0%, var(--warning-color) 100%);
box-shadow: 0 6px 12px rgba(255, 167, 38, 0.3);
transform: translateY(-1px);
}
.punishment-list {
max-height: 400px;
overflow-y: auto;
margin-bottom: 1.5rem;
padding-right: 0.5rem;
}
.punishment-list::-webkit-scrollbar {
width: 6px;
}
.punishment-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.punishment-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.punishment-list::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.punishment-item {
background: white;
padding: 1rem;
border-radius: 12px;
margin-bottom: 0.75rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
color: var(--text-primary);
border: 2px solid transparent;
box-shadow: 0 2px 4px var(--shadow-light);
}
.punishment-item:hover {
background: #f9f9f9;
transform: translateX(4px);
box-shadow: 0 4px 8px var(--shadow-light);
}
.punishment-item.active {
background: rgba(74, 144, 226, 0.08);
border-color: var(--primary-color);
font-weight: 600;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
}
.punishment-item .remove-btn {
background: transparent;
border: 2px solid var(--border-color);
color: var(--text-light);
cursor: pointer;
font-size: 1.2rem;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
touch-action: manipulation;
}
.punishment-item .remove-btn:hover {
background: var(--danger-color);
border-color: var(--danger-color);
color: white;
transform: scale(1.1);
}
.wheel-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
.wheel-wrapper {
position: relative;
margin-bottom: 2rem;
width: 100%;
max-width: 450px;
aspect-ratio: 1/1;
filter: drop-shadow(0 8px 16px var(--shadow-medium));
}
.wheel-pointer {
position: absolute;
top: -18px;
left: 50%;
transform: translateX(-50%);
z-index: 20;
width: 48px;
height: 48px;
background: linear-gradient(135deg, var(--accent-color) 0%, #ffa726 100%);
border: 4px solid white;
border-radius: 50%;
box-shadow: 0 6px 12px var(--shadow-heavy);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
}
.wheel-pointer::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border-left: 12px solid transparent;
border-right: 12px solid transparent;
border-top: 18px solid var(--accent-color);
filter: drop-shadow(0 4px 4px var(--shadow-medium));
}
.wheel-svg {
width: 100%;
height: 100%;
transition-property: transform;
transition-timing-function: cubic-bezier(0.17, 0.67, 0.12, 0.99);
}
.control-buttons {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
justify-content: center;
}
.result-display {
background: white;
border: 3px solid var(--primary-color);
color: var(--text-primary);
padding: 1.5rem 2.5rem;
border-radius: 16px;
text-align: center;
font-weight: 600;
box-shadow: 0 8px 24px var(--shadow-medium);
min-width: 280px;
max-width: 100%;
transform: scale(0.9);
opacity: 0;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.result-display.show {
transform: scale(1);
opacity: 1;
animation: bounce 0.6s ease;
}
.result-display h3 {
font-size: 1.3rem;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.result-display .punishment-text {
font-size: 1.8rem;
font-weight: 800;
word-break: break-word;
line-height: 1.4;
color: var(--primary-color);
}
.celebration-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
backdrop-filter: blur(4px);
}
.celebration-content {
background: white;
border-radius: 24px;
padding: 2.5rem;
max-width: 32rem;
width: 100%;
text-align: center;
position: relative;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.celebration-close {
position: absolute;
top: 1rem;
right: 1rem;
width: 2.8rem;
height: 2.8rem;
background: #f8f9fa;
border: 2px solid var(--border-color);
border-radius: 50%;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
transition: all 0.2s;
font-weight: 300;
touch-action: manipulation;
}
.celebration-close:hover {
background: var(--danger-color);
border-color: var(--danger-color);
color: white;
transform: scale(1.1);
}
.celebration-inner {
position: relative;
}
.celebration-emoji {
font-size: 5rem;
margin-bottom: 1rem;
display: block;
animation: bounce 1s ease infinite;
}
.celebration-title {
font-size: 2rem;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 1.5rem;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--accent-color) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.celebration-result-box {
background: linear-gradient(135deg, var(--bg-secondary) 0%, white 100%);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
border: 2px solid var(--border-color);
box-shadow: inset 0 2px 4px var(--shadow-light);
}
.celebration-result-label {
font-size: 1.1rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
font-weight: 600;
}
.celebration-result-text {
font-size: 2rem;
font-weight: 800;
color: var(--primary-color);
line-height: 1.3;
}
.celebration-btn {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
color: white;
border: none;
padding: 1rem 3rem;
border-radius: 12px;
font-weight: 700;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 6px 12px rgba(74, 144, 226, 0.3);
touch-action: manipulation;
}
.celebration-btn:hover {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 100%);
box-shadow: 0 8px 16px rgba(74, 144, 226, 0.4);
transform: translateY(-2px);
}
.celebration-btn:active {
transform: scale(0.96);
}
.empty-state {
padding: 3rem 2rem;
border-radius: 12px;
background: var(--bg-secondary);
color: var(--text-light);
text-align: center;
border: 2px dashed var(--border-color);
font-size: 1.1rem;
}
.file-import-note {
color: var(--text-secondary);
font-size: 0.85rem;
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.clear-all-section {
display: flex;
justify-content: flex-end;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
}
.confirm-clear-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
backdrop-filter: blur(4px);
}
.confirm-clear-content {
background: white;
border-radius: 24px;
padding: 2.5rem;
max-width: 28rem;
width: 100%;
text-align: center;
position: relative;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.confirm-clear-icon {
font-size: 4rem;
margin-bottom: 1rem;
display: block;
color: var(--warning-color);
}
.confirm-clear-title {
font-size: 1.8rem;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 1rem;
}
.confirm-clear-message {
font-size: 1rem;
color: var(--text-secondary);
margin-bottom: 2rem;
line-height: 1.6;
}
.confirm-clear-count {
font-weight: 800;
color: var(--danger-color);
font-size: 1.3rem;
}
.confirm-clear-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.confirm-clear-btn {
padding: 0.85rem 2rem;
border: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 120px;
touch-action: manipulation;
}
.confirm-clear-btn.confirm {
background: linear-gradient(135deg, var(--secondary-color) 0%, #66d98a 100%);
color: white;
box-shadow: 0 4px 8px rgba(80, 200, 120, 0.3);
}
.confirm-clear-btn.confirm:hover {
background: linear-gradient(135deg, #40b368 0%, var(--secondary-color) 100%);
box-shadow: 0 6px 12px rgba(80, 200, 120, 0.4);
transform: translateY(-1px);
}
.confirm-clear-btn.cancel {
background: white;
color: var(--text-primary);
border: 2px solid var(--border-color);
box-shadow: 0 2px 4px var(--shadow-light);
}
.confirm-clear-btn.cancel:hover {
background: var(--bg-secondary);
border-color: var(--primary-color);
box-shadow: 0 4px 8px var(--shadow-light);
transform: translateY(-1px);
}
@keyframes bounce {
0%, 20%, 53%, 80%, 100% { transform: translateY(0); }
40%, 43% { transform: translateY(-12px); }
70% { transform: translateY(-6px); }
90% { transform: translateY(-3px); }
}
@keyframes modalPop {
0% { transform: scale(0.85) translateY(30px); opacity: 0; }
100% { transform: scale(1) translateY(0); opacity: 1; }
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.08); }
100% { transform: scale(1); }
}
.hidden {
display: none !important;
}
.modal-enter {
animation: modalPop 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.pulse {
animation: pulse 0.6s ease;
}
/* 移动端深度优化 */
@media (max-width: 768px) {
body {
padding: 0.75rem;
}
.panel {
padding: 1.25rem;
border-radius: 16px;
}
.header h1 {
font-size: clamp(1.5rem, 6vw, 2.2rem);
}
.header p {
font-size: 1rem;
}
.shortcut-hint {
font-size: 0.85rem;
padding: 0.35rem 0.7rem;
}
.control-buttons {
flex-direction: column;
align-items: stretch;
width: 100%;
max-width: 350px;
}
.btn {
width: 100%;
padding: 1rem;
font-size: 1rem;
}
.celebration-content {
padding: 2rem 1.5rem;
border-radius: 20px;
}
.celebration-title {
font-size: 1.6rem;
}
.celebration-emoji {
font-size: 4rem;
}
.celebration-result-text {
font-size: 1.6rem;
}
.celebration-result-box {
padding: 1.5rem;
}
.confirm-clear-content {
padding: 2rem 1.5rem;
border-radius: 20px;
}
.confirm-clear-title {
font-size: 1.5rem;
}
.wheel-wrapper {
max-width: 380px;
}
.result-display {
padding: 1.25rem 1.5rem;
min-width: auto;
width: 100%;
max-width: 350px;
}
.result-display .punishment-text {
font-size: 1.5rem;
}
.punishment-item {
padding: 0.85rem;
}
.punishment-item .remove-btn {
width: 30px;
height: 30px;
font-size: 1.1rem;
}
.input-row {
flex-direction: column;
}
.input-row input {
min-width: auto;
}
.panel-header {
flex-direction: column;
align-items: flex-start;
}
.panel-header .btn {
width: auto;
align-self: flex-end;
}
}
/* 超小屏幕优化 */
@media (max-width: 360px) {
.wheel-wrapper {
max-width: 300px;
}
.celebration-content, .confirm-clear-content {
padding: 1.5rem 1rem;
}
.celebration-title {
font-size: 1.4rem;
}
.celebration-result-text {
font-size: 1.4rem;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 头部标题 -->
<div class="header">
<h1>🍽️ 美食大转盘</h1>
<p>还在为"吃什么"发愁吗?让转盘帮你决定今天的美食!</p>
<div class="shortcut-hint">💡 提示:按空格键可以快速开始点餐</div>
</div>
<div class="main-grid">
<!-- 菜品管理面板 -->
<div class="panel list-panel">
<div class="panel-header">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<h2>📋 我的美食菜单</h2>
<div class="count-badge" id="punishmentCount">0项</div>
</div>
<button class="btn btn-danger" id="clearAllPunishmentsBtn">
🗑️ 一键清空
</button>
</div>
<div class="input-group">
<label class="input-label">添加新菜品:</label>
<div class="input-row">
<input type="text" id="punishmentInput" placeholder="例如:麻辣香锅、披萨、寿司..." autocomplete="off">
<button class="btn btn-primary" id="addPunishmentBtn">
➕ 添加
</button>
</div>
</div>
<!-- 文件导入功能 -->
<div class="input-group">
<label class="input-label">导入菜品列表(TXT文件):</label>
<div class="input-row">
<input type="file" id="importFile" accept=".txt" style="flex: 1; padding: 0.6rem;">
<button class="btn btn-secondary" id="importBtn">
📁 导入
</button>
</div>
<div class="file-import-note">
ℹ️ 每行一个菜品,支持批量导入
</div>
</div>
<div class="punishment-list" id="punishmentList">
<!-- 菜品项目动态加载 -->
</div>
<div class="clear-all-section">
<button class="btn btn-warning" id="resetToDefaultBtn">
🔄 恢复默认菜单
</button>
</div>
</div>
<!-- 转盘控制面板 -->
<div class="panel wheel-panel" style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
<div class="wheel-container">
<div class="wheel-wrapper">
<!-- 转盘指针 -->
<div class="wheel-pointer">🎯</div>
<!-- SVG转盘 -->
<svg id="wheelSvg" class="wheel-svg" viewBox="-250 -250 500 500">
<defs>
<radialGradient id="centerGradient">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="100%" stop-color="#f0f0f0" />
</radialGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="rgba(0,0,0,0.15)"/>
</filter>
</defs>
<!-- 转盘扇形区域(动态生成) -->
<g id="wheelSections"></g>
<!-- 转盘中心装饰 -->
<circle cx="0" cy="0" r="50" fill="url(#centerGradient)" stroke="#fff" stroke-width="4" filter="url(#shadow)"/>
<circle cx="0" cy="0" r="35" fill="none" stroke="#e0e0e0" stroke-width="2" stroke-dasharray="4 4"/>
<text x="0" y="5" fill="#4a90e2" font-size="18" font-weight="bold" text-anchor="middle" dominant-baseline="middle">点餐</text>
</svg>
</div>
<!-- 控制按钮区域 -->
<div class="control-buttons">
<button class="btn btn-primary" id="startBtn" style="min-width: 160px; padding: 1.1rem;">
<span id="startBtnText">🎰 开始点餐</span>
</button>
<button class="btn btn-secondary" id="resetBtn" style="padding: 1.1rem;">
🔄 重置转盘
</button>
</div>
<!-- 结果展示区域 -->
<div id="resultDisplay" class="result-display">
<h3>🎉 今日推荐</h3>
<div class="punishment-text" id="winnerPunishment">等待选择...</div>
</div>
</div>
</div>
</div>
</div>
<!-- 庆祝弹窗 -->
<div id="celebrationModal" class="celebration-modal hidden">
<div class="celebration-content">
<button class="celebration-close" id="closeCelebrationBtn">×</button>
<div class="celebration-inner">
<span class="celebration-emoji">🍽️</span>
<h2 class="celebration-title">今日美食已选定!</h2>
<div class="celebration-result-box">
<div class="celebration-result-label">转盘推荐:</div>
<div class="celebration-result-text" id="celebrationPunishment"></div>
</div>
<button class="celebration-btn" id="confirmCelebrationBtn">
✅ 就吃这个了!
</button>
</div>
</div>
</div>
<!-- 一键清空确认弹窗 -->
<div id="confirmClearModal" class="confirm-clear-modal hidden">
<div class="confirm-clear-content modal-enter">
<span class="confirm-clear-icon">⚠️</span>
<h2 class="confirm-clear-title">确认清空菜单</h2>
<div class="confirm-clear-message">
您确定要清空所有 <span class="confirm-clear-count" id="clearCount">0</span> 个菜品吗?<br>
此操作无法撤销,所有自定义和导入的菜品将被永久删除。
</div>
<div class="confirm-clear-buttons">
<button class="confirm-clear-btn cancel" id="cancelClearBtn">
❌ 取消
</button>
<button class="confirm-clear-btn confirm" id="confirmClearBtn">
✅ 确定清空
</button>
</div>
</div>
</div>
<script>
// ========== 全局变量声明 ==========
// 默认菜品列表
const defaultPunishments = [
'披萨',
'汉堡',
'拉面',
'寿司',
'麻辣香锅',
'火锅',
'沙拉',
'咖喱饭',
'便当',
'三明治',
'意大利面',
'墨西哥卷饼',
'饺子',
'炸虾',
'冰淇淋'
];
// 从LocalStorage加载数据
let punishments = JSON.parse(localStorage.getItem('wheelPunishments')) || [...defaultPunishments];
// 转盘状态控制变量
let isSpinning = false;
let currentRotation = 0;
let currentPunishment = '';
// 键盘快捷键状态
let isSpacebarPressed = false;
let spacebarCooldown = false;
// 转盘扇形颜色列表 - 现代柔和色调
const colors = [
'#4a90e2', '#50c878', '#ff8c42', '#9c5bdf', '#ff6b6b',
'#20b2aa', '#ffa726', '#7e8c8d', '#e91e63', '#009688',
'#673ab7', '#ff5722', '#795548', '#607d8b', '#3f51b5',
'#00bcd4', '#8bc34a', '#ffc107', '#9e9e9e', '#f44336'
];
// ========== 工具函数 ==========
/**
* 更新菜品项目计数显示
*/
function updatePunishmentCount() {
const countElement = document.getElementById('punishmentCount');
const count = punishments.length;
countElement.textContent = `${count}项`;
}
/**
* 保存菜品列表到LocalStorage
*/
function savePunishments() {
localStorage.setItem('wheelPunishments', JSON.stringify(punishments));
}
/**
* 显示一键清空确认弹窗
*/
function showClearConfirmation() {
if (isSpinning) {
alert('转盘旋转时不能清空菜品!');
return;
}
if (punishments.length === 0) {
alert('当前没有菜品可清空!');
return;
}
// 更新清空数量显示
document.getElementById('clearCount').textContent = punishments.length;
// 显示确认弹窗
const modal = document.getElementById('confirmClearModal');
modal.classList.remove('hidden');
}
/**
* 隐藏一键清空确认弹窗
*/
function hideClearConfirmation() {
document.getElementById('confirmClearModal').classList.add('hidden');
}
/**
* 执行一键清空操作
*/
function clearAllPunishments() {
if (isSpinning) {
alert('转盘旋转时不能清空菜品!');
return;
}
// 记录清空前的数量
const previousCount = punishments.length;
// 清空菜品列表
punishments = [];
// 更新UI
updatePunishmentList();
updatePunishmentCount();
updateWheel();
// 保存到LocalStorage
savePunishments();
// 重置转盘状态
currentRotation = 0;
currentPunishment = '';
const wheelSvg = document.getElementById('wheelSvg');
wheelSvg.style.transitionDuration = '0ms';
wheelSvg.style.transform = 'rotate(0deg)';
// 隐藏结果
const resultDisplay = document.getElementById('resultDisplay');
resultDisplay.classList.remove('show');
// 隐藏庆祝弹窗
closeCelebration();
// 显示操作反馈
alert(`已清空 ${previousCount} 个菜品!`);
// 隐藏确认弹窗
hideClearConfirmation();
}
/**
* 恢复默认菜单
*/
function resetToDefaultMenu() {
if (isSpinning) {
alert('转盘旋转时不能恢复默认菜单!');
return;
}
if (!confirm('确定要恢复默认菜单吗?当前所有菜品将被替换为默认菜品。')) {
return;
}
// 恢复默认菜品列表
punishments = [...defaultPunishments];
// 更新UI
updatePunishmentList();
updatePunishmentCount();
updateWheel();
// 保存到LocalStorage
savePunishments();
alert('已恢复默认菜单!');
}
/**
* 导入TXT文件并解析菜品列表
*/
function importPunishmentsFromTxt(file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const content = e.target.result;
// 按换行符分割,过滤空行和首尾空格
const newPunishments = content.split(/\r?\n/)
.map(item => item.trim())
.filter(item => item.length > 0);
if (newPunishments.length === 0) {
alert('文件为空或格式不正确!');
return;
}
// 合并并去重
const uniqueNewPunishments = newPunishments.filter(
item => !punishments.includes(item)
);
if (uniqueNewPunishments.length === 0) {
alert('所有菜品已存在!');
return;
}
// 添加到现有列表
punishments.push(...uniqueNewPunishments);
// 更新UI并保存
updatePunishmentList();
updatePunishmentCount();
updateWheel();
savePunishments();
alert(`成功导入 ${uniqueNewPunishments.length} 个新菜品!${newPunishments.length - uniqueNewPunishments.length > 0 ? `有 ${newPunishments.length - uniqueNewPunishments.length} 个重复菜品已跳过。` : ''}`);
} catch (error) {
console.error('导入文件时出错:', error);
alert('文件解析失败,请检查文件格式!');
}
};
reader.onerror = function() {
alert('读取文件失败!');
};
reader.readAsText(file);
}
/**
* 高亮开始按钮(快捷键提示)
*/
function highlightStartButton() {
const startBtn = document.getElementById('startBtn');
startBtn.classList.add('highlight', 'pulse');
setTimeout(() => {
startBtn.classList.remove('pulse');
}, 600);
setTimeout(() => {
startBtn.classList.remove('highlight');
}, 1200);
}
// ========== DOM加载完成事件 ==========
document.addEventListener('DOMContentLoaded', function() {
// 初始化UI
updatePunishmentList();
updatePunishmentCount();
updateWheel();
// 输入框回车事件
const input = document.getElementById('punishmentInput');
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addPunishment();
}
});
// 绑定按钮事件
document.getElementById('addPunishmentBtn').addEventListener('click', addPunishment);
document.getElementById('importBtn').addEventListener('click', function() {
const fileInput = document.getElementById('importFile');
if (fileInput.files.length > 0) {
importPunishmentsFromTxt(fileInput.files[0]);
fileInput.value = ''; // 清空文件选择
} else {
alert('请先选择文件!');
}
});
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('resetBtn').addEventListener('click', resetWheel);
document.getElementById('resetToDefaultBtn').addEventListener('click', resetToDefaultMenu);
document.getElementById('closeCelebrationBtn').addEventListener('click', closeCelebration);
document.getElementById('confirmCelebrationBtn').addEventListener('click', closeCelebration);
// 一键清空相关事件
document.getElementById('clearAllPunishmentsBtn').addEventListener('click', showClearConfirmation);
document.getElementById('cancelClearBtn').addEventListener('click', hideClearConfirmation);
document.getElementById('confirmClearBtn').addEventListener('click', clearAllPunishments);
// 点击确认弹窗外部关闭弹窗
document.getElementById('confirmClearModal').addEventListener('click', function(e) {
if (e.target === this) {
hideClearConfirmation();
}
});
// 点击庆祝弹窗外部关闭弹窗
document.getElementById('celebrationModal').addEventListener('click', function(e) {
if (e.target === this) {
closeCelebration();
}
});
// 使用事件委托处理动态生成的删除按钮
document.getElementById('punishmentList').addEventListener('click', function(e) {
if (e.target.classList.contains('remove-btn')) {
const item = e.target.closest('.punishment-item');
const index = Array.from(this.children).indexOf(item);
if (index !== -1) {
removePunishment(index);
}
}
});
// 键盘事件监听 - 空格键开始点餐
document.addEventListener('keydown', function(e) {
// 检查是否按下了空格键
if (e.code === 'Space' || e.key === ' ') {
e.preventDefault(); // 防止页面滚动
// 检查是否在输入框中
const activeElement = document.activeElement;
const isInputFocused = activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA';
// 检查是否有弹窗打开
const isModalOpen = !document.getElementById('celebrationModal').classList.contains('hidden') ||
!document.getElementById('confirmClearModal').classList.contains('hidden');
// 如果不在输入框中且没有弹窗打开,且转盘没有在旋转
if (!isInputFocused && !isModalOpen && !isSpinning) {
startGame();
highlightStartButton();
}
}
});
// 添加空格键提示的视觉反馈
document.addEventListener('keyup', function(e) {
if (e.code === 'Space' || e.key === ' ') {
isSpacebarPressed = false;
}
});
});
// ========== 核心功能函数 ==========
/**
* 添加新菜品项目
*/
function addPunishment() {
const input = document.getElementById('punishmentInput');
const name = input.value.trim();
// 输入验证
if (!name) {
alert('请输入菜品名称!');
return;
}
if (name.length > 30) {
alert('菜品名称过长,请控制在30字以内!');
return;
}
if (!punishments.includes(name)) {
punishments.push(name);
input.value = '';
input.focus();
updatePunishmentList();
updatePunishmentCount();
updateWheel();
savePunishments();
} else {
alert('该菜品已存在!');
}
}
/**
* 删除菜品项目
*/
function removePunishment(index) {
if (isSpinning) {
alert('转盘旋转时不能删除菜品!');
return;
}
if (confirm('确定要删除这个菜品吗?')) {
punishments.splice(index, 1);
updatePunishmentList();
updatePunishmentCount();
updateWheel();
savePunishments();
}
}
/**
* 更新菜品列表UI
*/
function updatePunishmentList() {
const list = document.getElementById('punishmentList');
// 处理空列表状态
if (punishments.length === 0) {
list.innerHTML = '<div class="empty-state">🍽️ 菜品菜单是空的,请先添加菜品!</div>';
return;
}
// 生成列表HTML
list.innerHTML = punishments.map((punishment, index) => `
<div class="punishment-item ${currentPunishment === punishment ? 'active' : ''}">
<span>${punishment}</span>
<button class="remove-btn" title="删除此项">×</button>
</div>
`).join('');
}
/**
* 更新转盘SVG图形
*/
function updateWheel() {
const sectionsGroup = document.getElementById('wheelSections');
sectionsGroup.innerHTML = '';
// 空转盘状态
if (punishments.length === 0) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M 0 0 L 240 0 A 240 240 0 1 1 -240 0 Z');
path.setAttribute('fill', '#f5f5f5');
path.setAttribute('stroke', '#e0e0e0');
path.setAttribute('stroke-width', '2');
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', '0');
text.setAttribute('y', '0');
text.setAttribute('fill', '#999');
text.setAttribute('font-size', '24');
text.setAttribute('font-weight', '500');
text.setAttribute('text-anchor', 'middle');
text.setAttribute('dominant-baseline', 'middle');
text.textContent = '请添加菜品';
sectionsGroup.appendChild(path);
sectionsGroup.appendChild(text);
return;
}
// 计算每个扇形的角度
const sectionAngle = 360 / punishments.length;
const radius = 240;
// 生成每个扇形
punishments.forEach((punishment, index) => {
// 计算起始和结束角度(弧度制)
const startAngle = index * sectionAngle;
const endAngle = (index + 1) * sectionAngle;
const startAngleRad = (startAngle * Math.PI) / 180;
const endAngleRad = (endAngle * Math.PI) / 180;
// 判断是否为大圆弧
const largeArcFlag = sectionAngle > 180 ? 1 : 0;
// 计算弧线起点和终点坐标
const x1 = Math.cos(startAngleRad) * radius;
const y1 = Math.sin(startAngleRad) * radius;
const x2 = Math.cos(endAngleRad) * radius;
const y2 = Math.sin(endAngleRad) * radius;
// 构建扇形路径数据
const pathData = `M 0 0 L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z`;
// 创建扇形路径元素
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathData);
path.setAttribute('fill', colors[index % colors.length]);
path.setAttribute('stroke', '#fff');
path.setAttribute('stroke-width', '3');
path.setAttribute('filter', 'url(#shadow)');
// 计算文本位置
const midAngle = (startAngle + endAngle) / 2;
const textRadius = radius * 0.65;
const textX = Math.cos((midAngle * Math.PI) / 180) * textRadius;
const textY = Math.sin((midAngle * Math.PI) / 180) * textRadius;
// 根据项目数量动态调整字体大小
let fontSize = 20;
if (punishments.length > 20) fontSize = 12;
else if (punishments.length > 15) fontSize = 14;
else if (punishments.length > 10) fontSize = 16;
else if (punishments.length > 8) fontSize = 18;
// 创建文本元素
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', textX);
text.setAttribute('y', textY);
text.setAttribute('fill', '#fff');
text.setAttribute('font-size', fontSize);
text.setAttribute('font-weight', '700');
text.setAttribute('text-anchor', 'middle');
text.setAttribute('dominant-baseline', 'middle');
text.setAttribute('transform', `rotate(${midAngle}, ${textX}, ${textY})`);
text.setAttribute('style', 'text-shadow: 0 1px 2px rgba(0,0,0,0.3);');
// 智能文本截断处理
const maxChars = Math.max(3, Math.floor(60 / fontSize));
let displayText = punishment;
if (punishment.length > maxChars) {
displayText = punishment.substring(0, maxChars) + '..';
}
text.textContent = displayText;
// 添加到SVG
sectionsGroup.appendChild(path);
sectionsGroup.appendChild(text);
});
}
/**
* 开始转盘游戏
*/
function startGame() {
// 前置检查
if (punishments.length === 0) {
alert('请先添加菜品!');
return;
}
if (isSpinning) return;
// 设置旋转状态
isSpinning = true;
currentPunishment = '';
// 更新按钮状态
const startBtn = document.getElementById('startBtn');
const startBtnText = document.getElementById('startBtnText');
startBtn.disabled = true;
startBtnText.textContent = '⏳ 点餐中...';
// 隐藏上一个结果
const resultDisplay = document.getElementById('resultDisplay');
resultDisplay.classList.remove('show');
// 生成随机动画参数(4-8秒随机时长)
const randomDuration = 4000 + Math.random() * 4000;
// 计算最终旋转角度
const randomDegreeOffset = Math.floor(Math.random() * 360);
const randomSpins = 6 + Math.floor(Math.random() * 6); // 6-11圈
const finalRotation = currentRotation + (randomSpins * 360) + randomDegreeOffset;
// 更新当前角度
currentRotation = finalRotation;
// 应用旋转动画
const wheelSvg = document.getElementById('wheelSvg');
wheelSvg.style.transitionDuration = `${randomDuration}ms`;
wheelSvg.style.transform = `rotate(${currentRotation}deg)`;
// 动画结束后计算结果
setTimeout(() => {
isSpinning = false;
// 转盘指针固定角度(顶部)
const POINTER_ANGLE = 270;
// 计算实际旋转角度
const actualRotation = currentRotation % 360;
// 计算指针指向的角度
let winningAngle = (POINTER_ANGLE - actualRotation);
// 角度归一化处理
winningAngle = winningAngle % 360;
if (winningAngle < 0) {
winningAngle += 360;
}
// 根据角度计算选中扇形的索引
const sectionAngle = 360 / punishments.length;
const winningIndex = Math.floor(winningAngle / sectionAngle);
// 获取选中的菜品
currentPunishment = punishments[winningIndex] || punishments[0];
// 恢复按钮状态
startBtn.disabled = false;
startBtnText.textContent = '🎰 再次点餐';
// 显示结果
document.getElementById('winnerPunishment').textContent = currentPunishment;
resultDisplay.classList.add('show');
// 更新列表高亮
updatePunishmentList();
// 显示庆祝弹窗
showCelebration();
}, randomDuration);
}
/**
* 重置转盘
*/
function resetWheel() {
if (isSpinning) {
alert('转盘旋转时不能重置!');
return;
}
if (!confirm('确定要重置转盘吗?')) return;
currentRotation = 0;
currentPunishment = '';
// 立即重置转盘
const wheelSvg = document.getElementById('wheelSvg');
wheelSvg.style.transitionDuration = '0ms';
wheelSvg.style.transform = 'rotate(0deg)';
// 隐藏结果
const resultDisplay = document.getElementById('resultDisplay');
resultDisplay.classList.remove('show');
// 更新UI
updatePunishmentList();
closeCelebration();
}
/**
* 显示庆祝弹窗
*/
function showCelebration() {
const modal = document.getElementById('celebrationModal');
document.getElementById('celebrationPunishment').textContent = currentPunishment;
// 显示并添加动画
modal.classList.remove('hidden');
const content = modal.querySelector('.celebration-content');
content.classList.remove('modal-enter');
void content.offsetWidth; // 触发重排
content.classList.add('modal-enter');
}
/**
* 关闭庆祝弹窗
*/
function closeCelebration() {
document.getElementById('celebrationModal').classList.add('hidden');
}
</script>
</body>
</html>

请登录后发表评论
注册
停留在世界边缘,与之惜别