详细介绍:舒尔特方格开源

代码部分,全部在这里,直接用就行

<!DOCTYPE html>
    <html lang="zh-CN">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Schulte 方格</title>
      <!-- React 18 UMD -->
      <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
      <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
        <!-- Babel for in-browser JSX transform -->
        <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
          <!-- TailwindCSS (CDN) -->
            <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" />
          </head>
            <body class="bg-neutral-100">
          <div id="root"></div>
              <script type="text/babel">
              const { useEffect, useMemo, useRef, useState } = React;
              function SchulteGridApp() {
              const sizeOptions = [3,4,5,6,7,8,9,10];
              const [size, setSize] = useState(5);
              const [numbers, setNumbers] = useState([]);
              const [target, setTarget] = useState(1);
              const [running, setRunning] = useState(false);
              const [elapsed, setElapsed] = useState(0);
              const [mistakeId, setMistakeId] = useState(null);
              const [finishedAt, setFinishedAt] = useState(null);
              const timerRef = useRef(null);
              const startTsRef = useRef(null);
              const pausedOffsetRef = useRef(0);
              const total = useMemo(() => size * size, [size]);
              const bestKey = useMemo(() => "schulte_best_" + size, [size]);
              const bestMs = useMemo(() => {
              const v = localStorage.getItem(bestKey);
              return v ? parseInt(v) : null;
              }, [bestKey]);
              const format = (ms) => {
              if (ms == null) return "--:--.--";
              const s = Math.floor(ms / 1000);
              const cs = Math.floor((ms % 1000) / 10);
              const m = Math.floor(s / 60);
              const s2 = s % 60;
              return String(m).padStart(2,"0") + ":" + String(s2).padStart(2,"0") + "." + String(cs).padStart(2,"0");
              };
              const shuffleNew = (n) => {
              const arr = Array.from({length: n}, (_,i) => i+1);
              for (let i = arr.length - 1; i > 0; i--) {
              const j = Math.floor(Math.random() * (i + 1));
              const tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
              }
              return arr;
              };
              const reset = (keepSize = true) => {
              const newSize = keepSize ? size : 5;
              const n = newSize * newSize;
              setNumbers(shuffleNew(n));
              setTarget(1);
              setRunning(false);
              setElapsed(0);
              setMistakeId(null);
              setFinishedAt(null);
              pausedOffsetRef.current = 0;
              startTsRef.current = null;
              if (timerRef.current) {
              clearInterval(timerRef.current);
              timerRef.current = null;
              }
              };
              useEffect(() => { reset(true); }, [size]);
              useEffect(() => {
              if (!running) return;
              if (timerRef.current) return;
              timerRef.current = setInterval(() => {
              if (startTsRef.current != null) {
              setElapsed(Date.now() - startTsRef.current + pausedOffsetRef.current);
              }
              }, 16);
              return () => {
              if (timerRef.current) {
              clearInterval(timerRef.current);
              timerRef.current = null;
              }
              };
              }, [running]);
              const handleCellClick = (num) => {
              if (!running && target === 1) {
              startTsRef.current = Date.now();
              setRunning(true);
              }
              if (num === target) {
              const next = target + 1;
              setTarget(next);
              setMistakeId(null);
              if (next > total) {
              const finalMs = Date.now() - (startTsRef.current ?? Date.now()) + pausedOffsetRef.current;
              setRunning(false);
              setFinishedAt(finalMs);
              setElapsed(finalMs);
              if (timerRef.current) {
              clearInterval(timerRef.current);
              timerRef.current = null;
              }
              if (bestMs == null || finalMs < bestMs) {
              localStorage.setItem(bestKey, String(finalMs));
              }
              }
              } else {
              setMistakeId(num);
              try { if (navigator.vibrate) navigator.vibrate(40); } catch(e) {}
              }
              };
              const pause = () => {
              if (!running) return;
              setRunning(false);
              if (timerRef.current) {
              clearInterval(timerRef.current);
              timerRef.current = null;
              }
              if (startTsRef.current != null) {
              pausedOffsetRef.current = elapsed;
              }
              };
              const resume = () => {
              if (running) return;
              setRunning(true);
              startTsRef.current = Date.now();
              };
              const gridTemplate = useMemo(() => ({
              gridTemplateColumns: "repeat(" + size + ", 1fr)",
              }), [size]);
              const cellSizeClass = useMemo(() => {
              if (size <= 3) return "text-4xl";
              if (size === 4) return "text-3xl";
              if (size === 5) return "text-2xl";
              if (size <= 7) return "text-xl";
              if (size <= 9) return "text-lg";
              return "text-base";
              }, [size]);
              return (
              <div className="min-h-screen w-full bg-neutral-50 text-neutral-900 dark:bg-neutral-950 dark:text-neutral-50 flex items-center justify-center p-4">
                <div className="w-full max-w-5xl">
                  <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-4">
                    <div>
                      <h1 className="text-2xl font-bold tracking-tight">Schulte 方格</h1>
                        <p className="text-sm text-neutral-500">点击 1{total},锻炼专注与视野。首次点击开始计时。</p>
                          </div>
                            <div className="flex flex-wrap items-center gap-2">
                              <label className="text-sm">尺寸</label>
                                <select
                                className="px-3 py-2 rounded-xl border border-neutral-300 bg-white text-sm dark:bg-neutral-900 dark:border-neutral-700"
                                value={size}
                                onChange={(e) => setSize(parseInt(e.target.value))}
                                >
                                {sizeOptions.map((s) => (
                                <option key={s} value={s}>{s} × {s}{s * s}</option>
                                  ))}
                                  </select>
                                    <div className="flex items-baseline gap-2">
                                      <span className="text-sm text-neutral-500">用时</span>
                                        <span className="font-mono text-xl tabular-nums">{format(elapsed)}</span>
                                          </div>
                                            <div className="flex items-baseline gap-2">
                                              <span className="text-sm text-neutral-500">最佳({size}×{size})</span>
                                                <span className="font-mono text-lg tabular-nums">{format(bestMs)}</span>
                                                  </div>
                                                    </div>
                                                      </div>
                                                        <div className="flex flex-wrap items-center gap-2 mb-4">
                                                          {!running && target === 1 ? (
                                                          <button
                                                          onClick={resume}
                                                          className="px-4 py-2 rounded-2xl bg-black text-white text-sm font-semibold shadow-sm active:scale-[0.98] dark:bg-white dark:text-black"
                                                          >
                                                          开始
                                                          </button>
                                                            ) : (
                                                            <>
                                                              {running ? (
                                                              <button
                                                              onClick={pause}
                                                              className="px-4 py-2 rounded-2xl bg-neutral-900 text-white text-sm font-semibold shadow-sm active:scale-[0.98] dark:bg-neutral-100 dark:text-black"
                                                              >暂停</button>
                                                                ) : (
                                                                <button
                                                                onClick={resume}
                                                                className="px-4 py-2 rounded-2xl bg-neutral-900 text-white text-sm font-semibold shadow-sm active:scale-[0.98] dark:bg-neutral-100 dark:text-black"
                                                                >继续</button>
                                                                  )}
                                                                  <button
                                                                  onClick={() => reset(true)}
                                                                  className="px-4 py-2 rounded-2xl bg-white border text-sm font-semibold shadow-sm active:scale-[0.98] dark:bg-neutral-900 dark:border-neutral-700"
                                                                  >重置</button>
                                                                    <button
                                                                    onClick={() => setNumbers(shuffleNew(total))}
                                                                    className="px-4 py-2 rounded-2xl bg-white border text-sm font-semibold shadow-sm active:scale-[0.98] dark:bg-neutral-900 dark:border-neutral-700"
                                                                    >重新打乱</button>
                                                                      </>
                                                                        )}
                                                                        </div>
                                                                          <div
                                                                          className="grid gap-2 rounded-3xl p-3 bg-white/70 backdrop-blur border border-neutral-200 shadow-sm dark:bg-neutral-900/60 dark:border-neutral-800"
                                                                          style={gridTemplate}
                                                                          >
                                                                          {numbers.map((num) => {
                                                                          const isDone = num < target;
                                                                          const isMistake = mistakeId === num;
                                                                          return (
                                                                          <button
                                                                          key={num}
                                                                          onClick={() => handleCellClick(num)}
                                                                          className={[
                                                                          "relative select-none aspect-square rounded-xl border text-center font-semibold transition-all duration-150 flex items-center justify-center",
                                                                          "bg-neutral-50 dark:bg-neutral-900",
                                                                          "border-neutral-200 dark:border-neutral-800",
                                                                          isDone && "opacity-60",
                                                                          isMistake && "animate-shake border-red-400 ring-2 ring-red-400",
                                                                          cellSizeClass,
                                                                          ].filter(Boolean).join(" ")}
                                                                          aria-label={"数字 " + num}
                                                                          >
                                                                          <span className="tabular-nums">{num}</span>
                                                                            </button>
                                                                              );
                                                                              })}
                                                                              </div>
                                                                                {finishedAt != null && (
                                                                                <div className="mt-4 p-4 rounded-2xl border bg-white shadow-sm dark:bg-neutral-900 dark:border-neutral-800">
                                                                                  <div className="flex flex-wrap items-center justify-between gap-3">
                                                                                    <div className="flex items-baseline gap-3">
                                                                                      <span className="text-sm text-neutral-500">本次用时</span>
                                                                                        <span className="font-mono text-2xl tabular-nums">{format(finishedAt)}</span>
                                                                                          {bestMs != null && finishedAt <= bestMs && (
                                                                                          <span className="text-xs rounded-full px-2 py-1 bg-emerald-600 text-white">新纪录!</span>
                                                                                            )}
                                                                                            </div>
                                                                                              <div className="flex gap-2">
                                                                                                <button
                                                                                                onClick={() => reset(true)}
                                                                                                className="px-4 py-2 rounded-2xl bg-black text-white text-sm font-semibold shadow-sm active:scale-[0.98] dark:bg-white dark:text-black"
                                                                                                >再来一局</button>
                                                                                                  <button
                                                                                                  onClick={() => {
                                                                                                  localStorage.removeItem(bestKey);
                                                                                                  setFinishedAt((v) => (v == null ? 0 : v - 1));
                                                                                                  }}
                                                                                                  className="px-4 py-2 rounded-2xl bg-white border text-sm font-semibold shadow-sm active:scale-[0.98] dark:bg-neutral-900 dark:border-neutral-700"
                                                                                                  >清除本尺寸最佳</button>
                                                                                                    </div>
                                                                                                      </div>
                                                                                                        </div>
                                                                                                          )}
                                                                                                          </div>
                                                                                                            <style>{`
                                                                                                              @keyframes shake { 10%, 90% { transform: translateX(-1px); } 20%, 80% { transform: translateX(2px); } 30%, 50%, 70% { transform: translateX(-4px);} 40%, 60% { transform: translateX(4px);} }
                                                                                                              .animate-shake { animation: shake 0.4s both; }
                                                                                                              `}</style>
                                                                                                                </div>
                                                                                                                  );
                                                                                                                  }
                                                                                                                  const root = ReactDOM.createRoot(document.getElementById('root'));
                                                                                                                  root.render(<SchulteGridApp />);
                                                                                                                  </script>
                                                                                                                </body>
                                                                                                              </html>
posted @ 2025-11-04 17:43  ycfenxi  阅读(2)  评论(0)    收藏  举报