返回列表 发布新帖

【HTML】美食大转盘 - 点餐助手

1236 0
小K网牛逼 发表于 7 小时前 | 查看全部 阅读模式 <

马上注册,结交更多好友,享用更多功能,让你轻松玩转小K网。

您需要 登录 才可以下载或查看,没有账号?立即注册 微信登录

×

【HTML】美食大转盘 - 点餐助手

【HTML】美食大转盘 - 点餐助手


增加导入txt文本功能,增加一键清空功能,将所有代码进行注释,方便大家修改,瞎弄的,博大家一笑!!!
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>美食大转盘 - 点餐助手</title>
  7.     <style>
  8.         /*
  9.         ============================================
  10.         样式表说明
  11.         ============================================
  12.         给维护者:
  13.         - 使用CSS变量确保颜色一致性
  14.         - 采用响应式设计,移动端优先
  15.         - 使用backdrop-filter实现毛玻璃效果
  16.         给学习者:
  17.         - 使用clamp()实现响应式字体
  18.         - aspect-ratio保持元素比例
  19.         - CSS Grid创建两栏布局
  20.         ============================================
  21.         */
  22.          
  23.         :root {
  24.             --primary-color: #f97316;
  25.             --secondary-color: #db2777;
  26.             --accent-color: #7c3aed;
  27.             --success-color: #10b981;
  28.             --warning-color: #f59e0b;
  29.             --danger-color: #ef4444;
  30.             --text-light: rgba(255, 255, 255, 0.9);
  31.             --text-lighter: rgba(255, 255, 255, 0.7);
  32.             --bg-overlay: rgba(255, 255, 255, 0.1);
  33.             --bg-overlay-dark: rgba(0, 0, 0, 0.2);
  34.             --border-light: rgba(255, 255, 255, 0.2);
  35.             --shadow-color: rgba(0, 0, 0, 0.1);
  36.         }
  37.          
  38.         * {
  39.             margin: 0;
  40.             padding: 0;
  41.             box-sizing: border-box;
  42.         }
  43.    
  44.         body {
  45.             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  46.             min-height: 100vh;
  47.             /* NOTE: 美食主题渐变背景色 */
  48.             background: linear-gradient(135deg,
  49.                 #ff6b6b 0%,
  50.                 #ff9a3c 25%,
  51.                 #f6d365 50%,
  52.                 #84fab0 75%,
  53.                 #8fd3f4 100%);
  54.             background-attachment: fixed;
  55.             padding: 1rem;
  56.             color: white;
  57.             position: relative;
  58.             overflow-x: hidden;
  59.         }
  60.          
  61.         /* 背景装饰元素 */
  62.         body::before {
  63.             content: '';
  64.             position: fixed;
  65.             top: 0;
  66.             left: 0;
  67.             right: 0;
  68.             bottom: 0;
  69.             background:
  70.                 radial-gradient(circle at 20% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
  71.                 radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
  72.                 radial-gradient(circle at 40% 40%, rgba(255, 107, 107, 0.15) 0%, transparent 50%),
  73.                 radial-gradient(circle at 60% 60%, rgba(255, 154, 60, 0.15) 0%, transparent 50%);
  74.             z-index: -1;
  75.         }
  76.    
  77.         .container {
  78.             max-width: 1200px;
  79.             margin: 0 auto;
  80.             position: relative;
  81.         }
  82.    
  83.         .header {
  84.             text-align: center;
  85.             margin-bottom: 2rem;
  86.             position: relative;
  87.         }
  88.          
  89.         .header::after {
  90.             content: '';
  91.             position: absolute;
  92.             bottom: -10px;
  93.             left: 50%;
  94.             transform: translateX(-50%);
  95.             width: 150px;
  96.             height: 4px;
  97.             background: linear-gradient(90deg, transparent, var(--secondary-color), transparent);
  98.             border-radius: 2px;
  99.         }
  100.    
  101.         .header h1 {
  102.             /* OPTIMIZE: clamp()实现平滑的字体响应式变化 */
  103.             font-size: clamp(1.8rem, 6vw, 3rem);
  104.             font-weight: 900;
  105.             color: #fff;
  106.             margin: 0.5rem 0;
  107.             text-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  108.             letter-spacing: 1px;
  109.             background: linear-gradient(135deg, #fff 0%, #fcd34d 100%);
  110.             -webkit-background-clip: text;
  111.             -webkit-text-fill-color: transparent;
  112.             background-clip: text;
  113.         }
  114.    
  115.         .header p {
  116.             color: var(--text-light);
  117.             font-size: 1.2rem;
  118.             font-weight: 500;
  119.             margin-top: 0.5rem;
  120.             max-width: 600px;
  121.             margin-left: auto;
  122.             margin-right: auto;
  123.             line-height: 1.6;
  124.         }
  125.   
  126.         .header .emoji-bounce {
  127.             display: inline-block;
  128.             animation: bounce 2s infinite;
  129.             filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
  130.         }
  131.          
  132.         .subtitle {
  133.             font-size: 1rem;
  134.             color: var(--text-lighter);
  135.             margin-top: 0.5rem;
  136.             font-style: italic;
  137.         }
  138.    
  139.         .main-grid {
  140.             display: grid;
  141.             /* NOTE: 两栏布局,左侧固定宽度,右侧自适应 */
  142.             grid-template-columns: 350px 1fr;
  143.             gap: 2rem;
  144.             align-items: start;
  145.         }
  146.   
  147.         /* WARN: 移动端适配 - 900px以下变为单栏布局 */
  148.         @media (max-width: 900px) {
  149.             .main-grid {
  150.                 grid-template-columns: 1fr;
  151.             }
  152.             .list-panel { order: 2; }
  153.             .wheel-panel { order: 1; }
  154.         }
  155.    
  156.         .panel {
  157.             /* NOTE: 毛玻璃效果面板 */
  158.             background: var(--bg-overlay);
  159.             backdrop-filter: blur(20px) saturate(180%);
  160.             border-radius: 1.5rem;
  161.             padding: 1.8rem;
  162.             border: 1px solid var(--border-light);
  163.             box-shadow:
  164.                 0 8px 32px var(--shadow-color),
  165.                 inset 0 1px 0 rgba(255, 255, 255, 0.2);
  166.             transition: transform 0.3s ease, box-shadow 0.3s ease;
  167.             position: relative;
  168.             overflow: hidden;
  169.         }
  170.          
  171.         .panel::before {
  172.             content: '';
  173.             position: absolute;
  174.             top: 0;
  175.             left: 0;
  176.             right: 0;
  177.             height: 4px;
  178.             background: linear-gradient(90deg, var(--primary-color), var(--secondary-color), var(--accent-color));
  179.             border-radius: 1.5rem 1.5rem 0 0;
  180.         }
  181.          
  182.         .panel:hover {
  183.             transform: translateY(-5px);
  184.             box-shadow:
  185.                 0 12px 40px rgba(0, 0, 0, 0.15),
  186.                 inset 0 1px 0 rgba(255, 255, 255, 0.3);
  187.         }
  188.    
  189.         .panel-header {
  190.             display: flex;
  191.             align-items: center;
  192.             justify-content: space-between;
  193.             gap: 0.5rem;
  194.             margin-bottom: 1.8rem;
  195.             padding-bottom: 1.2rem;
  196.             border-bottom: 1px solid var(--border-light);
  197.         }
  198.    
  199.         .panel-header h2 {
  200.             font-size: 1.6rem;
  201.             font-weight: 800;
  202.             color: #fff;
  203.             display: flex;
  204.             align-items: center;
  205.             gap: 0.75rem;
  206.         }
  207.    
  208.         .icon {
  209.             color: #fcd34d;
  210.             font-size: 2rem;
  211.             filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
  212.         }
  213.          
  214.         .count-badge {
  215.             font-size: 0.9rem;
  216.             font-weight: 700;
  217.             color: #fff;
  218.             background: linear-gradient(135deg, var(--success-color), #0d9668);
  219.             padding: 0.4rem 0.8rem;
  220.             border-radius: 2rem;
  221.             box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
  222.         }
  223.    
  224.         .input-group {
  225.             margin-bottom: 1.5rem;
  226.         }
  227.    
  228.         .input-label {
  229.             color: var(--text-light);
  230.             margin-bottom: 0.75rem;
  231.             display: block;
  232.             font-weight: 600;
  233.             font-size: 1.1rem;
  234.             display: flex;
  235.             align-items: center;
  236.             gap: 0.5rem;
  237.         }
  238.          
  239.         .input-label .label-icon {
  240.             font-size: 1.2rem;
  241.         }
  242.    
  243.         .input-row {
  244.             display: flex;
  245.             gap: 0.75rem;
  246.         }
  247.    
  248.         input, textarea {
  249.             background: rgba(255, 255, 255, 0.15);
  250.             border: 2px solid var(--border-light);
  251.             border-radius: 0.75rem;
  252.             padding: 1rem 1.25rem;
  253.             color: white;
  254.             outline: none;
  255.             transition: all 0.3s;
  256.             width: 100%;
  257.             font-size: 1rem;
  258.             font-weight: 500;
  259.         }
  260.          
  261.         input::placeholder, textarea::placeholder {
  262.             color: rgba(255, 255, 255, 0.5);
  263.             font-weight: 400;
  264.         }
  265.    
  266.         input:focus, textarea:focus {
  267.             border-color: #fcd34d;
  268.             background: rgba(255, 255, 255, 0.25);
  269.             box-shadow: 0 0 0 4px rgba(252, 211, 77, 0.2);
  270.         }
  271.    
  272.         .input-row input {
  273.             flex: 1;
  274.         }
  275.    
  276.         .btn {
  277.             padding: 1rem 1.5rem;
  278.             border: none;
  279.             border-radius: 0.75rem;
  280.             font-weight: 700;
  281.             cursor: pointer;
  282.             transition: all 0.3s;
  283.             display: inline-flex;
  284.             align-items: center;
  285.             justify-content: center;
  286.             gap: 0.75rem;
  287.             text-decoration: none;
  288.             user-select: none;
  289.             white-space: nowrap;
  290.             font-size: 1rem;
  291.             letter-spacing: 0.5px;
  292.             position: relative;
  293.             overflow: hidden;
  294.         }
  295.          
  296.         .btn::before {
  297.             content: '';
  298.             position: absolute;
  299.             top: 0;
  300.             left: -100%;
  301.             width: 100%;
  302.             height: 100%;
  303.             background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
  304.             transition: left 0.5s;
  305.         }
  306.          
  307.         .btn:hover::before {
  308.             left: 100%;
  309.         }
  310.    
  311.         .btn:disabled {
  312.             opacity: 0.6;
  313.             cursor: not-allowed;
  314.             transform: none !important;
  315.         }
  316.    
  317.         .btn-primary {
  318.             background: linear-gradient(135deg, var(--primary-color), #ea580c);
  319.             color: white;
  320.             box-shadow: 0 6px 16px rgba(249, 115, 22, 0.4);
  321.         }
  322.    
  323.         .btn-primary:hover:not(:disabled) {
  324.             background: linear-gradient(135deg, #ea580c, #c2410c);
  325.             transform: translateY(-2px);
  326.             box-shadow: 0 8px 20px rgba(249, 115, 22, 0.5);
  327.         }
  328.    
  329.         .btn-success {
  330.             background: linear-gradient(135deg, var(--success-color), #0d9668);
  331.             color: white;
  332.             box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
  333.         }
  334.   
  335.         .btn-success:hover:not(:disabled) {
  336.             background: linear-gradient(135deg, #0d9668, #047857);
  337.             transform: translateY(-2px);
  338.             box-shadow: 0 8px 20px rgba(16, 185, 129, 0.5);
  339.         }
  340.          
  341.         .btn-secondary {
  342.             background: rgba(255, 255, 255, 0.2);
  343.             color: white;
  344.             border: 2px solid var(--border-light);
  345.             box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  346.         }
  347.    
  348.         .btn-secondary:hover:not(:disabled) {
  349.             background: rgba(255, 255, 255, 0.3);
  350.             transform: translateY(-2px);
  351.             box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
  352.         }
  353.          
  354.         .btn-danger {
  355.             background: linear-gradient(135deg, var(--danger-color), #b91c1c);
  356.             color: white;
  357.             box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
  358.         }
  359.          
  360.         .btn-danger:hover:not(:disabled) {
  361.             background: linear-gradient(135deg, #b91c1c, #991b1b);
  362.             transform: translateY(-2px);
  363.             box-shadow: 0 8px 20px rgba(239, 68, 68, 0.5);
  364.         }
  365.          
  366.         .btn-warning {
  367.             background: linear-gradient(135deg, var(--warning-color), #d97706);
  368.             color: white;
  369.             box-shadow: 0 6px 16px rgba(245, 158, 11, 0.4);
  370.         }
  371.          
  372.         .btn-warning:hover:not(:disabled) {
  373.             background: linear-gradient(135deg, #d97706, #b45309);
  374.             transform: translateY(-2px);
  375.             box-shadow: 0 8px 20px rgba(245, 158, 11, 0.5);
  376.         }
  377.    
  378.         .punishment-list {
  379.             max-height: 400px;
  380.             overflow-y: auto;
  381.             padding-right: 5px;
  382.             margin-bottom: 1.5rem;
  383.         }
  384.          
  385.         .punishment-list::-webkit-scrollbar {
  386.             width: 8px;
  387.         }
  388.         .punishment-list::-webkit-scrollbar-track {
  389.             background: rgba(255, 255, 255, 0.05);
  390.             border-radius: 4px;
  391.         }
  392.         .punishment-list::-webkit-scrollbar-thumb {
  393.             background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
  394.             border-radius: 4px;
  395.         }
  396.    
  397.         .punishment-item {
  398.             background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
  399.             padding: 1.25rem;
  400.             border-radius: 1rem;
  401.             margin-bottom: 0.75rem;
  402.             display: flex;
  403.             justify-content: space-between;
  404.             align-items: center;
  405.             transition: all 0.3s;
  406.             color: white;
  407.             border-left: 4px solid transparent;
  408.             border: 1px solid var(--border-light);
  409.             position: relative;
  410.             overflow: hidden;
  411.         }
  412.          
  413.         .punishment-item::before {
  414.             content: '';
  415.             position: absolute;
  416.             top: 0;
  417.             left: 0;
  418.             width: 4px;
  419.             height: 100%;
  420.             background: linear-gradient(180deg, var(--primary-color), var(--secondary-color));
  421.             opacity: 0;
  422.             transition: opacity 0.3s;
  423.         }
  424.    
  425.         .punishment-item:hover {
  426.             background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.1));
  427.             transform: translateX(8px);
  428.             box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
  429.         }
  430.          
  431.         .punishment-item:hover::before {
  432.             opacity: 1;
  433.         }
  434.    
  435.         .punishment-item.active {
  436.             background: linear-gradient(135deg, rgba(252, 211, 77, 0.25), rgba(245, 158, 11, 0.15));
  437.             border-left-color: #fcd34d;
  438.             font-weight: bold;
  439.             box-shadow: 0 4px 12px rgba(252, 211, 77, 0.2);
  440.         }
  441.          
  442.         .punishment-item .food-icon {
  443.             font-size: 1.5rem;
  444.             margin-right: 0.75rem;
  445.         }
  446.    
  447.         .punishment-item .remove-btn {
  448.             background: rgba(0, 0, 0, 0.2);
  449.             border: none;
  450.             color: #fca5a5;
  451.             cursor: pointer;
  452.             font-size: 1.3rem;
  453.             width: 32px;
  454.             height: 32px;
  455.             border-radius: 50%;
  456.             display: flex;
  457.             align-items: center;
  458.             justify-content: center;
  459.             transition: all 0.2s;
  460.             flex-shrink: 0;
  461.         }
  462.    
  463.         .punishment-item .remove-btn:hover {
  464.             background: #ef4444;
  465.             color: white;
  466.             transform: rotate(90deg);
  467.         }
  468.    
  469.         .wheel-container {
  470.             display: flex;
  471.             flex-direction: column;
  472.             align-items: center;
  473.             justify-content: center;
  474.             width: 100%;
  475.         }
  476.    
  477.         .wheel-wrapper {
  478.             position: relative;
  479.             margin-bottom: 2.5rem;
  480.             filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.3));
  481.             width: 100%;
  482.             /* OPTIMIZE: 使用aspect-ratio保持正方形 */
  483.             max-width: 500px;
  484.             aspect-ratio: 1/1;
  485.         }
  486.    
  487.         .wheel-pointer {
  488.             position: absolute;
  489.             top: -15px;
  490.             left: 50%;
  491.             transform: translateX(-50%);
  492.             z-index: 20;
  493.             width: 50px;
  494.             height: 50px;
  495.             background: linear-gradient(135deg, var(--danger-color), #b91c1c);
  496.             border: 5px solid #fff;
  497.             border-radius: 50%;
  498.             box-shadow: 0 8px 20px rgba(0,0,0,0.4);
  499.             display: flex;
  500.             align-items: center;
  501.             justify-content: center;
  502.             font-size: 1.5rem;
  503.             animation: pulsePointer 2s infinite;
  504.         }
  505.          
  506.         .wheel-pointer::after {
  507.             content: '&#128071;';
  508.             position: absolute;
  509.             top: 100%;
  510.             left: 50%;
  511.             transform: translateX(-50%);
  512.             font-size: 1.8rem;
  513.             filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));
  514.         }
  515.    
  516.         .wheel-svg {
  517.             width: 100%;
  518.             height: 100%;
  519.             transition-property: transform;
  520.             /* NOTE: 自定义贝塞尔曲线实现"缓慢停止"效果 */
  521.             transition-timing-function: cubic-bezier(0.15, 0, 0.20, 1);
  522.         }
  523.    
  524.         .control-buttons {
  525.             display: flex;
  526.             gap: 1.5rem;
  527.             margin-bottom: 2.5rem;
  528.             flex-wrap: wrap;
  529.             justify-content: center;
  530.         }
  531.    
  532.         .result-display {
  533.             background: linear-gradient(135deg, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.5));
  534.             backdrop-filter: blur(20px);
  535.             border: 3px solid #fcd34d;
  536.             color: #fcd34d;
  537.             padding: 2rem 3rem;
  538.             border-radius: 1.5rem;
  539.             text-align: center;
  540.             font-weight: bold;
  541.             box-shadow:
  542.                 0 15px 35px rgba(0, 0, 0, 0.3),
  543.                 inset 0 1px 0 rgba(255, 255, 255, 0.2);
  544.             min-width: 320px;
  545.             transform: scale(0.9);
  546.             opacity: 0;
  547.             transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  548.             position: relative;
  549.             overflow: hidden;
  550.         }
  551.          
  552.         .result-display::before {
  553.             content: '';
  554.             position: absolute;
  555.             top: 0;
  556.             left: 0;
  557.             right: 0;
  558.             height: 4px;
  559.             background: linear-gradient(90deg, var(--primary-color), #fcd34d, var(--secondary-color));
  560.         }
  561.   
  562.         .result-display.show {
  563.             transform: scale(1);
  564.             opacity: 1;
  565.         }
  566.    
  567.         .result-display h3 {
  568.             font-size: 1.6rem;
  569.             margin-bottom: 1rem;
  570.             color: white;
  571.             text-shadow: 0 2px 4px rgba(0,0,0,0.3);
  572.         }
  573.          
  574.         .result-display .punishment-text {
  575.             font-size: 2rem;
  576.             font-weight: 900;
  577.             word-break: break-word;
  578.             line-height: 1.4;
  579.             background: linear-gradient(135deg, #fcd34d, #f59e0b);
  580.             -webkit-background-clip: text;
  581.             -webkit-text-fill-color: transparent;
  582.             background-clip: text;
  583.             text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  584.         }
  585.          
  586.         .result-emoji {
  587.             font-size: 3rem;
  588.             margin-bottom: 1rem;
  589.             display: block;
  590.             filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
  591.         }
  592.    
  593.         .celebration-modal {
  594.             position: fixed;
  595.             top: 0;
  596.             left: 0;
  597.             right: 0;
  598.             bottom: 0;
  599.             background: rgba(0, 0, 0, 0.85);
  600.             backdrop-filter: blur(10px);
  601.             z-index: 50;
  602.             display: flex;
  603.             align-items: center;
  604.             justify-content: center;
  605.             padding: 1rem;
  606.         }
  607.    
  608.         .celebration-content {
  609.             background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
  610.             border-radius: 2rem;
  611.             padding: 3rem;
  612.             max-width: 36rem;
  613.             width: 100%;
  614.             text-align: center;
  615.             position: relative;
  616.             overflow: hidden;
  617.             box-shadow:
  618.                 0 30px 60px rgba(0, 0, 0, 0.5),
  619.                 inset 0 1px 0 rgba(255, 255, 255, 0.3);
  620.             border: 4px solid rgba(255,255,255,0.2);
  621.         }
  622.    
  623.         .celebration-bg {
  624.             position: absolute;
  625.             top: 0;
  626.             left: 0;
  627.             right: 0;
  628.             bottom: 0;
  629.             background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.2) 0%, transparent 50%),
  630.                        radial-gradient(circle at 70% 70%, rgba(255,255,255,0.15) 0%, transparent 50%);
  631.             animation: pulseBg 3s infinite;
  632.         }
  633.    
  634.         .celebration-close {
  635.             position: absolute;
  636.             top: 1.5rem;
  637.             right: 1.5rem;
  638.             width: 3rem;
  639.             height: 3rem;
  640.             background: rgba(0, 0, 0, 0.3);
  641.             border: 2px solid rgba(255, 255, 255, 0.3);
  642.             border-radius: 50%;
  643.             color: white;
  644.             cursor: pointer;
  645.             display: flex;
  646.             align-items: center;
  647.             justify-content: center;
  648.             font-size: 1.8rem;
  649.             transition: all 0.3s;
  650.             font-weight: bold;
  651.         }
  652.    
  653.         .celebration-close:hover {
  654.             background: rgba(255, 255, 255, 0.2);
  655.             transform: rotate(90deg);
  656.         }
  657.    
  658.         .celebration-inner {
  659.             position: relative;
  660.             z-index: 10;
  661.         }
  662.    
  663.         .celebration-emoji {
  664.             font-size: 6rem;
  665.             margin-bottom: 1rem;
  666.             display: block;
  667.             filter: drop-shadow(0 6px 12px rgba(0,0,0,0.4));
  668.             animation: bounce 2s infinite;
  669.         }
  670.    
  671.         .celebration-title {
  672.             font-size: 2.8rem;
  673.             font-weight: 900;
  674.             color: #fff;
  675.             margin-bottom: 1.5rem;
  676.             text-transform: uppercase;
  677.             letter-spacing: 2px;
  678.             text-shadow: 0 4px 8px rgba(0,0,0,0.3);
  679.         }
  680.    
  681.         .celebration-result-box {
  682.             background: rgba(0, 0, 0, 0.3);
  683.             backdrop-filter: blur(10px);
  684.             border-radius: 1.5rem;
  685.             padding: 2.5rem;
  686.             margin-bottom: 2.5rem;
  687.             border: 2px dashed rgba(255,255,255,0.3);
  688.         }
  689.    
  690.         .celebration-result-label {
  691.             font-size: 1.4rem;
  692.             color: rgba(255,255,255,0.9);
  693.             margin-bottom: 1rem;
  694.             font-weight: 600;
  695.         }
  696.   
  697.         .celebration-result-text {
  698.             font-size: 2.4rem;
  699.             font-weight: 900;
  700.             color: #fcd34d;
  701.             line-height: 1.3;
  702.             text-shadow: 0 3px 6px rgba(0,0,0,0.3);
  703.         }
  704.    
  705.         .celebration-btn {
  706.             background: white;
  707.             color: var(--secondary-color);
  708.             border: none;
  709.             padding: 1.25rem 4rem;
  710.             border-radius: 3rem;
  711.             font-weight: 900;
  712.             font-size: 1.4rem;
  713.             cursor: pointer;
  714.             transition: all 0.3s;
  715.             box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
  716.             position: relative;
  717.             overflow: hidden;
  718.         }
  719.    
  720.         .celebration-btn:hover {
  721.             background: #f3f4f6;
  722.             transform: translateY(-3px) scale(1.05);
  723.             box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
  724.         }
  725.    
  726.         .empty-state {
  727.             padding: 2.5rem;
  728.             border-radius: 1.5rem;
  729.             background: rgba(255, 255, 255, 0.05);
  730.             color: rgba(255, 255, 255, 0.6);
  731.             text-align: center;
  732.             font-style: italic;
  733.             border: 2px dashed rgba(255, 255, 255, 0.2);
  734.             font-size: 1.1rem;
  735.         }
  736.          
  737.         .file-import-note {
  738.             color: var(--text-lighter);
  739.             font-size: 0.9rem;
  740.             margin-top: 0.5rem;
  741.             display: flex;
  742.             align-items: center;
  743.             gap: 0.5rem;
  744.         }
  745.          
  746.         .clear-all-section {
  747.             display: flex;
  748.             justify-content: flex-end;
  749.             margin-top: 1.5rem;
  750.             padding-top: 1.5rem;
  751.             border-top: 1px solid var(--border-light);
  752.         }
  753.          
  754.         /* NOTE: 一键清空确认弹窗样式 */
  755.         .confirm-clear-modal {
  756.             position: fixed;
  757.             top: 0;
  758.             left: 0;
  759.             right: 0;
  760.             bottom: 0;
  761.             background: rgba(0, 0, 0, 0.85);
  762.             backdrop-filter: blur(10px);
  763.             z-index: 100;
  764.             display: flex;
  765.             align-items: center;
  766.             justify-content: center;
  767.             padding: 1rem;
  768.         }
  769.          
  770.         .confirm-clear-content {
  771.             background: linear-gradient(135deg, var(--danger-color), #b91c1c);
  772.             border-radius: 2rem;
  773.             padding: 3rem;
  774.             max-width: 32rem;
  775.             width: 100%;
  776.             text-align: center;
  777.             position: relative;
  778.             overflow: hidden;
  779.             box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5);
  780.             border: 4px solid rgba(255,255,255,0.2);
  781.         }
  782.          
  783.         .confirm-clear-icon {
  784.             font-size: 4.5rem;
  785.             margin-bottom: 1.5rem;
  786.             display: block;
  787.             filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
  788.             animation: shake 0.5s ease-in-out;
  789.         }
  790.          
  791.         .confirm-clear-title {
  792.             font-size: 2rem;
  793.             font-weight: 900;
  794.             color: #fff;
  795.             margin-bottom: 1.5rem;
  796.             text-shadow: 0 3px 6px rgba(0,0,0,0.3);
  797.         }
  798.          
  799.         .confirm-clear-message {
  800.             font-size: 1.2rem;
  801.             color: rgba(255, 255, 255, 0.9);
  802.             margin-bottom: 2.5rem;
  803.             line-height: 1.6;
  804.         }
  805.          
  806.         .confirm-clear-count {
  807.             font-weight: 900;
  808.             color: #fcd34d;
  809.             font-size: 1.5rem;
  810.             text-shadow: 0 2px 4px rgba(0,0,0,0.3);
  811.         }
  812.          
  813.         .confirm-clear-buttons {
  814.             display: flex;
  815.             gap: 1.5rem;
  816.             justify-content: center;
  817.         }
  818.          
  819.         .confirm-clear-btn {
  820.             padding: 1.2rem 2.5rem;
  821.             border: none;
  822.             border-radius: 1rem;
  823.             font-weight: 800;
  824.             font-size: 1.2rem;
  825.             cursor: pointer;
  826.             transition: all 0.3s;
  827.             min-width: 140px;
  828.             position: relative;
  829.             overflow: hidden;
  830.         }
  831.          
  832.         .confirm-clear-btn.confirm {
  833.             background: #10b981;
  834.             color: white;
  835.             box-shadow: 0 8px 20px rgba(16, 185, 129, 0.4);
  836.         }
  837.          
  838.         .confirm-clear-btn.confirm:hover {
  839.             background: #0d9668;
  840.             transform: translateY(-3px);
  841.             box-shadow: 0 12px 25px rgba(16, 185, 129, 0.5);
  842.         }
  843.          
  844.         .confirm-clear-btn.cancel {
  845.             background: rgba(255, 255, 255, 0.2);
  846.             color: white;
  847.             border: 2px solid rgba(255, 255, 255, 0.3);
  848.             box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
  849.         }
  850.          
  851.         .confirm-clear-btn.cancel:hover {
  852.             background: rgba(255, 255, 255, 0.3);
  853.             transform: translateY(-3px);
  854.             box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
  855.         }
  856.    
  857.         @keyframes pulse {
  858.             0%, 100% { transform: scale(1); }
  859.             50% { transform: scale(1.05); }
  860.         }
  861.          
  862.         @keyframes pulsePointer {
  863.             0%, 100% { transform: translateX(-50%) scale(1); }
  864.             50% { transform: translateX(-50%) scale(1.1); }
  865.         }
  866.   
  867.         @keyframes pulseBg {
  868.             0%, 100% { opacity: 0.5; }
  869.             50% { opacity: 1; }
  870.         }
  871.    
  872.         @keyframes bounce {
  873.             0%, 20%, 53%, 80%, 100% { transform: translateY(0); }
  874.             40%, 43% { transform: translateY(-20px); }
  875.             70% { transform: translateY(-10px); }
  876.             90% { transform: translateY(-5px); }
  877.         }
  878.   
  879.         @keyframes modalPop {
  880.             0% { transform: scale(0.8) translateY(50px); opacity: 0; }
  881.             70% { transform: scale(1.05); }
  882.             100% { transform: scale(1) translateY(0); opacity: 1; }
  883.         }
  884.          
  885.         @keyframes shake {
  886.             0%, 100% { transform: translateX(0); }
  887.             10%, 30%, 50%, 70%, 90% { transform: translateX(-8px); }
  888.             20%, 40%, 60%, 80% { transform: translateX(8px); }
  889.         }
  890.          
  891.         @keyframes spin {
  892.             0% { transform: rotate(0deg); }
  893.             100% { transform: rotate(360deg); }
  894.         }
  895.          
  896.         @keyframes float {
  897.             0%, 100% { transform: translateY(0); }
  898.             50% { transform: translateY(-20px); }
  899.         }
  900.    
  901.         .hidden {
  902.             display: none;
  903.         }
  904.   
  905.         .modal-enter {
  906.             animation: modalPop 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
  907.         }
  908.          
  909.         .shake {
  910.             animation: shake 0.5s ease-in-out;
  911.         }
  912.          
  913.         /* TODO: 添加暗色模式支持 */
  914.         /* OPTIMIZE: 将动画关键帧提取到单独部分以便维护 */
  915.          
  916.         /* 美食图标样式 */
  917.         .food-emoji {
  918.             display: inline-block;
  919.             animation: float 3s ease-in-out infinite;
  920.         }
  921.          
  922.         /* 响应式调整 */
  923.         @media (max-width: 768px) {
  924.             .panel {
  925.                 padding: 1.5rem;
  926.             }
  927.             
  928.             .header h1 {
  929.                 font-size: clamp(1.5rem, 5vw, 2.5rem);
  930.             }
  931.             
  932.             .header p {
  933.                 font-size: 1rem;
  934.             }
  935.             
  936.             .control-buttons {
  937.                 flex-direction: column;
  938.                 align-items: center;
  939.             }
  940.             
  941.             .btn {
  942.                 width: 100%;
  943.                 max-width: 300px;
  944.             }
  945.             
  946.             .celebration-content {
  947.                 padding: 2rem;
  948.             }
  949.             
  950.             .celebration-title {
  951.                 font-size: 2rem;
  952.             }
  953.             
  954.             .celebration-result-text {
  955.                 font-size: 1.8rem;
  956.             }
  957.         }
  958.     </style>
  959. </head>
  960. <body>
  961.     <div class="container">
  962.         <!-- 头部标题 -->
  963.         <div class="header">
  964.             <div style="display: flex; align-items: center; justify-content: center; gap: 1.5rem; margin-bottom: 1rem;">
  965.                 <span class="emoji-bounce food-emoji">&#127829;</span>
  966.                 <h1>美食大转盘 - 点餐助手</h1>
  967.                 <span class="emoji-bounce food-emoji">&#127828;</span>
  968.             </div>
  969.             <p>还在为"吃什么"发愁吗?让转盘帮你决定今天的美食!</p>
  970.             <div class="subtitle">添加你喜欢的菜品,让选择变得简单有趣</div>
  971.         </div>
  972.    
  973.         <div class="main-grid">
  974.             <!--
  975.             ============================================
  976.             菜品项目库面板
  977.             给使用者:
  978.             - 在此添加/删除菜品项目
  979.             - 支持导入txt文件批量添加
  980.             - 项目列表会保存在浏览器本地
  981.             ============================================
  982.             -->
  983.             <div class="panel list-panel">
  984.                 <div class="panel-header">
  985.                     <div style="display: flex; align-items: center; gap: 0.75rem;">
  986.                         <span class="icon">&#127869;&#65039;</span>
  987.                         <h2>我的美食菜单</h2>
  988.                         <div class="count-badge" id="punishmentCount">0项</div>
  989.                     </div>
  990.                     <button class="btn btn-danger" id="clearAllPunishmentsBtn" style="padding: 0.8rem 1.2rem;">
  991.                         <span>&#128465;&#65039;</span>
  992.                         一键清空
  993.                     </button>
  994.                 </div>
  995.                   
  996.                 <div class="input-group">
  997.                     <label class="input-label">
  998.                         <span class="label-icon">&#10133;</span>
  999.                         添加新菜品:
  1000.                     </label>
  1001.                     <div class="input-row">
  1002.                         <input type="text" id="punishmentInput" placeholder="例如:麻辣香锅、披萨、寿司..." autocomplete="off">
  1003.                         <button class="btn btn-success" id="addPunishmentBtn">
  1004.                             <span>&#127860;</span>
  1005.                             添加
  1006.                         </button>
  1007.                     </div>
  1008.                 </div>
  1009.                  
  1010.                 <!-- NOTE: 文件导入功能 -->
  1011.                 <div class="input-group">
  1012.                     <label class="input-label">
  1013.                         <span class="label-icon">&#128193;</span>
  1014.                         导入菜品列表(TXT文件):
  1015.                     </label>
  1016.                     <div class="input-row">
  1017.                         <input type="file" id="importFile" accept=".txt" style="flex: 1; padding: 0.75rem;">
  1018.                         <button class="btn btn-secondary" id="importBtn">
  1019.                             <span>&#128229;</span>
  1020.                             导入
  1021.                         </button>
  1022.                     </div>
  1023.                     <div class="file-import-note">
  1024.                         <span>&#128221;</span>
  1025.                         每行一个菜品,支持批量导入
  1026.                     </div>
  1027.                 </div>
  1028.    
  1029.                 <div class="punishment-list" id="punishmentList">
  1030.                     <!-- 菜品项目动态加载 -->
  1031.                 </div>
  1032.                  
  1033.                 <div class="clear-all-section">
  1034.                     <button class="btn btn-warning" id="resetToDefaultBtn" style="padding: 0.8rem 1.5rem;">
  1035.                         <span>&#128260;</span>
  1036.                         恢复默认菜单
  1037.                     </button>
  1038.                 </div>
  1039.             </div>
  1040.    
  1041.             <!--
  1042.             ============================================
  1043.             转盘控制面板
  1044.             给使用者:
  1045.             - 点击"开始点餐"旋转转盘
  1046.             - 点击"重置"将转盘归零
  1047.             - 结果会在下方和弹窗显示
  1048.             ============================================
  1049.             -->
  1050.             <div class="panel wheel-panel" style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
  1051.                 <div class="wheel-container">
  1052.                     <div class="wheel-wrapper">
  1053.                         <!-- 转盘指针 -->
  1054.                         <div class="wheel-pointer">&#128071;</div>
  1055.                         <!-- SVG转盘 -->
  1056.                         <svg id="wheelSvg" class="wheel-svg" viewBox="-250 -250 500 500">
  1057.                             <defs>
  1058.                                 <radialGradient id="centerGradient">
  1059.                                     <stop offset="0%" stop-color="#fcd34d" />
  1060.                                     <stop offset="100%" stop-color="#f59e0b" />
  1061.                                 </radialGradient>
  1062.                                 <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
  1063.                                     <feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="rgba(0,0,0,0.4)"/>
  1064.                                 </filter>
  1065.                                 <linearGradient id="wheelGradient" x1="0%" y1="0%" x2="100%" y2="100%">
  1066.                                     <stop offset="0%" style="stop-color:#ff6b6b;stop-opacity:1" />
  1067.                                     <stop offset="25%" style="stop-color:#ff9a3c;stop-opacity:1" />
  1068.                                     <stop offset="50%" style="stop-color:#f6d365;stop-opacity:1" />
  1069.                                     <stop offset="75%" style="stop-color:#84fab0;stop-opacity:1" />
  1070.                                     <stop offset="100%" style="stop-color:#8fd3f4;stop-opacity:1" />
  1071.                                 </linearGradient>
  1072.                             </defs>
  1073.                             <!-- 转盘扇形区域(动态生成) -->
  1074.                             <g id="wheelSections"></g>
  1075.                             <!-- 转盘中心装饰 -->
  1076.                             <circle cx="0" cy="0" r="50" fill="url(#centerGradient)" stroke="#fff" stroke-width="6" filter="url(#shadow)"/>
  1077.                             <circle cx="0" cy="0" r="35" fill="none" stroke="#fff" stroke-width="3" stroke-dasharray="4 4"/>
  1078.                             <text x="0" y="5" fill="#fff" font-size="18" font-weight="bold" text-anchor="middle" dominant-baseline="middle">点餐</text>
  1079.                         </svg>
  1080.                     </div>
  1081.    
  1082.                     <!-- 控制按钮区域 -->
  1083.                     <div class="control-buttons">
  1084.                         <button class="btn btn-primary" id="startBtn" style="min-width: 180px; font-size: 1.3rem; padding: 1.2rem 2rem;">
  1085.                             <span>&#127919;</span>
  1086.                             <span id="startBtnText">开始点餐</span>
  1087.                         </button>
  1088.                         <button class="btn btn-secondary" id="resetBtn" style="padding: 1.2rem 2rem;">
  1089.                             <span>&#128260;</span>
  1090.                             重置转盘
  1091.                         </button>
  1092.                     </div>
  1093.    
  1094.                     <!-- 结果展示区域 -->
  1095.                     <div id="resultDisplay" class="result-display">
  1096.                         <div class="result-emoji">&#129300;</div>
  1097.                         <h3>今日推荐</h3>
  1098.                         <div class="punishment-text" id="winnerPunishment">等待选择...</div>
  1099.                     </div>
  1100.                 </div>
  1101.             </div>
  1102.         </div>
  1103.     </div>
  1104.   
  1105.     <!--
  1106.     ============================================
  1107.     庆祝弹窗
  1108.     给使用者:
  1109.     - 抽取结果后会显示此弹窗
  1110.     - 点击"确定"或关闭按钮关闭弹窗
  1111.     ============================================
  1112.     -->
  1113.     <div id="celebrationModal" class="celebration-modal hidden">
  1114.         <div class="celebration-content">
  1115.             <div class="celebration-bg"></div>
  1116.             <button class="celebration-close" id="closeCelebrationBtn">×</button>
  1117.             <div class="celebration-inner">
  1118.                 <span class="celebration-emoji">&#127881;</span>
  1119.                 <h2 class="celebration-title">今日美食已选定!</h2>
  1120.                   
  1121.                 <div class="celebration-result-box">
  1122.                     <div class="celebration-result-label">转盘推荐:</div>
  1123.                     <div class="celebration-result-text" id="celebrationPunishment"></div>
  1124.                 </div>
  1125.    
  1126.                 <button class="celebration-btn" id="confirmCelebrationBtn">
  1127.                     <span>&#128523;</span>
  1128.                     太棒了,就吃这个!
  1129.                 </button>
  1130.             </div>
  1131.         </div>
  1132.     </div>
  1133.      
  1134.     <!--
  1135.     ============================================
  1136.     一键清空确认弹窗
  1137.     给使用者:
  1138.     - 确认是否清空所有菜品项目
  1139.     - 提供取消选项防止误操作
  1140.     ============================================
  1141.     -->
  1142.     <div id="confirmClearModal" class="confirm-clear-modal hidden">
  1143.         <div class="confirm-clear-content modal-enter">
  1144.             <span class="confirm-clear-icon">&#9888;&#65039;</span>
  1145.             <h2 class="confirm-clear-title">确认清空菜单</h2>
  1146.             <div class="confirm-clear-message">
  1147.                 您确定要清空所有 <span class="confirm-clear-count" id="clearCount">0</span> 个菜品吗?<br>
  1148.                 <strong>此操作无法撤销!</strong> 所有自定义和导入的菜品将被永久删除。
  1149.             </div>
  1150.             <div class="confirm-clear-buttons">
  1151.                 <button class="confirm-clear-btn cancel" id="cancelClearBtn">
  1152.                     <span>&#10060;</span>
  1153.                     取消
  1154.                 </button>
  1155.                 <button class="confirm-clear-btn confirm" id="confirmClearBtn">
  1156.                     <span>&#9989;</span>
  1157.                     确定清空
  1158.                 </button>
  1159.             </div>
  1160.         </div>
  1161.     </div>
  1162.    
  1163.     <script>
  1164.         /*
  1165.         ============================================
  1166.         美食大转盘 - JavaScript核心逻辑
  1167.         给维护者:
  1168.         - 使用LocalStorage持久化数据
  1169.         - 转盘算法基于角度计算
  1170.         - 事件委托处理动态列表
  1171.         给学习者:
  1172.         - 使用SVG动态生成转盘
  1173.         - 三角函数计算扇形区域
  1174.         - 贝塞尔曲线实现缓动效果
  1175.         ============================================
  1176.         */
  1177.          
  1178.         // ========== 全局变量声明 ==========
  1179.          
  1180.         // NOTE: 默认菜品列表 - 当本地存储为空时使用
  1181.         const defaultPunishments = [
  1182.             '&#127829; 披萨',
  1183.             '&#127828; 汉堡',
  1184.             '&#127836; 拉面',
  1185.             '&#127843; 寿司',
  1186.             '&#129368; 麻辣香锅',
  1187.             '&#127858; 火锅',
  1188.             '&#129367; 沙拉',
  1189.             '&#127835; 咖喱饭',
  1190.             '&#127857; 便当',
  1191.             '&#129386; 三明治',
  1192.             '&#127837; 意大利面',
  1193.             '&#127790; 墨西哥卷饼',
  1194.             '&#129375; 饺子',
  1195.             '&#127844; 炸虾',
  1196.             '&#127846; 冰淇淋'
  1197.         ];
  1198.          
  1199.         // WARN: 从LocalStorage加载数据,失败时使用默认值
  1200.         let punishments = JSON.parse(localStorage.getItem('wheelPunishments')) || [...defaultPunishments];
  1201.          
  1202.         // 转盘状态控制变量
  1203.         let isSpinning = false;      // 是否正在旋转
  1204.         let currentRotation = 0;     // 当前旋转角度
  1205.         let currentPunishment = '';  // 当前选中的菜品
  1206.    
  1207.         // NOTE: 转盘扇形颜色列表 - 美食主题渐变色
  1208.         const colors = [
  1209.             '#FF6B6B', '#FF9A3C', '#F6D365', '#84FAB0', '#8FD3F4',
  1210.             '#A78BFA', '#F472B6', '#60A5FA', '#34D399', '#FBBF24',
  1211.             '#EF4444', '#F59E0B', '#10B981', '#3B82F6', '#8B5CF6'
  1212.         ];
  1213.          
  1214.         // ========== 工具函数 ==========
  1215.          
  1216.         /**
  1217.          * 更新菜品项目计数显示
  1218.          */
  1219.         function updatePunishmentCount() {
  1220.             const countElement = document.getElementById('punishmentCount');
  1221.             const count = punishments.length;
  1222.             countElement.textContent = `${count}项`;
  1223.             
  1224.             // 根据数量调整颜色
  1225.             if (count === 0) {
  1226.                 countElement.style.background = 'linear-gradient(135deg, #ef4444, #b91c1c)';
  1227.             } else if (count <= 5) {
  1228.                 countElement.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)';
  1229.             } else if (count <= 10) {
  1230.                 countElement.style.background = 'linear-gradient(135deg, #10b981, #0d9668)';
  1231.             } else {
  1232.                 countElement.style.background = 'linear-gradient(135deg, #8b5cf6, #7c3aed)';
  1233.             }
  1234.         }
  1235.          
  1236.         /**
  1237.          * 保存菜品列表到LocalStorage
  1238.          * @description 持久化数据,页面刷新后不丢失
  1239.          */
  1240.         function savePunishments() {
  1241.             localStorage.setItem('wheelPunishments', JSON.stringify(punishments));
  1242.         }
  1243.          
  1244.         /**
  1245.          * 格式化日期时间
  1246.          * @Param {Date} date - 日期对象
  1247.          * @returns {string} 格式化后的日期字符串
  1248.          */
  1249.         function formatDateTime(date) {
  1250.             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')}`;
  1251.         }
  1252.          
  1253.         /**
  1254.          * 截断文本,添加省略号
  1255.          * @param {string} text - 原始文本
  1256.          * @param {number} maxLength - 最大长度
  1257.          * @returns {string} 截断后的文本
  1258.          */
  1259.         function truncateText(text, maxLength = 100) {
  1260.             if (text.length <= maxLength) return text;
  1261.             return text.substring(0, maxLength) + '...';
  1262.         }
  1263.          
  1264.         /**
  1265.          * 显示一键清空确认弹窗
  1266.          */
  1267.         function showClearConfirmation() {
  1268.             if (isSpinning) {
  1269.                 alert('转盘旋转时不能清空菜品!');
  1270.                 return;
  1271.             }
  1272.             
  1273.             if (punishments.length === 0) {
  1274.                 alert('当前没有菜品可清空!');
  1275.                 return;
  1276.             }
  1277.             
  1278.             // 更新清空数量显示
  1279.             document.getElementById('clearCount').textContent = punishments.length;
  1280.             
  1281.             // 显示确认弹窗
  1282.             const modal = document.getElementById('confirmClearModal');
  1283.             modal.classList.remove('hidden');
  1284.             
  1285.             // 添加抖动效果以强调危险操作
  1286.             const content = modal.querySelector('.confirm-clear-content');
  1287.             content.classList.remove('shake');
  1288.             void content.offsetWidth; // 触发重排
  1289.             content.classList.add('shake');
  1290.         }
  1291.          
  1292.         /**
  1293.          * 隐藏一键清空确认弹窗
  1294.          */
  1295.         function hideClearConfirmation() {
  1296.             document.getElementById('confirmClearModal').classList.add('hidden');
  1297.         }
  1298.          
  1299.         /**
  1300.          * 执行一键清空操作
  1301.          * @description 清空所有菜品项目
  1302.          */
  1303.         function clearAllPunishments() {
  1304.             if (isSpinning) {
  1305.                 alert('转盘旋转时不能清空菜品!');
  1306.                 return;
  1307.             }
  1308.             
  1309.             // 记录清空前的数量
  1310.             const previousCount = punishments.length;
  1311.             
  1312.             // 清空菜品列表
  1313.             punishments = [];
  1314.             
  1315.             // 更新UI
  1316.             updatePunishmentList();
  1317.             updatePunishmentCount();
  1318.             updateWheel();
  1319.             
  1320.             // 保存到LocalStorage
  1321.             savePunishments();
  1322.             
  1323.             // 重置转盘状态
  1324.             currentRotation = 0;
  1325.             currentPunishment = '';
  1326.             const wheelSvg = document.getElementById('wheelSvg');
  1327.             wheelSvg.style.transitionDuration = '0ms';
  1328.             wheelSvg.style.transform = 'rotate(0deg)';
  1329.             
  1330.             // 隐藏结果
  1331.             const resultDisplay = document.getElementById('resultDisplay');
  1332.             resultDisplay.classList.remove('show');
  1333.             
  1334.             // 隐藏庆祝弹窗
  1335.             closeCelebration();
  1336.             
  1337.             // 显示操作反馈
  1338.             alert(`已清空 ${previousCount} 个菜品!`);
  1339.             
  1340.             // 隐藏确认弹窗
  1341.             hideClearConfirmation();
  1342.         }
  1343.          
  1344.         /**
  1345.          * 恢复默认菜单
  1346.          */
  1347.         function resetToDefaultMenu() {
  1348.             if (isSpinning) {
  1349.                 alert('转盘旋转时不能恢复默认菜单!');
  1350.                 return;
  1351.             }
  1352.             
  1353.             if (!confirm('确定要恢复默认菜单吗?当前所有菜品将被替换为默认菜品。')) {
  1354.                 return;
  1355.             }
  1356.             
  1357.             // 恢复默认菜品列表
  1358.             punishments = [...defaultPunishments];
  1359.             
  1360.             // 更新UI
  1361.             updatePunishmentList();
  1362.             updatePunishmentCount();
  1363.             updateWheel();
  1364.             
  1365.             // 保存到LocalStorage
  1366.             savePunishments();
  1367.             
  1368.             alert('已恢复默认菜单!');
  1369.         }
  1370.          
  1371.         /**
  1372.          * 导入TXT文件并解析菜品列表
  1373.          * @description 每行一个菜品,自动过滤空行和重复项
  1374.          * @param {File} file - 用户选择的TXT文件
  1375.          */
  1376.         function importPunishmentsFromTxt(file) {
  1377.             const reader = new FileReader();
  1378.             
  1379.             reader.onload = function(e) {
  1380.                 try {
  1381.                     const content = e.target.result;
  1382.                     // 按换行符分割,过滤空行和首尾空格
  1383.                     const newPunishments = content.split(/\r?\n/)
  1384.                         .map(item => item.trim())
  1385.                         .filter(item => item.length > 0);
  1386.                      
  1387.                     if (newPunishments.length === 0) {
  1388.                         alert('文件为空或格式不正确!');
  1389.                         return;
  1390.                     }
  1391.                      
  1392.                     // 合并并去重
  1393.                     const uniqueNewPunishments = newPunishments.filter(
  1394.                         item => !punishments.includes(item)
  1395.                     );
  1396.                      
  1397.                     if (uniqueNewPunishments.length === 0) {
  1398.                         alert('所有菜品已存在!');
  1399.                         return;
  1400.                     }
  1401.                      
  1402.                     // 添加到现有列表
  1403.                     punishments.push(...uniqueNewPunishments);
  1404.                      
  1405.                     // 更新UI并保存
  1406.                     updatePunishmentList();
  1407.                     updatePunishmentCount();
  1408.                     updateWheel();
  1409.                     savePunishments();
  1410.                      
  1411.                     alert(`成功导入 ${uniqueNewPunishments.length} 个新菜品!${newPunishments.length - uniqueNewPunishments.length > 0 ? `有 ${newPunishments.length - uniqueNewPunishments.length} 个重复菜品已跳过。` : ''}`);
  1412.                      
  1413.                 } catch (error) {
  1414.                     console.error('导入文件时出错:', error);
  1415.                     alert('文件解析失败,请检查文件格式!');
  1416.                 }
  1417.             };
  1418.             
  1419.             reader.onerror = function() {
  1420.                 alert('读取文件失败!');
  1421.             };
  1422.             
  1423.             reader.readAsText(file);
  1424.         }
  1425.         // ========== DOM加载完成事件 ==========
  1426.         document.addEventListener('DOMContentLoaded', function() {
  1427.             // 初始化UI
  1428.             updatePunishmentList();
  1429.             updatePunishmentCount();
  1430.             updateWheel();
  1431.                
  1432.             // 输入框回车事件
  1433.             const input = document.getElementById('punishmentInput');
  1434.             input.addEventListener('keypress', function(e) {
  1435.                 if (e.key === 'Enter') {
  1436.                     addPunishment();
  1437.                 }
  1438.             });
  1439.             
  1440.             // 绑定按钮事件
  1441.             document.getElementById('addPunishmentBtn').addEventListener('click', addPunishment);
  1442.             document.getElementById('importBtn').addEventListener('click', function() {
  1443.                 const fileInput = document.getElementById('importFile');
  1444.                 if (fileInput.files.length > 0) {
  1445.                     importPunishmentsFromTxt(fileInput.files[0]);
  1446.                     fileInput.value = ''; // 清空文件选择
  1447.                 } else {
  1448.                     alert('请先选择文件!');
  1449.                 }
  1450.             });
  1451.             
  1452.             document.getElementById('startBtn').addEventListener('click', startGame);
  1453.             document.getElementById('resetBtn').addEventListener('click', resetWheel);
  1454.             document.getElementById('resetToDefaultBtn').addEventListener('click', resetToDefaultMenu);
  1455.             document.getElementById('closeCelebrationBtn').addEventListener('click', closeCelebration);
  1456.             document.getElementById('confirmCelebrationBtn').addEventListener('click', closeCelebration);
  1457.             
  1458.             // 一键清空相关事件
  1459.             document.getElementById('clearAllPunishmentsBtn').addEventListener('click', showClearConfirmation);
  1460.             document.getElementById('cancelClearBtn').addEventListener('click', hideClearConfirmation);
  1461.             document.getElementById('confirmClearBtn').addEventListener('click', clearAllPunishments);
  1462.             
  1463.             // 点击确认弹窗外部关闭弹窗
  1464.             document.getElementById('confirmClearModal').addEventListener('click', function(e) {
  1465.                 if (e.target === this) {
  1466.                     hideClearConfirmation();
  1467.                 }
  1468.             });
  1469.             
  1470.             // 使用事件委托处理动态生成的删除按钮
  1471.             document.getElementById('punishmentList').addEventListener('click', function(e) {
  1472.                 if (e.target.classList.contains('remove-btn')) {
  1473.                     const item = e.target.closest('.punishment-item');
  1474.                     const index = Array.from(this.children).indexOf(item);
  1475.                     if (index !== -1) {
  1476.                         removePunishment(index);
  1477.                     }
  1478.                 }
  1479.             });
  1480.         });
  1481.          
  1482.         // ========== 核心功能函数 ==========
  1483.          
  1484.         /**
  1485.          * 添加新菜品项目
  1486.          * @description 验证输入并添加到列表,过滤重复项
  1487.          */
  1488.         function addPunishment() {
  1489.             const input = document.getElementById('punishmentInput');
  1490.             const name = input.value.trim();
  1491.             
  1492.             // 输入验证
  1493.             if (!name) {
  1494.                 alert('请输入菜品名称!');
  1495.                 return;
  1496.             }
  1497.             
  1498.             if (name.length > 30) {
  1499.                 alert('菜品名称过长,请控制在30字以内!');
  1500.                 return;
  1501.             }
  1502.                
  1503.             if (!punishments.includes(name)) {
  1504.                 punishments.push(name);
  1505.                 input.value = '';
  1506.                 input.focus();
  1507.                 updatePunishmentList();
  1508.                 updatePunishmentCount();
  1509.                 updateWheel();
  1510.                 savePunishments();
  1511.             } else {
  1512.                 alert('该菜品已存在!');
  1513.             }
  1514.         }
  1515.    
  1516.         /**
  1517.          * 删除菜品项目
  1518.          * @param {number} index - 要删除的项目索引
  1519.          * @description 转盘旋转时禁止删除
  1520.          */
  1521.         function removePunishment(index) {
  1522.             if (isSpinning) {
  1523.                 alert('转盘旋转时不能删除菜品!');
  1524.                 return;
  1525.             }
  1526.             
  1527.             if (confirm('确定要删除这个菜品吗?')) {
  1528.                 punishments.splice(index, 1);
  1529.                 updatePunishmentList();
  1530.                 updatePunishmentCount();
  1531.                 updateWheel();
  1532.                 savePunishments();
  1533.             }
  1534.         }
  1535.    
  1536.         /**
  1537.          * 更新菜品列表UI
  1538.          * @description 动态生成列表项,处理空状态
  1539.          */
  1540.         function updatePunishmentList() {
  1541.             const list = document.getElementById('punishmentList');
  1542.                
  1543.             // 处理空列表状态
  1544.             if (punishments.length === 0) {
  1545.                 list.innerHTML = '<div class="empty-state">菜品菜单是空的,请先添加菜品!</div>';
  1546.                 return;
  1547.             }
  1548.                
  1549.             // 生成列表HTML
  1550.             list.innerHTML = punishments.map((punishment, index) => {
  1551.                 // 提取emoji和文本
  1552.                 const emojiMatch = punishment.match(/^([\u{1F300}-\u{1F9FF}]|\u{1F1E6}-\u{1F1FF}|\u{2600}-\u{26FF}|\u{2700}-\u{27BF})/u);
  1553.                 const emoji = emojiMatch ? emojiMatch[0] : '&#127869;&#65039;';
  1554.                 const text = emojiMatch ? punishment.substring(emojiMatch[0].length).trim() : punishment;
  1555.                  
  1556.                 return `
  1557.                 <div class="punishment-item ${currentPunishment === punishment ? 'active' : ''}">
  1558.                     <div style="display: flex; align-items: center;">
  1559.                         <span class="food-emoji" style="font-size: 1.8rem; margin-right: 0.75rem;">${emoji}</span>
  1560.                         <span>${text}</span>
  1561.                     </div>
  1562.                     <button class="remove-btn" title="删除此项">×</button>
  1563.                 </div>
  1564.                 `;
  1565.             }).join('');
  1566.         }
  1567.    
  1568.         /**
  1569.          * 更新转盘SVG图形
  1570.          * @description 动态计算扇形区域,处理文本显示
  1571.          * @算法说明:
  1572.          * 1. 计算每个扇形的角度
  1573.          * 2. 使用极坐标计算弧线路径
  1574.          * 3. 根据项目数量调整字体大小
  1575.          */
  1576.         function updateWheel() {
  1577.             const sectionsGroup = document.getElementById('wheelSections');
  1578.             sectionsGroup.innerHTML = '';
  1579.                
  1580.             // 空转盘状态
  1581.             if (punishments.length === 0) {
  1582.                 const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  1583.                 path.setAttribute('d', 'M 0 0 L 240 0 A 240 240 0 1 1 -240 0 Z');
  1584.                 path.setAttribute('fill', 'rgba(255,255,255,0.2)');
  1585.                 path.setAttribute('stroke', '#fff');
  1586.                 path.setAttribute('stroke-width', '4');
  1587.                   
  1588.                 const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  1589.                 text.setAttribute('x', '0');
  1590.                 text.setAttribute('y', '0');
  1591.                 text.setAttribute('fill', '#fff');
  1592.                 text.setAttribute('font-size', '32');
  1593.                 text.setAttribute('font-weight', 'bold');
  1594.                 text.setAttribute('text-anchor', 'middle');
  1595.                 text.setAttribute('dominant-baseline', 'middle');
  1596.                 text.textContent = '请添加菜品';
  1597.                   
  1598.                 sectionsGroup.appendChild(path);
  1599.                 sectionsGroup.appendChild(text);
  1600.                 return;
  1601.             }
  1602.                
  1603.             // 计算每个扇形的角度
  1604.             const sectionAngle = 360 / punishments.length;
  1605.             const radius = 240; // 转盘半径
  1606.    
  1607.             // 生成每个扇形
  1608.             punishments.forEach((punishment, index) => {
  1609.                 // 计算起始和结束角度(弧度制)
  1610.                 const startAngle = index * sectionAngle;
  1611.                 const endAngle = (index + 1) * sectionAngle;
  1612.                   
  1613.                 const startAngleRad = (startAngle * Math.PI) / 180;
  1614.                 const endAngleRad = (endAngle * Math.PI) / 180;
  1615.                   
  1616.                 // 判断是否为大圆弧(角度大于180度)
  1617.                 const largeArcFlag = sectionAngle > 180 ? 1 : 0;
  1618.                   
  1619.                 // 计算弧线起点和终点坐标
  1620.                 const x1 = Math.cos(startAngleRad) * radius;
  1621.                 const y1 = Math.sin(startAngleRad) * radius;
  1622.                 const x2 = Math.cos(endAngleRad) * radius;
  1623.                 const y2 = Math.sin(endAngleRad) * radius;
  1624.                   
  1625.                 // 构建扇形路径数据
  1626.                 const pathData = `M 0 0 L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z`;
  1627.                   
  1628.                 // 创建扇形路径元素
  1629.                 const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  1630.                 path.setAttribute('d', pathData);
  1631.                 path.setAttribute('fill', colors[index % colors.length]);
  1632.                 path.setAttribute('stroke', '#fff');
  1633.                 path.setAttribute('stroke-width', '4');
  1634.                 path.setAttribute('filter', 'url(#shadow)');
  1635.                   
  1636.                 // 计算文本位置(扇形中间)
  1637.                 const midAngle = (startAngle + endAngle) / 2;
  1638.                 const textRadius = radius * 0.65; // 文本位置半径
  1639.                 const textX = Math.cos((midAngle * Math.PI) / 180) * textRadius;
  1640.                 const textY = Math.sin((midAngle * Math.PI) / 180) * textRadius;
  1641.                   
  1642.                 // 根据项目数量调整字体大小
  1643.                 let fontSize = 22;
  1644.                 if (punishments.length > 15) fontSize = 14;
  1645.                 else if (punishments.length > 10) fontSize = 16;
  1646.                 else if (punishments.length > 8) fontSize = 18;
  1647.                   
  1648.                 // 提取emoji和文本
  1649.                 const emojiMatch = punishment.match(/^([\u{1F300}-\u{1F9FF}]|\u{1F1E6}-\u{1F1FF}|\u{2600}-\u{26FF}|\u{2700}-\u{27BF})/u);
  1650.                 const emoji = emojiMatch ? emojiMatch[0] : '';
  1651.                 const text = emojiMatch ? punishment.substring(emojiMatch[0].length).trim() : punishment;
  1652.                  
  1653.                 // 创建文本元素
  1654.                 const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
  1655.                 textElement.setAttribute('x', textX);
  1656.                 textElement.setAttribute('y', textY);
  1657.                 textElement.setAttribute('fill', '#fff');
  1658.                 textElement.setAttribute('font-size', fontSize);
  1659.                 textElement.setAttribute('font-weight', 'bold');
  1660.                 textElement.setAttribute('text-anchor', 'middle');
  1661.                 textElement.setAttribute('dominant-baseline', 'middle');
  1662.                 textElement.setAttribute('transform', `rotate(${midAngle}, ${textX}, ${textY})`);
  1663.                  
  1664.                 // 文本截断处理(长文本显示省略号)
  1665.                 const maxChars = 6;
  1666.                 let displayText = text;
  1667.                 if (text.length > maxChars) {
  1668.                     displayText = text.substring(0, maxChars) + '..';
  1669.                 }
  1670.                  
  1671.                 // 如果有emoji,添加到文本前面
  1672.                 const displayContent = emoji ? `${emoji} ${displayText}` : displayText;
  1673.                 textElement.textContent = displayContent;
  1674.                  
  1675.                 // 添加到SVG
  1676.                 sectionsGroup.appendChild(path);
  1677.                 sectionsGroup.appendChild(textElement);
  1678.             });
  1679.         }
  1680.    
  1681.         /**
  1682.          * 开始转盘游戏
  1683.          * @description 控制转盘旋转并计算最终结果
  1684.          * @算法说明:
  1685.          * 1. 生成随机旋转角度和时间
  1686.          * 2. 使用CSS transition实现动画
  1687.          * 3. 根据指针角度计算选中扇形
  1688.          */
  1689.         function startGame() {
  1690.             // 前置检查
  1691.             if (punishments.length === 0) {
  1692.                 alert('请先添加菜品!');
  1693.                 return;
  1694.             }
  1695.    
  1696.             if (isSpinning) return;
  1697.    
  1698.             // 设置旋转状态
  1699.             isSpinning = true;
  1700.             currentPunishment = '';
  1701.                
  1702.             // 更新按钮状态
  1703.             const startBtn = document.getElementById('startBtn');
  1704.             const startBtnText = document.getElementById('startBtnText');
  1705.             startBtn.disabled = true;
  1706.             startBtnText.textContent = '点餐中...';
  1707.                
  1708.             // 隐藏上一个结果
  1709.             const resultDisplay = document.getElementById('resultDisplay');
  1710.             resultDisplay.classList.remove('show');
  1711.             
  1712.             // 生成随机动画参数
  1713.             const randomDuration = 4000 + Math.random() * 4000; // 4-8秒
  1714.             
  1715.             // 计算最终旋转角度(多圈旋转 + 随机偏移)
  1716.             const randomDegreeOffset = Math.floor(Math.random() * 360);
  1717.             const randomSpins = 5 + Math.floor(Math.random() * 5); // 5-10圈
  1718.             const finalRotation = currentRotation + (randomSpins * 360) + randomDegreeOffset;
  1719.             
  1720.             // 更新当前角度
  1721.             currentRotation = finalRotation;
  1722.             
  1723.             // 应用旋转动画
  1724.             const wheelSvg = document.getElementById('wheelSvg');
  1725.             wheelSvg.style.transitionDuration = `${randomDuration}ms`;
  1726.             wheelSvg.style.transform = `rotate(${currentRotation}deg)`;
  1727.             
  1728.             // 动画结束后计算结果
  1729.             setTimeout(() => {
  1730.                 isSpinning = false;
  1731.                  
  1732.                 // 转盘指针固定角度(正上方为270度)
  1733.                 const POINTER_ANGLE = 270;
  1734.                  
  1735.                 // 计算实际旋转角度(归一化到0-360度)
  1736.                 const actualRotation = currentRotation % 360;
  1737.                  
  1738.                 // 计算指针指向的角度
  1739.                 let winningAngle = (POINTER_ANGLE - actualRotation);
  1740.                  
  1741.                 // 角度归一化处理
  1742.                 winningAngle = winningAngle % 360;
  1743.                 if (winningAngle < 0) {
  1744.                     winningAngle += 360;
  1745.                 }
  1746.                  
  1747.                 // 根据角度计算选中扇形的索引
  1748.                 const sectionAngle = 360 / punishments.length;
  1749.                 const winningIndex = Math.floor(winningAngle / sectionAngle);
  1750.                  
  1751.                 // 获取选中的菜品(边界情况处理)
  1752.                 currentPunishment = punishments[winningIndex] || punishments[0];
  1753.                  
  1754.                 // 恢复按钮状态
  1755.                 startBtn.disabled = false;
  1756.                 startBtnText.textContent = '再次点餐';
  1757.                  
  1758.                 // 显示结果
  1759.                 const resultEmoji = document.querySelector('#resultDisplay .result-emoji');
  1760.                 resultEmoji.textContent = '&#127881;';
  1761.                 document.getElementById('winnerPunishment').textContent = currentPunishment;
  1762.                 resultDisplay.classList.add('show');
  1763.                  
  1764.                 // 更新列表高亮
  1765.                 updatePunishmentList();
  1766.                  
  1767.                 // 显示庆祝弹窗
  1768.                 showCelebration();
  1769.             }, randomDuration);
  1770.         }
  1771.    
  1772.         /**
  1773.          * 重置转盘
  1774.          * @description 将转盘归零,清除结果
  1775.          */
  1776.         function resetWheel() {
  1777.             if (isSpinning) {
  1778.                 alert('转盘旋转时不能重置!');
  1779.                 return;
  1780.             }
  1781.             
  1782.             if (!confirm('确定要重置转盘吗?')) return;
  1783.                
  1784.             currentRotation = 0;
  1785.             currentPunishment = '';
  1786.                
  1787.             // 立即重置转盘(无动画)
  1788.             const wheelSvg = document.getElementById('wheelSvg');
  1789.             wheelSvg.style.transitionDuration = '0ms';
  1790.             wheelSvg.style.transform = 'rotate(0deg)';
  1791.                
  1792.             // 隐藏结果
  1793.             const resultDisplay = document.getElementById('resultDisplay');
  1794.             const resultEmoji = document.querySelector('#resultDisplay .result-emoji');
  1795.             resultEmoji.textContent = '&#129300;';
  1796.             resultDisplay.classList.remove('show');
  1797.             
  1798.             // 更新UI
  1799.             updatePunishmentList();
  1800.             closeCelebration();
  1801.         }
  1802.    
  1803.         /**
  1804.          * 显示庆祝弹窗
  1805.          * @description 展示选中的菜品,带有动画效果
  1806.          */
  1807.         function showCelebration() {
  1808.             const modal = document.getElementById('celebrationModal');
  1809.             document.getElementById('celebrationPunishment').textContent = currentPunishment;
  1810.             
  1811.             // 显示并添加动画
  1812.             modal.classList.remove('hidden');
  1813.             const content = modal.querySelector('.celebration-content');
  1814.             content.classList.remove('modal-enter');
  1815.             void content.offsetWidth; // 触发重排以重新播放动画
  1816.             content.classList.add('modal-enter');
  1817.         }
  1818.    
  1819.         /**
  1820.          * 关闭庆祝弹窗
  1821.          */
  1822.         function closeCelebration() {
  1823.             document.getElementById('celebrationModal').classList.add('hidden');
  1824.         }
  1825.          
  1826.         // TODO: 添加导出菜品列表功能
  1827.         // TODO: 添加转盘音效支持
  1828.         // TODO: 添加点餐历史记录功能
  1829.         // OPTIMIZE: 考虑使用Vue/React重构以提高可维护性
  1830.     </script>
  1831. </body>
  1832. </html>
复制代码


回复

您需要登录后才可以回帖 登录 | 立即注册 微信登录

本版积分规则

您需要 登录 后才可以回复,轻松玩转社区,没有帐号?立即注册
快速回复
关灯 在本版发帖
扫一扫添加微信客服
QQ客服返回顶部
快速回复 返回顶部 返回列表