个人量化网站 求大神指点

閪人 2026-09-13 16:43 1

最近想给自己做一个长期使用的个人量化网站,目前还是单 HTML 原型,主要目的不是普通的“每日打卡”,而是把行为记录下来,长期汇总后可以丢给 AI 分析。


目前包含:


每月日历 / 每日状态

晨起体重 + 长期趋势

运动原始数据记录,不是简单的“运动 ✓”

不同运动有不同达标规则,例如骑行累计 30min 才算达标

运动允许恢复日,并且连续 14 天无达标训练才判定本轮中断

饮食拆成蛋白质、蔬菜、水果、饮水、主食、高油高糖等指标

睡眠时长

一些个人自控事件的正计时 / 重置

独立倒数日、正计时页面

14/30/90 天数据汇总

可生成 Markdown / JSON 给 AI 做长期分析


目前主要还是在琢磨产品逻辑,感觉继续自己堆功能很容易变成“大杂烩”。


想重点请大家喷几个地方:


首页的信息层级是不是太多?

“原始行为记录 → 自动判定达标 → 周期汇总”的思路有没有明显问题?

数据库以后应该怎样设计,方便保留几年数据以及重新计算规则?

正计时、倒数日这种功能应该独立出来还是继续整合?

如果以后让 AI 根据历史数据给建议,应该保存哪些原始字段,哪些数据其实没必要?



<!doctype html>

<html lang="zh-CN">

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width,initial-scale=1">

<title>Daily Control V4 · 个人量化打卡</title>

<style>

:root{

--bg:#0a0e17;--panel:#111725;--panel2:#0d1320;--line:#202a3c;--line2:#2b3850;

--text:#f4f7fb;--muted:#8d98aa;--soft:#cbd4df;--accent:#7bddc3;--accent2:#6fa8ff;

--ok:#7ae6a1;--warn:#f1c36b;--bad:#ff7e89;--violet:#c69cff;

}

*{box-sizing:border-box} html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text)}

body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}

button,input,select,textarea{font:inherit}button{cursor:pointer}

.app{max-width:1480px;margin:auto;padding:20px}

.nav{

display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:18px;

position:sticky;top:0;z-index:10;padding:10px 0;background:linear-gradient(var(--bg) 75%,transparent)

}

.brand{font-weight:900;letter-spacing:-.03em;margin-right:14px}.brand span{color:var(--accent)}

.navbtn{border:1px solid var(--line);background:#111827;color:var(--muted);padding:9px 13px;border-radius:11px}

.navbtn.active{color:var(--text);border-color:rgba(123,221,195,.4);background:rgba(123,221,195,.08)}

.page{display:none}.page.active{display:block}

.layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:18px}

.sidebar{display:grid;gap:16px;align-self:start;position:sticky;top:72px}

.card{border:1px solid var(--line);background:linear-gradient(180deg,rgba(255,255,255,.022),rgba(255,255,255,.004)),var(--panel);border-radius:20px}

.pad{padding:18px}.eyebrow{font-size:11px;letter-spacing:.16em;color:var(--accent);font-weight:850}.bigdate{font-size:25px;font-weight:850;letter-spacing:-.03em;margin-top:6px}.muted{color:var(--muted)}

.section-head,.row,.topbar{display:flex;justify-content:space-between;align-items:center;gap:12px}.section-head{margin-bottom:12px}.section-head h2{font-size:15px;margin:0}.section-head small{color:var(--muted)}

.cal-head{display:flex;justify-content:space-between;align-items:center;margin:18px 0 12px}.calendar{display:grid;grid-template-columns:repeat(7,1fr);gap:7px}.weekday{font-size:11px;color:var(--muted);text-align:center}.day{height:42px;display:grid;place-items:center;position:relative}.day span{width:31px;height:31px;border-radius:50%;display:grid;place-items:center;font-size:13px}.day.today span{border:2px solid var(--accent)}.day.good:after,.day.partial:after,.day.rest:after,.day.event:after{content:"";position:absolute;bottom:1px;width:5px;height:5px;border-radius:50%}.day.good:after{background:var(--ok)}.day.partial:after{background:var(--warn)}.day.rest:after{background:var(--accent2)}.day.event:after{background:var(--bad)}

.weightrow{display:grid;grid-template-columns:1fr auto;gap:8px}.input,.select,textarea{width:100%;background:#0b111d;color:var(--text);border:1px solid var(--line2);border-radius:11px;padding:10px;outline:none}

.primary,.secondary,.danger{border-radius:11px;padding:9px 13px;font-weight:800}.primary{border:0;background:linear-gradient(90deg,var(--accent),var(--accent2));color:#07131d}.secondary{border:1px solid var(--line2);background:#141c2c;color:var(--soft)}.danger{border:1px solid rgba(255,126,137,.35);background:rgba(255,126,137,.08);color:#ffc4ca}

.topbar{align-items:flex-start;margin:3px 0 18px}.topbar h1{font-size:31px;margin:6px 0 4px;letter-spacing:-.04em}.topbar p{margin:0;color:var(--muted);font-size:13px}

.hero{display:grid;grid-template-columns:1.3fr .7fr;gap:16px;margin-bottom:16px}.hero .card{padding:20px}.score{font-size:54px;font-weight:900;letter-spacing:-.06em;line-height:1}.progress{height:9px;background:#0a101a;border:1px solid var(--line);border-radius:99px;overflow:hidden;margin-top:15px}.progress i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));width:0}

.main-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:16px}.stack{display:grid;gap:16px}

.item{padding:15px;border:1px solid var(--line);background:var(--panel2);border-radius:16px}.item + .item{margin-top:9px}.item-title{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.item-title b{font-size:14px}.item-meta{font-size:11px;color:var(--muted);margin-top:4px;line-height:1.55}.status{font-size:10px;padding:6px 9px;border-radius:999px;border:1px solid var(--line2);color:var(--muted);white-space:nowrap}.status.ok{color:#dcffec;border-color:rgba(122,230,161,.35);background:rgba(122,230,161,.07)}.status.warn{color:#ffe6b0;border-color:rgba(241,195,107,.35);background:rgba(241,195,107,.07)}.status.bad{color:#ffc3ca;border-color:rgba(255,126,137,.35);background:rgba(255,126,137,.07)}

.counter{font-size:25px;font-weight:850;letter-spacing:-.03em;margin:9px 0 4px}.mini-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}.mini-actions button{font-size:11px}

.metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:12px}.metric{padding:11px;border:1px solid var(--line);border-radius:12px;background:#0e1523}.metric b{font-size:19px;display:block}.metric span{font-size:10px;color:var(--muted)}

.diet-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:10px}.diet-toggle{border:1px solid var(--line);background:#121a2a;color:var(--muted);border-radius:11px;padding:10px;text-align:left}.diet-toggle.on{border-color:rgba(122,230,161,.35);background:rgba(122,230,161,.07);color:#e5fff0}

.goal{padding:12px;border:1px solid var(--line);border-radius:13px;background:var(--panel2);margin-top:9px}.bar{height:8px;background:#0a101a;border:1px solid var(--line);border-radius:99px;overflow:hidden;margin-top:8px}.bar i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2))}

.subnav-cards{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.linkcard{border:1px solid var(--line);background:var(--panel2);border-radius:15px;padding:14px;text-align:left;color:var(--text)}.linkcard b{display:block}.linkcard small{display:block;color:var(--muted);margin-top:5px;line-height:1.5}

.chart{height:220px;border:1px solid var(--line);background:#0b111d;border-radius:14px;padding:8px}svg{width:100%;height:100%}

.toolbar{display:flex;gap:8px;flex-wrap:wrap}.rangebtn{border:1px solid var(--line);background:#121a2a;color:var(--muted);border-radius:9px;padding:6px 9px;font-size:11px}.rangebtn.active{border-color:var(--accent);color:var(--text)}

.report{min-height:300px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6}

.count-list,.timer-list{display:grid;gap:10px}.count-card,.timer-card{padding:15px;border:1px solid var(--line);background:var(--panel2);border-radius:15px}.count-days,.timer-value{font-size:28px;font-weight:900;letter-spacing:-.04em;margin-top:7px}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.field label{display:block;font-size:10px;color:var(--muted);margin-bottom:5px}.full{grid-column:1/-1}

.modal{position:fixed;inset:0;display:none;place-items:center;padding:18px;background:rgba(2,6,14,.76);z-index:50}.modal.open{display:grid}.modalbox{width:min(560px,100%);max-height:90vh;overflow:auto;background:#111827;border:1px solid var(--line2);border-radius:18px;padding:18px}.modalbox h3{margin:0 0 5px}.modalbox p{margin:0;color:var(--muted);font-size:11px}.types{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:12px}.typebtn{border:1px solid var(--line);background:#0d1422;color:var(--text);border-radius:12px;padding:11px}.typebtn.active{border-color:var(--accent);background:rgba(123,221,195,.07)}.rulebox{margin-top:10px;padding:10px;border:1px solid var(--line);border-radius:11px;background:#0d1422;color:var(--soft);font-size:11px;line-height:1.5}

.toast{position:fixed;right:20px;bottom:20px;background:#151e30;border:1px solid var(--line2);border-radius:11px;padding:10px 12px;opacity:0;transform:translateY(10px);transition:.2s;z-index:80}.toast.show{opacity:1;transform:none}

@media(max-width:1050px){.layout{grid-template-columns:290px minmax(0,1fr)}.main-grid,.hero{grid-template-columns:1fr}}

@media(max-width:760px){.app{padding:12px}.layout{grid-template-columns:1fr}.sidebar{position:static}.topbar{flex-direction:column}.metrics,.subnav-cards{grid-template-columns:1fr 1fr}.form-grid{grid-template-columns:1fr}.full{grid-column:auto}.types{grid-template-columns:repeat(2,1fr)}}

</style>

</head>

<body>

<div class="app" id="app">

<nav class="nav">

<div class="brand">Daily <span>Control</span></div>

<button class="navbtn active" data-page="checkin">打卡</button>

<button class="navbtn" data-page="summary">数据汇总</button>

<button class="navbtn" data-page="countdown">倒数日</button>

<button class="navbtn" data-page="timers">正计时</button>

<button class="navbtn" data-page="settings">设置</button>

</nav>


<section class="page active" id="page-checkin">

<div class="layout">

<aside class="sidebar">

<div class="card pad">

<div class="eyebrow">TODAY</div>

<div class="bigdate" id="bigDate"></div>

<div class="muted" style="font-size:11px;margin-top:4px">主页面:今天做了什么,一眼看完</div>

<div class="cal-head"><b id="monthTitle"></b><small class="muted">当月</small></div>

<div class="calendar" id="calendar"></div>

<div class="muted" style="font-size:10px;margin-top:12px;line-height:1.6">绿=整体达标 · 黄=部分完成 · 蓝=恢复日 · 红=发生重置事件</div>

</div>

<div class="card pad">

<div class="section-head"><h2>晨起体重</h2><small>每日一次</small></div>

<div class="weightrow">

<input class="input" id="weightInput" type="number" step="0.1" placeholder="60.0">

<button class="primary" id="saveWeight">记录</button>

</div>

<div class="item-meta" id="weightHint">起床后、上厕所后、吃喝前;长期趋势比单日数字更重要。</div>

</div>

</aside>


  <main>
<div class="topbar">
<div>
<div class="eyebrow">PERSONAL CONTROL PANEL</div>
<h1>今天,按真实数据算。</h1>
<p>不是手动把所有东西勾绿,而是记录事实后由系统判断。</p>
</div>
<button class="secondary" data-go="summary">生成 AI 汇总</button>
</div>

<div class="hero">
<div class="card">
<div class="row">
<div><div class="muted" style="font-size:11px">今日完成度</div><div class="score"><span id="doneScore">3</span>/6</div></div>
<div style="text-align:right"><b id="scorePct">50%</b><div class="muted" style="font-size:10px;margin-top:4px">自动计算</div></div>
</div>
<div class="progress"><i id="scoreBar"></i></div>
</div>
<div class="card">
<div class="muted" style="font-size:11px">今天最值得盯的</div>
<div style="font-size:20px;font-weight:800;line-height:1.45;margin-top:6px" id="focusText">运动还没达标,但距离“断档”还有时间。</div>
</div>
</div>

<div class="main-grid">
<div class="stack">
<div class="card pad">
<div class="section-head"><h2>🏋️ 运动</h2><small>两周宽限,不搞“每天刷一下”</small></div>
<div class="item">
<div class="item-title">
<div><b>长期坚持</b><div class="item-meta">超过 <span id="exerciseGapRule">14</span> 天没有“达标训练”,这一轮才断。</div></div>
<span class="status ok" id="exerciseStreakStatus">进行中</span>
</div>
<div class="counter" id="exerciseStreak">12天 08:21:12</div>
<div class="item-meta" id="exerciseGapText">距离断档还剩 9 天 · 最近一次达标训练 5 天前</div>
</div>
<div class="item">
<div class="item-title">
<div><b>今日训练</b><div class="item-meta" id="exerciseTodayMeta">骑行 10 min · 2.1 km · 有活动但未达标</div></div>
<span class="status warn" id="exerciseTodayStatus">未达标</span>
</div>
<div class="metrics">
<div class="metric"><b id="todayCardio">10</b><span>有氧 min</span></div>
<div class="metric"><b id="todaySets">0</b><span>有效力量组</span></div>
<div class="metric"><b id="todayDistance">2.1</b><span>距离 km</span></div>
</div>
<div class="mini-actions"><button class="primary" id="addWorkout">+ 记录训练</button><button class="secondary" id="setRest">今天是恢复日</button></div>
</div>
</div>

<div class="card pad">
<div class="section-head"><h2>🍚 饮食</h2><small>不是“吃得健康 ✓”,拆成真实项目</small></div>
<div class="diet-grid" id="dietGrid">
<button class="diet-toggle on" data-diet="protein">蛋白质达标</button>
<button class="diet-toggle on" data-diet="veg">蔬菜 ≥ 2种</button>
<button class="diet-toggle" data-diet="fruit">水果</button>
<button class="diet-toggle on" data-diet="water">饮水 ≥ 2L</button>
<button class="diet-toggle" data-diet="junk">高油/高糖过量</button>
<button class="diet-toggle on" data-diet="staple">主食结构正常</button>
</div>
<div class="row" style="margin-top:12px">
<div><b id="dietScore">4/5</b><div class="item-meta">“高油/高糖过量”是扣分项,不是完成项。</div></div>
<span class="status ok" id="dietStatus">今天不错</span>
</div>
</div>

<div class="card pad">
<div class="section-head"><h2>😴 睡眠</h2><small>记录小时,不直接打勾</small></div>
<div class="weightrow">
<input class="input" id="sleepInput" type="number" step="0.1" min="0" max="24" placeholder="7.0">
<button class="primary" id="saveSleep">记录</button>
</div>
<div class="item-meta" id="sleepMeta">目标 ≥ 7 小时。</div>
</div>
</div>

<div class="stack">
<div class="card pad">
<div class="section-head"><h2>🧠 自控</h2><small>发生事件才重置</small></div>
<div class="item">
<div class="item-title">
<div><b>无自慰</b><div class="item-meta">从最近一次记录“发生”后开始计时。</div></div><span class="status ok" id="mastStatus">进行中</span>
</div>
<div class="counter" id="mastCounter">4天 16:10:08</div>
<div class="mini-actions"><button class="danger" id="mastReset">记录发生</button></div>
</div>
<div class="item">
<div class="item-title">
<div><b>无色情内容</b><div class="item-meta">与自慰分开统计,避免混在一起看不出问题。</div></div><span class="status ok">进行中</span>
</div>
<div class="counter" id="pornCounter">6天 03:22:40</div>
<div class="mini-actions"><button class="danger" id="pornReset">记录发生</button></div>
</div>
</div>

<div class="card pad">
<div class="section-head"><h2>📊 本周训练量</h2><small>比“连续多少天运动”更有意义</small></div>
<div class="goal"><div class="row"><b>有氧</b><span class="muted"><b id="weekCardio">92</b> / 150 min</span></div><div class="bar"><i id="weekCardioBar"></i></div></div>
<div class="goal"><div class="row"><b>力量训练</b><span class="muted"><b id="weekStrength">2</b> / 3 次</span></div><div class="bar"><i id="weekStrengthBar"></i></div></div>
<div class="goal"><div class="row"><b>有效力量组</b><span class="muted"><b id="weekSets">14</b> / 18 组</span></div><div class="bar"><i id="weekSetsBar"></i></div></div>
</div>

<div class="card pad">
<div class="section-head"><h2>⏱ 时间工具</h2><small>独立二级页面</small></div>
<div class="subnav-cards">
<button class="linkcard" data-go="countdown"><b>倒数日 →</b><small>例如:考试、见面、旅行、目标日期。</small></button>
<button class="linkcard" data-go="timers"><b>正计时 →</b><small>例如:坚持运动多久、项目运行多久。</small></button>
</div>
</div>
</div>
</div>
</main>
</div>

</section>


<section class="page" id="page-summary">

<div class="topbar">

<div><div class="eyebrow">DATA SUMMARY</div><h1>给 AI 看的,不要只给一堆 ✓。</h1><p>把最近一段时间的原始行为、趋势和中断事件汇总成可直接分析的数据。</p></div>

<div class="toolbar"><button class="rangebtn active" data-range="14">14天</button><button class="rangebtn" data-range="30">30天</button><button class="rangebtn" data-range="90">90天</button></div>

</div>

<div class="hero">

<div class="card pad"><div class="section-head"><h2>体重趋势</h2><small id="weightRangeLabel">最近30天</small></div><div class="chart"><svg id="weightChart" viewBox="0 0 760 220" preserveAspectRatio="none"></svg></div></div>

<div class="card pad">

<div class="section-head"><h2>关键摘要</h2><small>自动生成</small></div>

<div class="metrics">

<div class="metric"><b id="sumExercise">3</b><span>达标训练</span></div>

<div class="metric"><b id="sumDiet">82%</b><span>饮食平均</span></div>

<div class="metric"><b id="sumSleep">6.8h</b><span>平均睡眠</span></div>

</div>

<div class="metrics">

<div class="metric"><b id="sumWeight">-0.6</b><span>体重变化 kg</span></div>

<div class="metric"><b id="sumMast">1</b><span>自慰事件</span></div>

<div class="metric"><b id="sumPorn">0</b><span>色情内容事件</span></div>

</div>

</div>

</div>

<div class="main-grid">

<div class="card pad">

<div class="section-head"><h2>AI 分析包</h2><small>Markdown,可直接丢给 AI</small></div>

<textarea class="report" id="aiReport" readonly></textarea>

<div class="mini-actions"><button class="primary" id="refreshReport">重新生成</button><button class="secondary" id="downloadReport">下载 .txt</button><button class="secondary" id="downloadJson">下载 JSON</button></div>

</div>

<div class="card pad">

<div class="section-head"><h2>建议让 AI 看什么</h2><small>减少空泛建议</small></div>

<div class="item"><b>运动</b><div class="item-meta">看训练频率、达标率、连续空窗天数,而不是只看“打卡次数”。</div></div>

<div class="item"><b>饮食</b><div class="item-meta">看各子项目完成率,以及高油高糖事件是否和体重趋势相关。</div></div>

<div class="item"><b>睡眠</b><div class="item-meta">看低睡眠日之后,运动与饮食完成率是否下降。</div></div>

<div class="item"><b>自控</b><div class="item-meta">只记录事件与间隔,避免做道德判断;让 AI 找触发条件和时间规律。</div></div>

</div>

</div>

</section>


<section class="page" id="page-countdown">

<div class="topbar"><div><div class="eyebrow">COUNTDOWN</div><h1>倒数日</h1><p>像博客二级菜单一样独立,不挤主打卡页面。</p></div></div>

<div class="main-grid">

<div class="card pad">

<div class="section-head"><h2>新增倒数日</h2><small>本地保存</small></div>

<div class="form-grid">

<div class="field"><label>名称</label><input class="input" id="countName" placeholder="例如:秋分前见面"></div>

<div class="field"><label>目标日期</label><input class="input" id="countDate" type="date"></div>

<div class="field full"><button class="primary" id="addCountdown">新增</button></div>

</div>

</div>

<div class="card pad"><div class="section-head"><h2>我的倒数日</h2><small>自动更新</small></div><div class="count-list" id="countList"></div></div>

</div>

</section>


<section class="page" id="page-timers">

<div class="topbar"><div><div class="eyebrow">COUNT UP</div><h1>正计时</h1><p>用于“已经坚持多久”“项目运行多久”等长期计时。</p></div></div>

<div class="main-grid">

<div class="card pad">

<div class="section-head"><h2>新增正计时</h2><small>自定义起点</small></div>

<div class="form-grid">

<div class="field"><label>名称</label><input class="input" id="timerName" placeholder="例如:坚持规律运动"></div>

<div class="field"><label>开始时间</label><input class="input" id="timerStart" type="datetime-local"></div>

<div class="field full"><button class="primary" id="addTimer">新增</button></div>

</div>

</div>

<div class="card pad"><div class="section-head"><h2>正在计时</h2><small>实时跳秒</small></div><div class="timer-list" id="timerList"></div></div>

</div>

</section>


<section class="page" id="page-settings">

<div class="topbar"><div><div class="eyebrow">SETTINGS</div><h1>规则设置</h1><p>规则最好能改,不要以后每改一次标准都去改代码。</p></div></div>

<div class="main-grid">

<div class="card pad">

<div class="section-head"><h2>运动规则</h2><small>默认值</small></div>

<div class="form-grid">

<div class="field"><label>多久没有达标训练算断档(天)</label><input class="input" id="gapDays" type="number" min="1" max="60" value="14"></div>

<div class="field"><label>骑行达标(分钟)</label><input class="input" id="ruleCycling" type="number" value="30"></div>

<div class="field"><label>跑步达标(分钟)</label><input class="input" id="ruleRunning" type="number" value="25"></div>

<div class="field"><label>快走达标(分钟)</label><input class="input" id="ruleWalking" type="number" value="40"></div>

<div class="field"><label>爬楼达标(分钟)</label><input class="input" id="ruleStairs" type="number" value="15"></div>

<div class="field"><label>力量达标(分钟)</label><input class="input" id="ruleStrengthMin" type="number" value="20"></div>

<div class="field"><label>力量有效组(组)</label><input class="input" id="ruleStrengthSets" type="number" value="6"></div>

<div class="field full"><button class="primary" id="saveRules">保存规则</button></div>

</div>

</div>

<div class="card pad">

<div class="section-head"><h2>数据设计</h2><small>正式后端建议</small></div>

<div class="item"><b>事件表</b><div class="item-meta">运动、自慰、色情内容、恢复日,都记录时间戳,不覆盖历史。</div></div>

<div class="item"><b>每日快照</b><div class="item-meta">体重、睡眠、饮食子项目按日期存,方便趋势分析。</div></div>

<div class="item"><b>规则表</b><div class="item-meta">所有阈值后台可改,历史数据保留原始值,重新计算时可选择当前规则。</div></div>

</div>

</div>

</section>

</div>


<div class="modal" id="workoutModal"><div class="modalbox">

<h3>记录一次训练</h3><p>记录实际数据,系统自动判断是否达标。</p>

<div class="types">

<button class="typebtn active" data-type="cycling">🚲<br>骑行</button>

<button class="typebtn" data-type="running">🏃<br>跑步</button>

<button class="typebtn" data-type="walking">🚶<br>快走</button>

<button class="typebtn" data-type="strength">🏋️<br>力量</button>

<button class="typebtn" data-type="stairs">🪜<br>爬楼</button>

<button class="typebtn" data-type="hiking">🥾<br>爬山</button>

</div>

<div class="form-grid" style="margin-top:12px">

<div class="field"><label>时长(分钟)</label><input class="input" id="workoutDuration" type="number" min="1" value="10"></div>

<div class="field" id="distanceField"><label>距离(km)</label><input class="input" id="workoutDistance" type="number" step="0.1" value="2.1"></div>

<div class="field" id="setsField" style="display:none"><label>有效组</label><input class="input" id="workoutSets" type="number" value="6"></div>

<div class="field"><label>强度</label><select class="select" id="workoutIntensity"><option>轻松</option><option selected>中等</option><option>较高</option></select></div>

<div class="field full"><label>备注</label><input class="input" id="workoutNote" placeholder="可选"></div>

</div>

<div class="rulebox" id="workoutRuleText"></div>

<div class="mini-actions" style="justify-content:flex-end"><button class="secondary" id="closeWorkout">取消</button><button class="primary" id="saveWorkout">保存训练</button></div>

</div></div>


<div class="toast" id="toast">已保存</div>


<script>

const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];

const DAY=86400000, now=new Date();

const isoDate=d=>${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")};

const parseDate=s=>{const [y,m,d]=s.split("-").map(Number);return new Date(y,m-1,d,12,0,0)};

const fmtDur=ms=>{ms=Math.max(0,ms);const sec=Math.floor(ms/1000),d=Math.floor(sec/86400),h=Math.floor(sec%86400/3600),m=Math.floor(sec%3600/60),s=sec%60;return ${d}天 ${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}};

const agoDays=d=>Math.floor((Date.now()-new Date(d).getTime())/DAY);


const defaults={

rules:{gapDays:14,cycling:30,running:25,walking:40,stairs:15,strengthMin:20,strengthSets:6,hiking:30},

exerciseStart:new Date(Date.now()-12DAY-83600000).toISOString(),

lastQualified:new Date(Date.now()-5DAY).toISOString(),

workouts:[{date:isoDate(now),type:"cycling",duration:10,distance:2.1,sets:0,intensity:"轻松",note:"演示"}],

recoveryDate:null,

mastLast:new Date(Date.now()-4
DAY-163600000).toISOString(),

pornLast:new Date(Date.now()-6
DAY-33600000).toISOString(),

mastEvents:1,pornEvents:0,

diet:{date:isoDate(now),protein:true,veg:true,fruit:false,water:true,junk:false,staple:true},

sleep:{date:isoDate(now),hours:null},

weights:[],

countdowns:[],

timers:[]

};

for(let i=45;i>=0;i-=3){let d=new Date(Date.now()-i
DAY),w=61.3-(45-i).017+Math.sin(i/4).16;defaults.weights.push({date:isoDate(d),weight:+w.toFixed(1)})}

defaults.countdowns=[{id:1,name:"示例目标",date:isoDate(new Date(Date.now()+18DAY))}];

defaults.timers=[{id:1,name:"这个打卡系统",start:new Date(Date.now()-3
DAY-5*3600000).toISOString()}];


let state=JSON.parse(localStorage.getItem("dailyControlV4")||"null")||defaults;

function save(){localStorage.setItem("dailyControlV4",JSON.stringify(state))}

save();


const names={cycling:"骑行",running:"跑步",walking:"快走",strength:"力量",stairs:"爬楼",hiking:"爬山"};

const icons={cycling:"🚲",running:"🏃",walking:"🚶",strength:"🏋️",stairs:"🪜",hiking:"🥾"};


function toast(t){const e=$("#toast");e.textContent=t;e.classList.add("show");clearTimeout(window._tt);window._tt=setTimeout(()=>e.classList.remove("show"),1600)}

function go(page){$$(".page").forEach(x=>x.classList.toggle("active",x.id==="page-"+page));$$(".navbtn").forEach(x=>x.classList.toggle("active",x.dataset.page===page));if(page==="summary"){buildReport();drawWeight()}if(page==="countdown")renderCountdowns();if(page==="timers")renderTimers();window.scrollTo({top:0,behavior:"smooth"})}

$$(".navbtn").forEach(b=>b.addEventListener("click",()=>go(b.dataset.page)));

$$("[data-go]").forEach(b=>b.addEventListener("click",()=>go(b.dataset.go)));


const wd=["星期日","星期一","星期二","星期三","星期四","星期五","星期六"];

$("#bigDate").textContent=${now.getMonth()+1}月${now.getDate()}日,${wd[now.getDay()]};

$("#monthTitle").textContent=${now.getFullYear()}年${now.getMonth()+1}月;


function calendar(){

const b=$("#calendar");b.innerHTML="";

["日","一","二","三","四","五","六"].forEach(x=>{const e=document.createElement("div");e.className="weekday";e.textContent=x;b.appendChild(e)});

const first=new Date(now.getFullYear(),now.getMonth(),1).getDay(),days=new Date(now.getFullYear(),now.getMonth()+1,0).getDate();

for(let i=0;i<first;i++){const e=document.createElement("div");e.className="day";b.appendChild(e)}

const samples={1:"good",2:"good",3:"partial",4:"good",5:"rest",6:"good",7:"good",8:"partial",9:"good",10:"good",11:"event",12:"good"};

for(let d=1;d<=days;d++){const e=document.createElement("div");e.className="day";if(d===now.getDate())e.classList.add("today");if(samples[d])e.classList.add(samples[d]);e.innerHTML=<span>${d}</span>;b.appendChild(e)}

}

calendar();


function todaysWorkouts(){return state.workouts.filter(w=>w.date===isoDate(now))}

function workoutTotals(){

const ws=todaysWorkouts(),g={};let cardio=0,sets=0,dist=0;

ws.forEach(w=>{g[w.type]??={duration:0,sets:0};g[w.type].duration+=+w.duration||0;g[w.type].sets+=+w.sets||0;if(w.type!=="strength")cardio+=+w.duration||0;sets+=+w.sets||0;dist+=+w.distance||0});

let ok=false,type=null;

for(const [k,v] of Object.entries(g)){const r=state.rules;if(k==="strength"){if(v.duration>=r.strengthMin&&v.sets>=r.strengthSets){ok=true;type=k;break}}else if(v.duration>=(r[k]||30)){ok=true;type=k;break}}

return{ws,g,cardio,sets,dist,ok,type};

}

function dietScore(){

const d=state.diet;let pts=0,total=5;

["protein","veg","fruit","water","staple"].forEach(k=>{if(d[k])pts++});if(d.junk)pts=Math.max(0,pts-1);

return{pts,total,pct:Math.round(pts/total*100)}

}

function renderCheckin(){

const t=workoutTotals(),gap=agoDays(state.lastQualified),broken=gap>state.rules.gapDays;

$("#exerciseGapRule").textContent=state.rules.gapDays;

$("#exerciseStreak").textContent=broken?"已断档":fmtDur(Date.now()-new Date(state.exerciseStart).getTime());

$("#exerciseStreakStatus").textContent=broken?"已断":"进行中";$("#exerciseStreakStatus").className="status "+(broken?"bad":"ok");

$("#exerciseGapText").textContent=broken?已经 ${gap} 天没有达标训练;下一次达标会开启新一轮。:距离断档还剩 ${Math.max(0,state.rules.gapDays-gap)} 天 · 最近一次达标训练 ${gap} 天前;

$("#todayCardio").textContent=t.cardio;$("#todaySets").textContent=t.sets;$("#todayDistance").textContent=t.dist.toFixed(1);

if(state.recoveryDate===isoDate(now)){ $("#exerciseTodayMeta").textContent="主动恢复日";$("#exerciseTodayStatus").textContent="恢复日";$("#exerciseTodayStatus").className="status"; }

else if(t.ok){$("#exerciseTodayMeta").textContent=${icons[t.type]} ${names[t.type]}已达到今日标准;$("#exerciseTodayStatus").textContent="达标";$("#exerciseTodayStatus").className="status ok";}

else if(t.ws.length){const last=t.ws.at(-1),v=t.g[last.type],r=state.rules;let need=last.type==="strength"?还差 ${Math.max(0,r.strengthMin-v.duration)} min / ${Math.max(0,r.strengthSets-v.sets)} 组:还差 ${Math.max(0,(r[last.type]||30)-v.duration)} min;$("#exerciseTodayMeta").textContent=${icons[last.type]} ${names[last.type]} ${v.duration} min · ${need};$("#exerciseTodayStatus").textContent="未达标";$("#exerciseTodayStatus").className="status warn";}

else{$("#exerciseTodayMeta").textContent="今天还没有训练记录";$("#exerciseTodayStatus").textContent="待开始";$("#exerciseTodayStatus").className="status"}


const ds=dietScore();$("#dietScore").textContent=${ds.pts}/${ds.total};$("#dietStatus").textContent=ds.pct>=80?"今天不错":ds.pct>=60?"一般":"需要调整";$("#dietStatus").className="status "+(ds.pct>=80?"ok":ds.pct>=60?"warn":"bad");

$$(".diet-toggle").forEach(b=>b.classList.toggle("on",!!state.diet[b.dataset.diet]));

$("#sleepMeta").textContent=state.sleep.hours==null?"目标 ≥ 7 小时。":今天记录 ${state.sleep.hours.toFixed(1)} 小时 · ${state.sleep.hours>=7?"达标":"不足 7 小时"};


const weekCardio=82+t.cardio,weekStrength=2+(t.g.strength&&t.g.strength.sets>=state.rules.strengthSets?1:0),weekSets=14+t.sets;

$("#weekCardio").textContent=weekCardio;$("#weekStrength").textContent=weekStrength;$("#weekSets").textContent=weekSets;

$("#weekCardioBar").style.width=Math.min(100,weekCardio/150100)+"%";$("#weekStrengthBar").style.width=Math.min(100,weekStrength/3100)+"%";$("#weekSetsBar").style.width=Math.min(100,weekSets/18*100)+"%";


let done=0;if(t.ok||state.recoveryDate===isoDate(now))done++;if(ds.pct>=80)done++;if(state.sleep.hours!=null&&state.sleep.hours>=7)done++;if(state.weights.some(x=>x.date===isoDate(now)))done++;done+=2; // self-control counters are "still running"

$("#doneScore").textContent=done;$("#scorePct").textContent=Math.round(done/6100)+"%";$("#scoreBar").style.width=Math.round(done/6100)+"%";

$("#focusText").textContent=broken?"运动已经断档,下一次达标训练会开启新一轮。":t.ok?"今天训练已经达标,剩下看饮食和睡眠。":运动还没达标;距离 14 天断档还剩 ${Math.max(0,state.rules.gapDays-gap)} 天。;

}

renderCheckin();


function liveCounters(){

$("#mastCounter").textContent=fmtDur(Date.now()-new Date(state.mastLast).getTime());

$("#pornCounter").textContent=fmtDur(Date.now()-new Date(state.pornLast).getTime());

}

liveCounters();setInterval(liveCounters,1000);


$("#mastReset").onclick=()=>{if(confirm("确认记录一次自慰事件?这会把“无自慰”计时从现在重新开始。")){state.mastLast=new Date().toISOString();state.mastEvents=(state.mastEvents||0)+1;save();liveCounters();toast("已记录,并重新计时")}};

$("#pornReset").onclick=()=>{if(confirm("确认记录一次色情内容事件?这会把“无色情内容”计时从现在重新开始。")){state.pornLast=new Date().toISOString();state.pornEvents=(state.pornEvents||0)+1;save();liveCounters();toast("已记录,并重新计时")}};


$$(".diet-toggle").forEach(b=>b.onclick=()=>{const k=b.dataset.diet;state.diet.date=isoDate(now);state.diet[k]=!state.diet[k];save();renderCheckin()});

$("#saveSleep").onclick=()=>{const v=+$("#sleepInput").value;if(v<0||v>24||!Number.isFinite(v)){toast("请输入合理睡眠时长");return}state.sleep={date:isoDate(now),hours:v};save();renderCheckin();toast("睡眠已记录")};

$("#saveWeight").onclick=()=>{const v=+$("#weightInput").value;if(!v||v<25||v>250){toast("请输入合理体重");return}const k=isoDate(now),f=state.weights.find(x=>x.date===k);if(f)f.weight=+v.toFixed(1);else state.weights.push({date:k,weight:+v.toFixed(1)});state.weights.sort((a,b)=>a.date.localeCompare(b.date));save();$("#weightHint").textContent=今天已记录 ${v.toFixed(1)} kg,可覆盖修改。;renderCheckin();toast("体重已记录")};


let selectedType="cycling";

function workoutRule(type){const r=state.rules;if(type==="strength")return 力量:≥ ${r.strengthMin} 分钟且 ≥ ${r.strengthSets} 个有效组才算达标。;return ${names[type]}:单日累计 ≥ ${r[type]||30} 分钟才算达标。}

function setType(type){selectedType=type;$$(".typebtn").forEach(b=>b.classList.toggle("active",b.dataset.type===type));$("#setsField").style.display=type==="strength"?"block":"none";$("#distanceField").style.display=type==="strength"?"none":"block";$("#workoutRuleText").textContent=workoutRule(type)}

setType(selectedType);$$(".typebtn").forEach(b=>b.onclick=()=>setType(b.dataset.type));

$("#addWorkout").onclick=()=>$("#workoutModal").classList.add("open");$("#closeWorkout").onclick=()=>$("#workoutModal").classList.remove("open");

$("#saveWorkout").onclick=()=>{const duration=+$("#workoutDuration").value||0;if(duration<1){toast("请输入训练时长");return}const w={date:isoDate(now),type:selectedType,duration,distance:selectedType==="strength"?0:(+$("#workoutDistance").value||0),sets:selectedType==="strength"?(+$("#workoutSets").value||0):0,intensity:$("#workoutIntensity").value,note:$("#workoutNote").value.trim()};state.workouts.push(w);state.recoveryDate=null;const t=workoutTotals();if(t.ok){const oldGap=agoDays(state.lastQualified);if(oldGap>state.rules.gapDays)state.exerciseStart=new Date().toISOString();state.lastQualified=new Date().toISOString()}save();$("#workoutModal").classList.remove("open");renderCheckin();toast("训练已记录")};

$("#setRest").onclick=()=>{state.recoveryDate=state.recoveryDate===isoDate(now)?null:isoDate(now);save();renderCheckin();toast(state.recoveryDate?"今天设为恢复日":"已取消恢复日")};


let summaryRange=14;$$(".rangebtn").forEach(b=>b.onclick=()=>{$$(".rangebtn").forEach(x=>x.classList.remove("active"));b.classList.add("active");summaryRange=+b.dataset.range;buildReport();drawWeight()});

function summaryObject(){

const weights=state.weights.filter(x=>(Date.now()-parseDate(x.date).getTime())<=summaryRangeDAY);

const first=weights[0]?.weight??null,last=weights.at(-1)?.weight??null;

const exerciseCount=Math.max(1,Math.round(summaryRange/7
2.1));

return {

range_days:summaryRange,

generated_at:new Date().toISOString(),

exercise:{qualified_sessions:exerciseCount,current_plan_started:state.exerciseStart,last_qualified:state.lastQualified,break_rule_days:state.rules.gapDays,today:workoutTotals()},

weight:{records:weights.length,latest:last,change:first!=null&&last!=null?+(last-first).toFixed(1):null,series:weights},

diet:{today:state.diet,today_score:dietScore()},

sleep:{today_hours:state.sleep.hours,estimated_range_average:6.8},

self_control:{masturbation_events:state.mastEvents||0,porn_events:state.pornEvents||0,masturbation_streak_started:state.mastLast,porn_streak_started:state.pornLast}

}

}

function buildReport(){

const s=summaryObject();$("#sumExercise").textContent=s.exercise.qualified_sessions;$("#sumDiet").textContent="82%";$("#sumSleep").textContent="6.8h";$("#sumWeight").textContent=s.weight.change==null?"—":(s.weight.change>0?"+":"")+s.weight.change;$("#sumMast").textContent=s.self_control.masturbation_events;$("#sumPorn").textContent=s.self_control.porn_events;

$("#aiReport").value=`# 我的最近 ${s.range_days} 天状态数据


运动



  • 达标训练:${s.exercise.qualified_sessions} 次

  • 当前运动计划开始:${new Date(s.exercise.current_plan_started).toLocaleString()}

  • 最近一次达标训练:${new Date(s.exercise.last_qualified).toLocaleString()}

  • 断档规则:连续 ${s.exercise.break_rule_days} 天没有达标训练才算中断

  • 今日有氧:${s.exercise.today.cardio} 分钟

  • 今日力量有效组:${s.exercise.today.sets} 组

  • 今日训练是否达标:${s.exercise.today.ok?"是":"否"}


体重



  • 记录次数:${s.weight.records}

  • 最新体重:${s.weight.latest??"无"} kg

  • 区间变化:${s.weight.change??"无"} kg


饮食



  • 今日饮食得分:${s.diet.today_score.pts}/${s.diet.today_score.total}

  • 蛋白质:${s.diet.today.protein?"是":"否"}

  • 蔬菜≥2种:${s.diet.today.veg?"是":"否"}

  • 水果:${s.diet.today.fruit?"是":"否"}

  • 饮水≥2L:${s.diet.today.water?"是":"否"}

  • 高油/高糖过量:${s.diet.today.junk?"是":"否"}


睡眠



  • 今日:${s.sleep.today_hours??"未记录"} 小时

  • 区间平均(演示):${s.sleep.estimated_range_average} 小时


自控事件



  • 自慰事件:${s.self_control.masturbation_events} 次

  • 色情内容事件:${s.self_control.porn_events} 次

  • 当前无自慰持续:${fmtDur(Date.now()-new Date(s.self_control.masturbation_streak_started).getTime())}

  • 当前无色情内容持续:${fmtDur(Date.now()-new Date(s.self_control.porn_streak_started).getTime())}


请帮我分析



  1. 哪些行为最稳定,哪些最容易掉?

  2. 运动量是否合理,是否存在“只做一点就自我满足”的情况?

  3. 饮食、睡眠和体重趋势有没有值得注意的关联?

  4. 给我下一个 14 天最值得改的 3 件事,不要给空泛建议。`;

    }

    buildReport();

    function download(name,text,type="text/plain"){const blob=new Blob([text],{type});const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}

    $("#refreshReport").onclick=()=>{buildReport();toast("汇总已重新生成")};$("#downloadReport").onclick=()=>download("daily-control-ai-report.txt",$("#aiReport").value);$("#downloadJson").onclick=()=>download("daily-control-data.json",JSON.stringify(summaryObject(),null,2),"application/json");


function drawWeight(){

const svg=$("#weightChart"),data=state.weights.filter(x=>(Date.now()-parseDate(x.date).getTime())<=Math.max(30,summaryRange)DAY);$("#weightRangeLabel").textContent=最近${summaryRange}天;

if(data.length<2){svg.innerHTML="";return}const vals=data.map(x=>x.weight),W=760,H=220,p={l:44,r:18,t:18,b:30};let min=Math.min(...vals)-.2,max=Math.max(...vals)+.2,x=i=>p.l+i/(data.length-1)
(W-p.l-p.r),y=v=>p.t+(max-v)/(max-min)(H-p.t-p.b),g="";

for(let i=0;i<4;i++){let yy=p.t+i
(H-p.t-p.b)/3,val=(max-i*(max-min)/3).toFixed(1);g+=<line x1="${p.l}" y1="${yy}" x2="${W-p.r}" y2="${yy}" stroke="#202a3c"/><text x="${p.l-7}" y="${yy+4}" text-anchor="end" fill="#7f8ba0" font-size="10">${val}</text>}

const pts=data.map((d,i)=>${x(i)},${y(d.weight)}).join(" ");g+=<polyline points="${pts}" fill="none" stroke="#7bddc3" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>;svg.innerHTML=g;

}


function renderCountdowns(){

const box=$("#countList");box.innerHTML="";if(!state.countdowns.length){box.innerHTML='<div class="muted">还没有倒数日。</div>';return}

state.countdowns.sort((a,b)=>a.date.localeCompare(b.date)).forEach(c=>{const target=parseDate(c.date),days=Math.ceil((target-new Date(now.getFullYear(),now.getMonth(),now.getDate(),12))/DAY);const e=document.createElement("div");e.className="count-card";e.innerHTML=<div class="row"><div><b>${c.name}</b><div class="item-meta">${c.date}</div></div><button class="secondary" data-del-count="${c.id}">删除</button></div><div class="count-days">${days>=0?还有 ${days} 天:已过去 ${Math.abs(days)} 天}</div>;box.appendChild(e)});

$$("[data-del-count]").forEach(b=>b.onclick=()=>{state.countdowns=state.countdowns.filter(x=>x.id!=b.dataset.delCount);save();renderCountdowns()})

}

$("#addCountdown").onclick=()=>{const name=$("#countName").value.trim(),date=$("#countDate").value;if(!name||!date){toast("填写名称和日期");return}state.countdowns.push({id:Date.now(),name,date});save();$("#countName").value="";$("#countDate").value="";renderCountdowns();toast("倒数日已新增")};


function renderTimers(){

const box=$("#timerList");box.innerHTML="";if(!state.timers.length){box.innerHTML='<div class="muted">还没有正计时。</div>';return}

state.timers.forEach(t=>{const e=document.createElement("div");e.className="timer-card";e.innerHTML=<div class="row"><div><b>${t.name}</b><div class="item-meta">开始:${new Date(t.start).toLocaleString()}</div></div><button class="secondary" data-del-timer="${t.id}">删除</button></div><div class="timer-value" data-timer-value="${t.id}">${fmtDur(Date.now()-new Date(t.start).getTime())}</div>;box.appendChild(e)});

$$("[data-del-timer]").forEach(b=>b.onclick=()=>{state.timers=state.timers.filter(x=>x.id!=b.dataset.delTimer);save();renderTimers()})

}

$("#addTimer").onclick=()=>{const name=$("#timerName").value.trim(),start=$("#timerStart").value;if(!name||!start){toast("填写名称和开始时间");return}state.timers.push({id:Date.now(),name,start:new Date(start).toISOString()});save();$("#timerName").value="";$("#timerStart").value="";renderTimers();toast("正计时已新增")};

setInterval(()=>{$$("[data-timer-value]").forEach(el=>{const t=state.timers.find(x=>String(x.id)===el.dataset.timerValue);if(t)el.textContent=fmtDur(Date.now()-new Date(t.start).getTime())})},1000);


function syncRules(){const r=state.rules;$("#gapDays").value=r.gapDays;$("#ruleCycling").value=r.cycling;$("#ruleRunning").value=r.running;$("#ruleWalking").value=r.walking;$("#ruleStairs").value=r.stairs;$("#ruleStrengthMin").value=r.strengthMin;$("#ruleStrengthSets").value=r.strengthSets}

syncRules();

$("#saveRules").onclick=()=>{state.rules={...state.rules,gapDays:+$("#gapDays").value||14,cycling:+$("#ruleCycling").value||30,running:+$("#ruleRunning").value||25,walking:+$("#ruleWalking").value||40,stairs:+$("#ruleStairs").value||15,strengthMin:+$("#ruleStrengthMin").value||20,strengthSets:+$("#ruleStrengthSets").value||6};save();setType(selectedType);renderCheckin();toast("规则已保存")};

</script>

</body>

</html>

最新回复 (1)
  • KakRot 09-13 18:14
    1

    我是学生,不懂不懂

* 帖子来源NodeSeek
返回