马上注册,结交更多好友,享用更多功能,让你轻松玩转小K网。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
【HTML】美食大转盘 - 点餐助手
增加导入txt文本功能,增加一键清空功能,将所有代码进行注释,方便大家修改,瞎弄的,博大家一笑!!!
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>美食大转盘 - 点餐助手</title>
- <style>
- /*
- ============================================
- 样式表说明
- ============================================
- 给维护者:
- - 使用CSS变量确保颜色一致性
- - 采用响应式设计,移动端优先
- - 使用backdrop-filter实现毛玻璃效果
- 给学习者:
- - 使用clamp()实现响应式字体
- - aspect-ratio保持元素比例
- - CSS Grid创建两栏布局
- ============================================
- */
-
- :root {
- --primary-color: #f97316;
- --secondary-color: #db2777;
- --accent-color: #7c3aed;
- --success-color: #10b981;
- --warning-color: #f59e0b;
- --danger-color: #ef4444;
- --text-light: rgba(255, 255, 255, 0.9);
- --text-lighter: rgba(255, 255, 255, 0.7);
- --bg-overlay: rgba(255, 255, 255, 0.1);
- --bg-overlay-dark: rgba(0, 0, 0, 0.2);
- --border-light: rgba(255, 255, 255, 0.2);
- --shadow-color: rgba(0, 0, 0, 0.1);
- }
-
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
- min-height: 100vh;
- /* NOTE: 美食主题渐变背景色 */
- background: linear-gradient(135deg,
- #ff6b6b 0%,
- #ff9a3c 25%,
- #f6d365 50%,
- #84fab0 75%,
- #8fd3f4 100%);
- background-attachment: fixed;
- padding: 1rem;
- color: white;
- position: relative;
- overflow-x: hidden;
- }
-
- /* 背景装饰元素 */
- body::before {
- content: '';
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background:
- radial-gradient(circle at 20% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
- radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
- radial-gradient(circle at 40% 40%, rgba(255, 107, 107, 0.15) 0%, transparent 50%),
- radial-gradient(circle at 60% 60%, rgba(255, 154, 60, 0.15) 0%, transparent 50%);
- z-index: -1;
- }
-
- .container {
- max-width: 1200px;
- margin: 0 auto;
- position: relative;
- }
-
- .header {
- text-align: center;
- margin-bottom: 2rem;
- position: relative;
- }
-
- .header::after {
- content: '';
- position: absolute;
- bottom: -10px;
- left: 50%;
- transform: translateX(-50%);
- width: 150px;
- height: 4px;
- background: linear-gradient(90deg, transparent, var(--secondary-color), transparent);
- border-radius: 2px;
- }
-
- .header h1 {
- /* OPTIMIZE: clamp()实现平滑的字体响应式变化 */
- font-size: clamp(1.8rem, 6vw, 3rem);
- font-weight: 900;
- color: #fff;
- margin: 0.5rem 0;
- text-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- letter-spacing: 1px;
- background: linear-gradient(135deg, #fff 0%, #fcd34d 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- }
-
- .header p {
- color: var(--text-light);
- font-size: 1.2rem;
- font-weight: 500;
- margin-top: 0.5rem;
- max-width: 600px;
- margin-left: auto;
- margin-right: auto;
- line-height: 1.6;
- }
-
- .header .emoji-bounce {
- display: inline-block;
- animation: bounce 2s infinite;
- filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
- }
-
- .subtitle {
- font-size: 1rem;
- color: var(--text-lighter);
- margin-top: 0.5rem;
- font-style: italic;
- }
-
- .main-grid {
- display: grid;
- /* NOTE: 两栏布局,左侧固定宽度,右侧自适应 */
- grid-template-columns: 350px 1fr;
- gap: 2rem;
- align-items: start;
- }
-
- /* WARN: 移动端适配 - 900px以下变为单栏布局 */
- @media (max-width: 900px) {
- .main-grid {
- grid-template-columns: 1fr;
- }
- .list-panel { order: 2; }
- .wheel-panel { order: 1; }
- }
-
- .panel {
- /* NOTE: 毛玻璃效果面板 */
- background: var(--bg-overlay);
- backdrop-filter: blur(20px) saturate(180%);
- border-radius: 1.5rem;
- padding: 1.8rem;
- border: 1px solid var(--border-light);
- box-shadow:
- 0 8px 32px var(--shadow-color),
- inset 0 1px 0 rgba(255, 255, 255, 0.2);
- transition: transform 0.3s ease, box-shadow 0.3s ease;
- position: relative;
- overflow: hidden;
- }
-
- .panel::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 4px;
- background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), var(--accent-color));
- border-radius: 1.5rem 1.5rem 0 0;
- }
-
- .panel:hover {
- transform: translateY(-5px);
- box-shadow:
- 0 12px 40px rgba(0, 0, 0, 0.15),
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
- }
-
- .panel-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 0.5rem;
- margin-bottom: 1.8rem;
- padding-bottom: 1.2rem;
- border-bottom: 1px solid var(--border-light);
- }
-
- .panel-header h2 {
- font-size: 1.6rem;
- font-weight: 800;
- color: #fff;
- display: flex;
- align-items: center;
- gap: 0.75rem;
- }
-
- .icon {
- color: #fcd34d;
- font-size: 2rem;
- filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
- }
-
- .count-badge {
- font-size: 0.9rem;
- font-weight: 700;
- color: #fff;
- background: linear-gradient(135deg, var(--success-color), #0d9668);
- padding: 0.4rem 0.8rem;
- border-radius: 2rem;
- box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
- }
-
- .input-group {
- margin-bottom: 1.5rem;
- }
-
- .input-label {
- color: var(--text-light);
- margin-bottom: 0.75rem;
- display: block;
- font-weight: 600;
- font-size: 1.1rem;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
-
- .input-label .label-icon {
- font-size: 1.2rem;
- }
-
- .input-row {
- display: flex;
- gap: 0.75rem;
- }
-
- input, textarea {
- background: rgba(255, 255, 255, 0.15);
- border: 2px solid var(--border-light);
- border-radius: 0.75rem;
- padding: 1rem 1.25rem;
- color: white;
- outline: none;
- transition: all 0.3s;
- width: 100%;
- font-size: 1rem;
- font-weight: 500;
- }
-
- input::placeholder, textarea::placeholder {
- color: rgba(255, 255, 255, 0.5);
- font-weight: 400;
- }
-
- input:focus, textarea:focus {
- border-color: #fcd34d;
- background: rgba(255, 255, 255, 0.25);
- box-shadow: 0 0 0 4px rgba(252, 211, 77, 0.2);
- }
-
- .input-row input {
- flex: 1;
- }
-
- .btn {
- padding: 1rem 1.5rem;
- border: none;
- border-radius: 0.75rem;
- font-weight: 700;
- cursor: pointer;
- transition: all 0.3s;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 0.75rem;
- text-decoration: none;
- user-select: none;
- white-space: nowrap;
- font-size: 1rem;
- letter-spacing: 0.5px;
- position: relative;
- overflow: hidden;
- }
-
- .btn::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
- transition: left 0.5s;
- }
-
- .btn:hover::before {
- left: 100%;
- }
-
- .btn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none !important;
- }
-
- .btn-primary {
- background: linear-gradient(135deg, var(--primary-color), #ea580c);
- color: white;
- box-shadow: 0 6px 16px rgba(249, 115, 22, 0.4);
- }
-
- .btn-primary:hover:not(:disabled) {
- background: linear-gradient(135deg, #ea580c, #c2410c);
- transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(249, 115, 22, 0.5);
- }
-
- .btn-success {
- background: linear-gradient(135deg, var(--success-color), #0d9668);
- color: white;
- box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
- }
-
- .btn-success:hover:not(:disabled) {
- background: linear-gradient(135deg, #0d9668, #047857);
- transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(16, 185, 129, 0.5);
- }
-
- .btn-secondary {
- background: rgba(255, 255, 255, 0.2);
- color: white;
- border: 2px solid var(--border-light);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
-
- .btn-secondary:hover:not(:disabled) {
- background: rgba(255, 255, 255, 0.3);
- transform: translateY(-2px);
- box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
- }
-
- .btn-danger {
- background: linear-gradient(135deg, var(--danger-color), #b91c1c);
- color: white;
- box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
- }
-
- .btn-danger:hover:not(:disabled) {
- background: linear-gradient(135deg, #b91c1c, #991b1b);
- transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(239, 68, 68, 0.5);
- }
-
- .btn-warning {
- background: linear-gradient(135deg, var(--warning-color), #d97706);
- color: white;
- box-shadow: 0 6px 16px rgba(245, 158, 11, 0.4);
- }
-
- .btn-warning:hover:not(:disabled) {
- background: linear-gradient(135deg, #d97706, #b45309);
- transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(245, 158, 11, 0.5);
- }
-
- .punishment-list {
- max-height: 400px;
- overflow-y: auto;
- padding-right: 5px;
- margin-bottom: 1.5rem;
- }
-
- .punishment-list::-webkit-scrollbar {
- width: 8px;
- }
- .punishment-list::-webkit-scrollbar-track {
- background: rgba(255, 255, 255, 0.05);
- border-radius: 4px;
- }
- .punishment-list::-webkit-scrollbar-thumb {
- background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
- border-radius: 4px;
- }
-
- .punishment-item {
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
- padding: 1.25rem;
- border-radius: 1rem;
- margin-bottom: 0.75rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
- transition: all 0.3s;
- color: white;
- border-left: 4px solid transparent;
- border: 1px solid var(--border-light);
- position: relative;
- overflow: hidden;
- }
-
- .punishment-item::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- width: 4px;
- height: 100%;
- background: linear-gradient(180deg, var(--primary-color), var(--secondary-color));
- opacity: 0;
- transition: opacity 0.3s;
- }
-
- .punishment-item:hover {
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.1));
- transform: translateX(8px);
- box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
- }
-
- .punishment-item:hover::before {
- opacity: 1;
- }
-
- .punishment-item.active {
- background: linear-gradient(135deg, rgba(252, 211, 77, 0.25), rgba(245, 158, 11, 0.15));
- border-left-color: #fcd34d;
- font-weight: bold;
- box-shadow: 0 4px 12px rgba(252, 211, 77, 0.2);
- }
-
- .punishment-item .food-icon {
- font-size: 1.5rem;
- margin-right: 0.75rem;
- }
-
- .punishment-item .remove-btn {
- background: rgba(0, 0, 0, 0.2);
- border: none;
- color: #fca5a5;
- cursor: pointer;
- font-size: 1.3rem;
- width: 32px;
- height: 32px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- flex-shrink: 0;
- }
-
- .punishment-item .remove-btn:hover {
- background: #ef4444;
- color: white;
- transform: rotate(90deg);
- }
-
- .wheel-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- width: 100%;
- }
-
- .wheel-wrapper {
- position: relative;
- margin-bottom: 2.5rem;
- filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.3));
- width: 100%;
- /* OPTIMIZE: 使用aspect-ratio保持正方形 */
- max-width: 500px;
- aspect-ratio: 1/1;
- }
-
- .wheel-pointer {
- position: absolute;
- top: -15px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 20;
- width: 50px;
- height: 50px;
- background: linear-gradient(135deg, var(--danger-color), #b91c1c);
- border: 5px solid #fff;
- border-radius: 50%;
- box-shadow: 0 8px 20px rgba(0,0,0,0.4);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.5rem;
- animation: pulsePointer 2s infinite;
- }
-
- .wheel-pointer::after {
- content: '👇';
- position: absolute;
- top: 100%;
- left: 50%;
- transform: translateX(-50%);
- font-size: 1.8rem;
- filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));
- }
-
- .wheel-svg {
- width: 100%;
- height: 100%;
- transition-property: transform;
- /* NOTE: 自定义贝塞尔曲线实现"缓慢停止"效果 */
- transition-timing-function: cubic-bezier(0.15, 0, 0.20, 1);
- }
-
- .control-buttons {
- display: flex;
- gap: 1.5rem;
- margin-bottom: 2.5rem;
- flex-wrap: wrap;
- justify-content: center;
- }
-
- .result-display {
- background: linear-gradient(135deg, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.5));
- backdrop-filter: blur(20px);
- border: 3px solid #fcd34d;
- color: #fcd34d;
- padding: 2rem 3rem;
- border-radius: 1.5rem;
- text-align: center;
- font-weight: bold;
- box-shadow:
- 0 15px 35px rgba(0, 0, 0, 0.3),
- inset 0 1px 0 rgba(255, 255, 255, 0.2);
- min-width: 320px;
- transform: scale(0.9);
- opacity: 0;
- transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
- position: relative;
- overflow: hidden;
- }
-
- .result-display::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 4px;
- background: linear-gradient(90deg, var(--primary-color), #fcd34d, var(--secondary-color));
- }
-
- .result-display.show {
- transform: scale(1);
- opacity: 1;
- }
-
- .result-display h3 {
- font-size: 1.6rem;
- margin-bottom: 1rem;
- color: white;
- text-shadow: 0 2px 4px rgba(0,0,0,0.3);
- }
-
- .result-display .punishment-text {
- font-size: 2rem;
- font-weight: 900;
- word-break: break-word;
- line-height: 1.4;
- background: linear-gradient(135deg, #fcd34d, #f59e0b);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
-
- .result-emoji {
- font-size: 3rem;
- margin-bottom: 1rem;
- display: block;
- filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
- }
-
- .celebration-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.85);
- backdrop-filter: blur(10px);
- z-index: 50;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 1rem;
- }
-
- .celebration-content {
- background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
- border-radius: 2rem;
- padding: 3rem;
- max-width: 36rem;
- width: 100%;
- text-align: center;
- position: relative;
- overflow: hidden;
- box-shadow:
- 0 30px 60px rgba(0, 0, 0, 0.5),
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
- border: 4px solid rgba(255,255,255,0.2);
- }
-
- .celebration-bg {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.2) 0%, transparent 50%),
- radial-gradient(circle at 70% 70%, rgba(255,255,255,0.15) 0%, transparent 50%);
- animation: pulseBg 3s infinite;
- }
-
- .celebration-close {
- position: absolute;
- top: 1.5rem;
- right: 1.5rem;
- width: 3rem;
- height: 3rem;
- background: rgba(0, 0, 0, 0.3);
- border: 2px solid rgba(255, 255, 255, 0.3);
- border-radius: 50%;
- color: white;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.8rem;
- transition: all 0.3s;
- font-weight: bold;
- }
-
- .celebration-close:hover {
- background: rgba(255, 255, 255, 0.2);
- transform: rotate(90deg);
- }
-
- .celebration-inner {
- position: relative;
- z-index: 10;
- }
-
- .celebration-emoji {
- font-size: 6rem;
- margin-bottom: 1rem;
- display: block;
- filter: drop-shadow(0 6px 12px rgba(0,0,0,0.4));
- animation: bounce 2s infinite;
- }
-
- .celebration-title {
- font-size: 2.8rem;
- font-weight: 900;
- color: #fff;
- margin-bottom: 1.5rem;
- text-transform: uppercase;
- letter-spacing: 2px;
- text-shadow: 0 4px 8px rgba(0,0,0,0.3);
- }
-
- .celebration-result-box {
- background: rgba(0, 0, 0, 0.3);
- backdrop-filter: blur(10px);
- border-radius: 1.5rem;
- padding: 2.5rem;
- margin-bottom: 2.5rem;
- border: 2px dashed rgba(255,255,255,0.3);
- }
-
- .celebration-result-label {
- font-size: 1.4rem;
- color: rgba(255,255,255,0.9);
- margin-bottom: 1rem;
- font-weight: 600;
- }
-
- .celebration-result-text {
- font-size: 2.4rem;
- font-weight: 900;
- color: #fcd34d;
- line-height: 1.3;
- text-shadow: 0 3px 6px rgba(0,0,0,0.3);
- }
-
- .celebration-btn {
- background: white;
- color: var(--secondary-color);
- border: none;
- padding: 1.25rem 4rem;
- border-radius: 3rem;
- font-weight: 900;
- font-size: 1.4rem;
- cursor: pointer;
- transition: all 0.3s;
- box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
- position: relative;
- overflow: hidden;
- }
-
- .celebration-btn:hover {
- background: #f3f4f6;
- transform: translateY(-3px) scale(1.05);
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
- }
-
- .empty-state {
- padding: 2.5rem;
- border-radius: 1.5rem;
- background: rgba(255, 255, 255, 0.05);
- color: rgba(255, 255, 255, 0.6);
- text-align: center;
- font-style: italic;
- border: 2px dashed rgba(255, 255, 255, 0.2);
- font-size: 1.1rem;
- }
-
- .file-import-note {
- color: var(--text-lighter);
- font-size: 0.9rem;
- 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-light);
- }
-
- /* NOTE: 一键清空确认弹窗样式 */
- .confirm-clear-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.85);
- backdrop-filter: blur(10px);
- z-index: 100;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 1rem;
- }
-
- .confirm-clear-content {
- background: linear-gradient(135deg, var(--danger-color), #b91c1c);
- border-radius: 2rem;
- padding: 3rem;
- max-width: 32rem;
- width: 100%;
- text-align: center;
- position: relative;
- overflow: hidden;
- box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5);
- border: 4px solid rgba(255,255,255,0.2);
- }
-
- .confirm-clear-icon {
- font-size: 4.5rem;
- margin-bottom: 1.5rem;
- display: block;
- filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
- animation: shake 0.5s ease-in-out;
- }
-
- .confirm-clear-title {
- font-size: 2rem;
- font-weight: 900;
- color: #fff;
- margin-bottom: 1.5rem;
- text-shadow: 0 3px 6px rgba(0,0,0,0.3);
- }
-
- .confirm-clear-message {
- font-size: 1.2rem;
- color: rgba(255, 255, 255, 0.9);
- margin-bottom: 2.5rem;
- line-height: 1.6;
- }
-
- .confirm-clear-count {
- font-weight: 900;
- color: #fcd34d;
- font-size: 1.5rem;
- text-shadow: 0 2px 4px rgba(0,0,0,0.3);
- }
-
- .confirm-clear-buttons {
- display: flex;
- gap: 1.5rem;
- justify-content: center;
- }
-
- .confirm-clear-btn {
- padding: 1.2rem 2.5rem;
- border: none;
- border-radius: 1rem;
- font-weight: 800;
- font-size: 1.2rem;
- cursor: pointer;
- transition: all 0.3s;
- min-width: 140px;
- position: relative;
- overflow: hidden;
- }
-
- .confirm-clear-btn.confirm {
- background: #10b981;
- color: white;
- box-shadow: 0 8px 20px rgba(16, 185, 129, 0.4);
- }
-
- .confirm-clear-btn.confirm:hover {
- background: #0d9668;
- transform: translateY(-3px);
- box-shadow: 0 12px 25px rgba(16, 185, 129, 0.5);
- }
-
- .confirm-clear-btn.cancel {
- background: rgba(255, 255, 255, 0.2);
- color: white;
- border: 2px solid rgba(255, 255, 255, 0.3);
- box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
- }
-
- .confirm-clear-btn.cancel:hover {
- background: rgba(255, 255, 255, 0.3);
- transform: translateY(-3px);
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
- }
-
- @keyframes pulse {
- 0%, 100% { transform: scale(1); }
- 50% { transform: scale(1.05); }
- }
-
- @keyframes pulsePointer {
- 0%, 100% { transform: translateX(-50%) scale(1); }
- 50% { transform: translateX(-50%) scale(1.1); }
- }
-
- @keyframes pulseBg {
- 0%, 100% { opacity: 0.5; }
- 50% { opacity: 1; }
- }
-
- @keyframes bounce {
- 0%, 20%, 53%, 80%, 100% { transform: translateY(0); }
- 40%, 43% { transform: translateY(-20px); }
- 70% { transform: translateY(-10px); }
- 90% { transform: translateY(-5px); }
- }
-
- @keyframes modalPop {
- 0% { transform: scale(0.8) translateY(50px); opacity: 0; }
- 70% { transform: scale(1.05); }
- 100% { transform: scale(1) translateY(0); opacity: 1; }
- }
-
- @keyframes shake {
- 0%, 100% { transform: translateX(0); }
- 10%, 30%, 50%, 70%, 90% { transform: translateX(-8px); }
- 20%, 40%, 60%, 80% { transform: translateX(8px); }
- }
-
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
-
- @keyframes float {
- 0%, 100% { transform: translateY(0); }
- 50% { transform: translateY(-20px); }
- }
-
- .hidden {
- display: none;
- }
-
- .modal-enter {
- animation: modalPop 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
- }
-
- .shake {
- animation: shake 0.5s ease-in-out;
- }
-
- /* TODO: 添加暗色模式支持 */
- /* OPTIMIZE: 将动画关键帧提取到单独部分以便维护 */
-
- /* 美食图标样式 */
- .food-emoji {
- display: inline-block;
- animation: float 3s ease-in-out infinite;
- }
-
- /* 响应式调整 */
- @media (max-width: 768px) {
- .panel {
- padding: 1.5rem;
- }
-
- .header h1 {
- font-size: clamp(1.5rem, 5vw, 2.5rem);
- }
-
- .header p {
- font-size: 1rem;
- }
-
- .control-buttons {
- flex-direction: column;
- align-items: center;
- }
-
- .btn {
- width: 100%;
- max-width: 300px;
- }
-
- .celebration-content {
- padding: 2rem;
- }
-
- .celebration-title {
- font-size: 2rem;
- }
-
- .celebration-result-text {
- font-size: 1.8rem;
- }
- }
- </style>
- </head>
- <body>
- <div class="container">
- <!-- 头部标题 -->
- <div class="header">
- <div style="display: flex; align-items: center; justify-content: center; gap: 1.5rem; margin-bottom: 1rem;">
- <span class="emoji-bounce food-emoji">🍕</span>
- <h1>美食大转盘 - 点餐助手</h1>
- <span class="emoji-bounce food-emoji">🍔</span>
- </div>
- <p>还在为"吃什么"发愁吗?让转盘帮你决定今天的美食!</p>
- <div class="subtitle">添加你喜欢的菜品,让选择变得简单有趣</div>
- </div>
-
- <div class="main-grid">
- <!--
- ============================================
- 菜品项目库面板
- 给使用者:
- - 在此添加/删除菜品项目
- - 支持导入txt文件批量添加
- - 项目列表会保存在浏览器本地
- ============================================
- -->
- <div class="panel list-panel">
- <div class="panel-header">
- <div style="display: flex; align-items: center; gap: 0.75rem;">
- <span class="icon">🍽️</span>
- <h2>我的美食菜单</h2>
- <div class="count-badge" id="punishmentCount">0项</div>
- </div>
- <button class="btn btn-danger" id="clearAllPunishmentsBtn" style="padding: 0.8rem 1.2rem;">
- <span>🗑️</span>
- 一键清空
- </button>
- </div>
-
- <div class="input-group">
- <label class="input-label">
- <span class="label-icon">➕</span>
- 添加新菜品:
- </label>
- <div class="input-row">
- <input type="text" id="punishmentInput" placeholder="例如:麻辣香锅、披萨、寿司..." autocomplete="off">
- <button class="btn btn-success" id="addPunishmentBtn">
- <span>🍴</span>
- 添加
- </button>
- </div>
- </div>
-
- <!-- NOTE: 文件导入功能 -->
- <div class="input-group">
- <label class="input-label">
- <span class="label-icon">📁</span>
- 导入菜品列表(TXT文件):
- </label>
- <div class="input-row">
- <input type="file" id="importFile" accept=".txt" style="flex: 1; padding: 0.75rem;">
- <button class="btn btn-secondary" id="importBtn">
- <span>📥</span>
- 导入
- </button>
- </div>
- <div class="file-import-note">
- <span>📝</span>
- 每行一个菜品,支持批量导入
- </div>
- </div>
-
- <div class="punishment-list" id="punishmentList">
- <!-- 菜品项目动态加载 -->
- </div>
-
- <div class="clear-all-section">
- <button class="btn btn-warning" id="resetToDefaultBtn" style="padding: 0.8rem 1.5rem;">
- <span>🔄</span>
- 恢复默认菜单
- </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="#fcd34d" />
- <stop offset="100%" stop-color="#f59e0b" />
- </radialGradient>
- <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
- <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="rgba(0,0,0,0.4)"/>
- </filter>
- <linearGradient id="wheelGradient" x1="0%" y1="0%" x2="100%" y2="100%">
- <stop offset="0%" style="stop-color:#ff6b6b;stop-opacity:1" />
- <stop offset="25%" style="stop-color:#ff9a3c;stop-opacity:1" />
- <stop offset="50%" style="stop-color:#f6d365;stop-opacity:1" />
- <stop offset="75%" style="stop-color:#84fab0;stop-opacity:1" />
- <stop offset="100%" style="stop-color:#8fd3f4;stop-opacity:1" />
- </linearGradient>
- </defs>
- <!-- 转盘扇形区域(动态生成) -->
- <g id="wheelSections"></g>
- <!-- 转盘中心装饰 -->
- <circle cx="0" cy="0" r="50" fill="url(#centerGradient)" stroke="#fff" stroke-width="6" filter="url(#shadow)"/>
- <circle cx="0" cy="0" r="35" fill="none" stroke="#fff" stroke-width="3" stroke-dasharray="4 4"/>
- <text x="0" y="5" fill="#fff" 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: 180px; font-size: 1.3rem; padding: 1.2rem 2rem;">
- <span>🎯</span>
- <span id="startBtnText">开始点餐</span>
- </button>
- <button class="btn btn-secondary" id="resetBtn" style="padding: 1.2rem 2rem;">
- <span>🔄</span>
- 重置转盘
- </button>
- </div>
-
- <!-- 结果展示区域 -->
- <div id="resultDisplay" class="result-display">
- <div class="result-emoji">🤔</div>
- <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">
- <div class="celebration-bg"></div>
- <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">
- <span>😋</span>
- 太棒了,就吃这个!
- </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>
- <strong>此操作无法撤销!</strong> 所有自定义和导入的菜品将被永久删除。
- </div>
- <div class="confirm-clear-buttons">
- <button class="confirm-clear-btn cancel" id="cancelClearBtn">
- <span>❌</span>
- 取消
- </button>
- <button class="confirm-clear-btn confirm" id="confirmClearBtn">
- <span>✅</span>
- 确定清空
- </button>
- </div>
- </div>
- </div>
-
- <script>
- /*
- ============================================
- 美食大转盘 - JavaScript核心逻辑
- 给维护者:
- - 使用LocalStorage持久化数据
- - 转盘算法基于角度计算
- - 事件委托处理动态列表
- 给学习者:
- - 使用SVG动态生成转盘
- - 三角函数计算扇形区域
- - 贝塞尔曲线实现缓动效果
- ============================================
- */
-
- // ========== 全局变量声明 ==========
-
- // NOTE: 默认菜品列表 - 当本地存储为空时使用
- const defaultPunishments = [
- '🍕 披萨',
- '🍔 汉堡',
- '🍜 拉面',
- '🍣 寿司',
- '🥘 麻辣香锅',
- '🍲 火锅',
- '🥗 沙拉',
- '🍛 咖喱饭',
- '🍱 便当',
- '🥪 三明治',
- '🍝 意大利面',
- '🌮 墨西哥卷饼',
- '🥟 饺子',
- '🍤 炸虾',
- '🍦 冰淇淋'
- ];
-
- // WARN: 从LocalStorage加载数据,失败时使用默认值
- let punishments = JSON.parse(localStorage.getItem('wheelPunishments')) || [...defaultPunishments];
-
- // 转盘状态控制变量
- let isSpinning = false; // 是否正在旋转
- let currentRotation = 0; // 当前旋转角度
- let currentPunishment = ''; // 当前选中的菜品
-
- // NOTE: 转盘扇形颜色列表 - 美食主题渐变色
- const colors = [
- '#FF6B6B', '#FF9A3C', '#F6D365', '#84FAB0', '#8FD3F4',
- '#A78BFA', '#F472B6', '#60A5FA', '#34D399', '#FBBF24',
- '#EF4444', '#F59E0B', '#10B981', '#3B82F6', '#8B5CF6'
- ];
-
- // ========== 工具函数 ==========
-
- /**
- * 更新菜品项目计数显示
- */
- function updatePunishmentCount() {
- const countElement = document.getElementById('punishmentCount');
- const count = punishments.length;
- countElement.textContent = `${count}项`;
-
- // 根据数量调整颜色
- if (count === 0) {
- countElement.style.background = 'linear-gradient(135deg, #ef4444, #b91c1c)';
- } else if (count <= 5) {
- countElement.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)';
- } else if (count <= 10) {
- countElement.style.background = 'linear-gradient(135deg, #10b981, #0d9668)';
- } else {
- countElement.style.background = 'linear-gradient(135deg, #8b5cf6, #7c3aed)';
- }
- }
-
- /**
- * 保存菜品列表到LocalStorage
- * @description 持久化数据,页面刷新后不丢失
- */
- function savePunishments() {
- localStorage.setItem('wheelPunishments', JSON.stringify(punishments));
- }
-
- /**
- * 格式化日期时间
- * @Param {Date} date - 日期对象
- * @returns {string} 格式化后的日期字符串
- */
- function formatDateTime(date) {
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
- }
-
- /**
- * 截断文本,添加省略号
- * @param {string} text - 原始文本
- * @param {number} maxLength - 最大长度
- * @returns {string} 截断后的文本
- */
- function truncateText(text, maxLength = 100) {
- if (text.length <= maxLength) return text;
- return text.substring(0, maxLength) + '...';
- }
-
- /**
- * 显示一键清空确认弹窗
- */
- 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');
-
- // 添加抖动效果以强调危险操作
- const content = modal.querySelector('.confirm-clear-content');
- content.classList.remove('shake');
- void content.offsetWidth; // 触发重排
- content.classList.add('shake');
- }
-
- /**
- * 隐藏一键清空确认弹窗
- */
- function hideClearConfirmation() {
- document.getElementById('confirmClearModal').classList.add('hidden');
- }
-
- /**
- * 执行一键清空操作
- * @description 清空所有菜品项目
- */
- 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文件并解析菜品列表
- * @description 每行一个菜品,自动过滤空行和重复项
- * @param {File} file - 用户选择的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);
- }
-
- // ========== 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('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);
- }
- }
- });
- });
-
- // ========== 核心功能函数 ==========
-
- /**
- * 添加新菜品项目
- * @description 验证输入并添加到列表,过滤重复项
- */
- 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('该菜品已存在!');
- }
- }
-
- /**
- * 删除菜品项目
- * @param {number} index - 要删除的项目索引
- * @description 转盘旋转时禁止删除
- */
- function removePunishment(index) {
- if (isSpinning) {
- alert('转盘旋转时不能删除菜品!');
- return;
- }
-
- if (confirm('确定要删除这个菜品吗?')) {
- punishments.splice(index, 1);
- updatePunishmentList();
- updatePunishmentCount();
- updateWheel();
- savePunishments();
- }
- }
-
- /**
- * 更新菜品列表UI
- * @description 动态生成列表项,处理空状态
- */
- 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) => {
- // 提取emoji和文本
- const emojiMatch = punishment.match(/^([\u{1F300}-\u{1F9FF}]|\u{1F1E6}-\u{1F1FF}|\u{2600}-\u{26FF}|\u{2700}-\u{27BF})/u);
- const emoji = emojiMatch ? emojiMatch[0] : '🍽️';
- const text = emojiMatch ? punishment.substring(emojiMatch[0].length).trim() : punishment;
-
- return `
- <div class="punishment-item ${currentPunishment === punishment ? 'active' : ''}">
- <div style="display: flex; align-items: center;">
- <span class="food-emoji" style="font-size: 1.8rem; margin-right: 0.75rem;">${emoji}</span>
- <span>${text}</span>
- </div>
- <button class="remove-btn" title="删除此项">×</button>
- </div>
- `;
- }).join('');
- }
-
- /**
- * 更新转盘SVG图形
- * @description 动态计算扇形区域,处理文本显示
- * @算法说明:
- * 1. 计算每个扇形的角度
- * 2. 使用极坐标计算弧线路径
- * 3. 根据项目数量调整字体大小
- */
- 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', 'rgba(255,255,255,0.2)');
- path.setAttribute('stroke', '#fff');
- path.setAttribute('stroke-width', '4');
-
- const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
- text.setAttribute('x', '0');
- text.setAttribute('y', '0');
- text.setAttribute('fill', '#fff');
- text.setAttribute('font-size', '32');
- text.setAttribute('font-weight', 'bold');
- 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;
-
- // 判断是否为大圆弧(角度大于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', '4');
- 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 = 22;
- if (punishments.length > 15) fontSize = 14;
- else if (punishments.length > 10) fontSize = 16;
- else if (punishments.length > 8) fontSize = 18;
-
- // 提取emoji和文本
- const emojiMatch = punishment.match(/^([\u{1F300}-\u{1F9FF}]|\u{1F1E6}-\u{1F1FF}|\u{2600}-\u{26FF}|\u{2700}-\u{27BF})/u);
- const emoji = emojiMatch ? emojiMatch[0] : '';
- const text = emojiMatch ? punishment.substring(emojiMatch[0].length).trim() : punishment;
-
- // 创建文本元素
- const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
- textElement.setAttribute('x', textX);
- textElement.setAttribute('y', textY);
- textElement.setAttribute('fill', '#fff');
- textElement.setAttribute('font-size', fontSize);
- textElement.setAttribute('font-weight', 'bold');
- textElement.setAttribute('text-anchor', 'middle');
- textElement.setAttribute('dominant-baseline', 'middle');
- textElement.setAttribute('transform', `rotate(${midAngle}, ${textX}, ${textY})`);
-
- // 文本截断处理(长文本显示省略号)
- const maxChars = 6;
- let displayText = text;
- if (text.length > maxChars) {
- displayText = text.substring(0, maxChars) + '..';
- }
-
- // 如果有emoji,添加到文本前面
- const displayContent = emoji ? `${emoji} ${displayText}` : displayText;
- textElement.textContent = displayContent;
-
- // 添加到SVG
- sectionsGroup.appendChild(path);
- sectionsGroup.appendChild(textElement);
- });
- }
-
- /**
- * 开始转盘游戏
- * @description 控制转盘旋转并计算最终结果
- * @算法说明:
- * 1. 生成随机旋转角度和时间
- * 2. 使用CSS transition实现动画
- * 3. 根据指针角度计算选中扇形
- */
- 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');
-
- // 生成随机动画参数
- const randomDuration = 4000 + Math.random() * 4000; // 4-8秒
-
- // 计算最终旋转角度(多圈旋转 + 随机偏移)
- const randomDegreeOffset = Math.floor(Math.random() * 360);
- const randomSpins = 5 + Math.floor(Math.random() * 5); // 5-10圈
- 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;
-
- // 转盘指针固定角度(正上方为270度)
- const POINTER_ANGLE = 270;
-
- // 计算实际旋转角度(归一化到0-360度)
- 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 = '再次点餐';
-
- // 显示结果
- const resultEmoji = document.querySelector('#resultDisplay .result-emoji');
- resultEmoji.textContent = '🎉';
- document.getElementById('winnerPunishment').textContent = currentPunishment;
- resultDisplay.classList.add('show');
-
- // 更新列表高亮
- updatePunishmentList();
-
- // 显示庆祝弹窗
- showCelebration();
- }, randomDuration);
- }
-
- /**
- * 重置转盘
- * @description 将转盘归零,清除结果
- */
- 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');
- const resultEmoji = document.querySelector('#resultDisplay .result-emoji');
- resultEmoji.textContent = '🤔';
- resultDisplay.classList.remove('show');
-
- // 更新UI
- updatePunishmentList();
- closeCelebration();
- }
-
- /**
- * 显示庆祝弹窗
- * @description 展示选中的菜品,带有动画效果
- */
- 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');
- }
-
- // TODO: 添加导出菜品列表功能
- // TODO: 添加转盘音效支持
- // TODO: 添加点餐历史记录功能
- // OPTIMIZE: 考虑使用Vue/React重构以提高可维护性
- </script>
- </body>
- </html>
复制代码
|