feat: rebuild pet app and backend around core care flows
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#f5f3ed" />
|
||||
<link rel="icon" type="image/png" href="pets-fe/assets/logo-corgi-v2.png" />
|
||||
<title>肉垫计划 · 宠物生活原型</title>
|
||||
<style>
|
||||
:root{--ink:#183b36;--muted:#83928d;--green:#174e42;--green2:#28735d;--mint:#dcece2;--cream:#f5f3ed;--card:#fff;--line:#e9eee9;--orange:#ecaa69;--shadow:0 18px 50px rgba(22,64,54,.09)}
|
||||
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--cream);color:var(--ink);font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}button,input,select{font:inherit}button{cursor:pointer}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #a2d4b3;outline-offset:2px}
|
||||
.desktop{min-height:100vh;display:flex;align-items:center;justify-content:center;gap:90px;padding:40px}.intro{width:330px}.logo{display:flex;align-items:center;gap:11px;font-size:24px;font-weight:900;letter-spacing:-1px}.logo-mark{width:42px;height:42px;display:grid;place-items:center;background:var(--green);border-radius:15px;color:white;font-size:25px;transform:rotate(-8deg)}.eyebrow{font-size:12px;letter-spacing:3px;font-weight:800;color:var(--green2);margin:50px 0 14px}.intro h1{font-size:54px;line-height:1.16;letter-spacing:-3px;margin:0}.intro h1 em{font-style:normal;color:var(--green2)}.intro p{color:#687d74;font-size:16px;line-height:1.9;margin:22px 0}.intro-tags{display:flex;gap:8px;flex-wrap:wrap}.intro-tags span{background:#e8ebe3;color:#4b6e60;border-radius:999px;padding:8px 13px;font-size:12px;font-weight:700}.intro-note{margin-top:65px;font-size:12px;color:#a0aaa2;line-height:1.7}
|
||||
.phone{width:430px;height:min(890px,calc(100vh - 42px));min-height:680px;background:#f8faf7;border:9px solid #1d302e;border-radius:45px;box-shadow:0 40px 90px rgba(17,52,42,.23);overflow:hidden;position:relative;flex:none}.phone:before{content:"";position:absolute;z-index:30;top:9px;left:50%;transform:translateX(-50%);width:103px;height:25px;background:#1d302e;border-radius:0 0 17px 17px}.app{height:100%;display:flex;flex-direction:column;position:relative}.screen{flex:1;overflow-y:auto;scrollbar-width:none;padding-bottom:24px}.screen::-webkit-scrollbar{display:none}.screen-head{padding:43px 25px 14px}.topline{display:flex;justify-content:space-between;align-items:center}.date-kicker{font-size:12px;color:var(--muted);letter-spacing:.4px;font-weight:700}.screen-title{font-size:29px;letter-spacing:-1.1px;margin:5px 0 0;font-weight:850}.icon-btn{width:40px;height:40px;border:0;border-radius:14px;background:#fff;color:var(--ink);display:grid;place-items:center;box-shadow:0 3px 15px #163b3612}.icon-btn svg,.nav-item svg,.small-icon svg,.action-icon svg,.outline-icon svg{width:21px;height:21px;stroke:currentColor;fill:none;stroke-width:1.85;stroke-linecap:round;stroke-linejoin:round}.avatar-mini{width:42px;height:42px;border:3px solid white;border-radius:50%;background:#f5cf9e;display:grid;place-items:center;font-size:25px;box-shadow:0 3px 14px #2444361b}
|
||||
.content{padding:0 20px}.card{background:#fff;border-radius:23px;box-shadow:0 3px 22px rgba(27,63,47,.035)}.hero{position:relative;overflow:hidden;background:#174e42;color:#fff;padding:22px 22px 20px;min-height:194px}.hero:before,.hero:after{content:"";position:absolute;border:1px solid #ffffff2b;border-radius:50%;width:240px;height:240px;right:-63px;top:-87px}.hero:after{width:184px;height:184px;right:-35px;top:-55px}.hero-top{position:relative;z-index:1;display:flex;justify-content:space-between}.hero-label{font-size:12px;color:#c4ddcb;font-weight:700}.hero h2{font-size:29px;letter-spacing:-1px;margin:8px 0 5px}.hero p{font-size:12px;color:#c6ddd0;margin:0}.hero-pet{width:107px;height:107px;background:#f1c584;border-radius:35px;display:grid;place-items:center;font-size:72px;transform:rotate(7deg);margin-right:4px;box-shadow:inset 0 -10px 0 #ffffff30}.hero-pet span{transform:rotate(-7deg)}.pet-picker{border:0;background:#ffffff26;color:#fff;border-radius:100px;padding:8px 12px;font-size:12px;font-weight:700;margin-top:13px}.hero-bottom{position:relative;z-index:1;display:flex;align-items:center;gap:8px;margin-top:9px;color:#e8f2e9;font-size:12px}.hero-dot{height:7px;width:7px;border-radius:50%;background:#8fdbac}
|
||||
.section-heading{display:flex;align-items:end;justify-content:space-between;margin:24px 2px 13px}.section-heading h3{font-size:19px;margin:0;letter-spacing:-.4px}.section-heading button{border:0;background:none;color:var(--green2);font-size:12px;font-weight:800;padding:4px}.walk-card{display:flex;align-items:center;gap:15px;padding:16px}.walk-illustration{width:98px;height:100px;flex:none;border-radius:18px;background:#e1eedc;position:relative;overflow:hidden;display:grid;place-items:center;font-size:48px}.walk-illustration:before{content:"";position:absolute;width:110px;height:40px;border-radius:50%;background:#bcd9ac;bottom:-18px;left:-5px}.walk-illustration span{position:relative}.walk-info{flex:1}.walk-info h4{margin:0 0 7px;font-size:16px}.walk-info p{font-size:12px;line-height:1.6;color:var(--muted);margin:0 0 11px}.primary{border:0;background:var(--green);color:white;border-radius:12px;padding:11px 16px;font-size:12px;font-weight:800;box-shadow:0 8px 18px #174e4222}.primary:active,.soft-button:active,.nav-item:active{transform:scale(.97)}.stats{display:flex;gap:9px;margin-top:11px}.stat{flex:1;padding:15px 13px}.stat small{display:block;color:var(--muted);font-size:11px;font-weight:700}.stat strong{display:block;font-size:22px;margin-top:4px;letter-spacing:-.7px}.stat strong i{font-size:11px;font-style:normal;color:#94a29a;font-weight:600;margin-left:3px}.stat:nth-child(2){background:#f5e9db}.stat:nth-child(3){background:#e8eee9}
|
||||
.task-row{display:flex;align-items:center;gap:13px;padding:13px 4px;border-bottom:1px solid var(--line)}.task-row:last-child{border:0}.check{height:23px;width:23px;border:2px solid #ccdcd1;border-radius:8px;background:#fff;color:white;flex:none;padding:0;display:grid;place-items:center}.task-row.done .check{background:var(--green2);border-color:var(--green2)}.task-row.done .task-name{text-decoration:line-through;color:#a8b4ad}.task-icon{height:41px;width:41px;display:grid;place-items:center;border-radius:13px;background:#f1f5ec;font-size:22px}.task-copy{flex:1}.task-name{font-weight:750;font-size:13px}.task-sub{color:#9aa9a0;font-size:11px;margin-top:3px}.task-time{font-size:11px;color:#a1aea5}.tasks-card{padding:5px 15px}.progress{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:11px;margin:0 2px 12px}.progress-track{flex:1;height:6px;background:#e4eae5;border-radius:20px;overflow:hidden}.progress-track span{display:block;height:100%;background:#79b894;border-radius:20px;transition:width .25s}
|
||||
.meet-mini{display:flex;gap:12px;padding:14px}.meet-photo{width:88px;min-height:88px;border-radius:16px;background:linear-gradient(150deg,#c9e0c0,#85b08a);display:grid;place-items:center;font-size:43px;flex:none}.meet-title{font-size:13px;font-weight:800;margin:4px 0 8px}.meet-meta{font-size:11px;color:#91a096;line-height:1.6}.pill{display:inline-block;background:#f0f6ed;color:#4a8b62;border-radius:8px;font-size:10px;font-weight:800;padding:4px 7px;margin-top:9px}
|
||||
.tabbar{height:76px;flex:none;background:#fff;box-shadow:0 -5px 30px #294c3b0a;display:flex;align-items:flex-start;padding:11px 12px max(10px,env(safe-area-inset-bottom));z-index:20}.nav-item{border:0;background:none;flex:1;color:#a1aca3;display:flex;align-items:center;flex-direction:column;gap:4px;font-size:10px;font-weight:700;padding:0}.nav-item.active{color:var(--green)}.nav-item.active svg{stroke-width:2.5}.nav-item svg{height:24px;width:24px}.nav-item.walk-nav{position:relative}.walk-nav .nav-bubble{width:50px;height:50px;border-radius:17px;background:var(--green);color:white;display:grid;place-items:center;margin-top:-25px;box-shadow:0 9px 17px #174e4238}.walk-nav svg{width:25px;height:25px}.walk-nav span:last-child{margin-top:4px}.walk-nav.active .nav-bubble{background:#e6a66b}
|
||||
.map-wrap{height:365px;position:relative;background:#e8efe7;overflow:hidden}.map-wrap svg{width:100%;height:100%;display:block}.map-overlay{position:absolute;left:18px;right:18px;top:16px;display:flex;justify-content:space-between;align-items:start}.map-badge{background:#fff;padding:10px 12px;border-radius:13px;box-shadow:0 4px 18px #204a321c;font-size:11px;font-weight:750}.map-badge i{display:inline-block;width:7px;height:7px;background:#70b788;border-radius:50%;margin-right:5px}.map-control{background:#fff;border:0;width:40px;height:40px;border-radius:13px;box-shadow:0 4px 18px #204a321c;font-size:19px;color:var(--green)}.walk-panel{position:relative;margin:-25px 16px 0;padding:19px 18px 18px;z-index:2}.walk-panel h3{margin:0;font-size:20px}.walk-panel .subtitle{font-size:12px;color:var(--muted);margin:7px 0 19px}.walk-numbers{display:flex;margin:0 0 18px}.walk-numbers div{flex:1;border-right:1px solid var(--line);padding-left:12px}.walk-numbers div:first-child{padding-left:0}.walk-numbers div:last-child{border:0}.walk-numbers strong{display:block;font-size:24px;letter-spacing:-1px}.walk-numbers small{display:block;color:#9cab9e;font-size:10px;margin-top:3px}.walk-actions{display:flex;gap:9px}.walk-actions button{flex:1;height:45px;font-size:13px}.soft-button{background:#e9f2ea;color:var(--green);border:0;border-radius:12px;font-weight:800}.share-line{display:flex;justify-content:space-between;align-items:center;padding:15px 4px 0;font-size:12px;font-weight:750}.share-line small{display:block;font-size:10px;color:var(--muted);font-weight:500;margin-top:4px}.switch{width:42px;height:25px;border:0;border-radius:100px;background:#d8e2da;padding:3px;transition:background .2s}.switch:before{content:"";display:block;width:19px;height:19px;background:#fff;border-radius:50%;transition:transform .2s}.switch.on{background:#2d896b}.switch.on:before{transform:translateX(17px)}.tip-card{background:#e9f1e6;color:#527661;padding:12px 14px;border-radius:14px;font-size:11px;line-height:1.6;margin-top:13px}
|
||||
.filters{display:flex;gap:8px;overflow-x:auto;padding:2px 0 14px;scrollbar-width:none}.filter{border:1px solid #e0e8e0;background:#fff;color:#6f8778;white-space:nowrap;padding:9px 13px;border-radius:10px;font-size:12px;font-weight:750}.filter.active{background:var(--green);color:white;border-color:var(--green)}.meet-card{padding:13px;margin-bottom:11px}.meet-banner{height:117px;border-radius:16px;position:relative;overflow:hidden;background:linear-gradient(140deg,#c3d9bd,#8bb490)}.meet-banner.peach{background:linear-gradient(140deg,#f1d7b8,#d6b591)}.meet-banner.blue{background:linear-gradient(140deg,#c1dcd9,#8fb8b1)}.meet-banner:before{content:"";position:absolute;width:240px;height:120px;border-radius:50%;background:#ffffff3d;left:-25px;bottom:-80px}.meet-banner:after{content:"";position:absolute;width:180px;height:90px;border-radius:50%;background:#ffffff4a;right:-30px;bottom:-55px}.meet-banner .scene{font-size:57px;position:absolute;right:27px;bottom:7px;z-index:1}.meet-banner .tag{position:absolute;left:12px;top:12px;background:#fff;color:var(--green);padding:6px 9px;border-radius:8px;font-size:10px;font-weight:800}.meet-card h4{margin:14px 2px 6px;font-size:16px}.meet-card p{font-size:11px;color:#94a39a;margin:0 2px 12px}.meet-footer{display:flex;justify-content:space-between;align-items:center}.participants{display:flex;align-items:center;font-size:11px;color:#94a39a}.face{width:25px;height:25px;border-radius:50%;border:2px solid white;background:#eed2a0;display:grid;place-items:center;font-size:16px;margin-left:-5px}.face:first-child{margin-left:0}.participants span:last-child{margin-left:7px}.join-btn{border:0;border-radius:10px;background:#e5f0e6;color:var(--green);padding:9px 14px;font-size:11px;font-weight:800}.join-btn.joined{background:#f2eee5;color:#a78d66}
|
||||
.day-strip{display:flex;justify-content:space-between;margin:7px 0 21px}.day{border:0;background:transparent;color:#a3b0a6;display:flex;align-items:center;flex-direction:column;gap:9px;font-size:10px;font-weight:750;width:42px}.day strong{width:37px;height:37px;display:grid;place-items:center;border-radius:12px;font-size:13px;color:#597064}.day.active strong{background:var(--green);color:white}.day.active{color:var(--green)}.task-group-title{display:flex;align-items:center;justify-content:space-between;margin:21px 1px 9px}.task-group-title strong{font-size:14px}.task-group-title span{font-size:11px;color:#9ba99f}.task-card{padding:2px 14px}.task-card .task-row{padding:15px 2px}.add-dashed{width:100%;background:none;border:1.5px dashed #bed1c0;color:#6c9875;border-radius:14px;padding:14px;font-size:12px;font-weight:800;margin-top:12px}.pet-list-card{padding:8px}.pet-list-row{display:flex;align-items:center;gap:13px;padding:11px;border-radius:15px;cursor:pointer}.pet-list-row.active{background:#edf5ed}.pet-avatar{width:57px;height:57px;display:grid;place-items:center;border-radius:18px;background:#f4d19d;font-size:37px;flex:none}.pet-list-row:nth-child(2) .pet-avatar{background:#d8e5dc}.pet-list-row h4{margin:0 0 5px;font-size:14px}.pet-list-row p{margin:0;color:var(--muted);font-size:11px}.pet-list-row .arrow{margin-left:auto;color:#9aaba0}.profile-card{padding:18px}.profile-top{display:flex;gap:16px;align-items:center}.profile-big{width:76px;height:76px;border-radius:24px;background:#f4d19d;display:grid;place-items:center;font-size:51px}.profile-name{font-size:22px;font-weight:850;margin:0 0 5px}.profile-desc{font-size:12px;color:var(--muted)}.profile-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:18px}.profile-field{border-radius:12px;background:#f5f8f4;padding:11px 9px}.profile-field small{color:#96a69a;display:block;font-size:10px}.profile-field strong{font-size:12px;display:block;margin-top:5px}.profile-note{margin-top:14px;background:#f7f3e9;padding:12px;border-radius:12px;color:#816f57;font-size:11px;line-height:1.6}.outline-button{background:white;border:1px solid #cbded0;color:var(--green);border-radius:12px;padding:12px;font-size:12px;font-weight:800;width:100%;margin-top:13px}
|
||||
.modal-backdrop{position:absolute;inset:0;background:#15392e70;z-index:40;display:flex;align-items:end;justify-content:center}.modal{width:100%;max-height:82%;overflow:auto;background:#fff;border-radius:26px 26px 0 0;padding:20px 24px 32px;box-shadow:0 -10px 40px #132d2a20}.modal-handle{width:38px;height:4px;background:#d7e3da;border-radius:3px;margin:-5px auto 18px}.modal-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:18px}.modal-head h3{font-size:20px;margin:0}.close{border:0;background:#f0f5f0;color:#496558;border-radius:10px;width:30px;height:30px;font-size:19px}.field{display:block;margin:13px 0}.field span{display:block;color:#687e70;font-weight:750;font-size:12px;margin-bottom:7px}.field input,.field select{width:100%;border:1px solid #dce8dd;background:#f9fbf8;border-radius:11px;padding:12px;color:var(--ink);font-size:14px}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.modal .primary{width:100%;height:46px;margin-top:12px}.choice-row{display:flex;gap:8px}.choice{border:1px solid #dce8dd;background:#fff;border-radius:11px;padding:10px 14px;font-size:12px;color:#6d8074}.choice.active{background:#e8f3ea;color:var(--green);border-color:#9acaaa;font-weight:800}.toast{position:absolute;bottom:92px;left:50%;transform:translateX(-50%);z-index:70;background:#1e4639;color:white;border-radius:12px;padding:11px 16px;font-size:12px;white-space:nowrap;box-shadow:0 8px 20px #183b3640;animation:toastin .2s ease}@keyframes toastin{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}.empty{padding:30px;text-align:center;color:var(--muted);font-size:12px}.small-note{color:#9aa99d;font-size:11px;line-height:1.6}
|
||||
@media(max-width:900px){.desktop{padding:0;display:block}.intro{display:none}.phone{width:100%;height:100dvh;min-height:0;border:0;border-radius:0;box-shadow:none}.phone:before{display:none}.screen-head{padding-top:max(28px,env(safe-area-inset-top))}.tabbar{height:calc(68px + env(safe-area-inset-bottom));padding-top:8px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="desktop">
|
||||
<aside class="intro"><div class="logo"><img class="brand-logo" src="pets-fe/assets/logo-corgi-v2.png" alt="肉垫计划头像" /> 肉垫计划</div><div class="eyebrow">FIVE CORGIS, ONE HAPPY HOME</div><h1>陪它们走过<br><em>每一天。</em></h1><p>把遛狗、约伴和日常照护,放在一个轻松好用的地方。五只柯基也能一起出门,各有自己的生活档案。</p><div class="intro-tags"><span>🐕 遛狗轨迹</span><span>🫶 同城约遛</span><span>💧 每日照护</span><span>📋 宠物档案</span></div><div class="intro-note">交互原型 · 浏览器本地保存体验数据<br>点击右侧手机中的导航和按钮体验</div></aside>
|
||||
<main class="phone"><div class="app"><div id="screen" class="screen"></div><nav id="tabbar" class="tabbar"></nav><div id="modalRoot"></div><div id="toastRoot"></div></div></main>
|
||||
</div>
|
||||
<script>
|
||||
const icons={home:'<path d="m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1z"/>',walk:'<path d="M12 3c1.3 0 2.3 1 2.3 2.3S13.3 7.6 12 7.6 9.7 6.6 9.7 5.3 10.7 3 12 3ZM7 7c1 0 1.8.8 1.8 1.8S8 10.6 7 10.6 5.2 9.8 5.2 8.8 6 7 7 7Zm10 0c1 0 1.8.8 1.8 1.8S18 10.6 17 10.6s-1.8-.8-1.8-1.8S16 7 17 7ZM5 12.1c1 0 1.7.8 1.7 1.8S6 15.7 5 15.7s-1.7-.8-1.7-1.8S4 12.1 5 12.1Zm14 0c1 0 1.7.8 1.7 1.8s-.7 1.8-1.7 1.8-1.7-.8-1.7-1.8.7-1.8 1.7-1.8ZM12 10.3c2.2 0 5.8 4.3 5.8 6.9 0 1.6-1.2 2.8-2.7 2.8-1.2 0-2-.8-3.1-.8s-1.9.8-3.1.8c-1.5 0-2.7-1.2-2.7-2.8 0-2.6 3.6-6.9 5.8-6.9Z"/>',meet:'<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm8 0a3 3 0 1 0 0-6M2 20v-2a6 6 0 0 1 12 0v2H2Zm14-8a5 5 0 0 1 6 5v3h-5"/>',tasks:'<rect x="4" y="4" width="16" height="16" rx="3"/><path d="m8 12 2.5 2.5L16 9"/>',pets:'<circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 3.6-7 8-7s8 3 8 7"/>',bell:'<path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9ZM10 21h4"/>',pin:'<path d="M20 10c0 5-8 11-8 11S4 15 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/>',chev:'<path d="m9 18 6-6-6-6"/>',plus:'<path d="M12 5v14M5 12h14"/>',share:'<path d="M12 16V3m0 0L7 8m5-5 5 5M4 14v6h16v-6"/>',check:'<path d="m5 12 4 4L19 6"/>',pause:'<rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/>',play:'<path d="m7 4 13 8-13 8z"/>',edit:'<path d="M12 20h9M16 4l4 4L9 19H5v-4L16 4Z"/>'};
|
||||
function icon(name){return `<svg viewBox="0 0 24 24" aria-hidden="true">${icons[name]||icons.home}</svg>`}
|
||||
function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
|
||||
const todayKey=()=>new Date().toISOString().slice(0,10);const defaultState={pets:[{id:1,name:'布丁',emoji:'🐕',type:'狗狗',breed:'柯基',gender:'男孩',age:'2岁3个月',weight:'12.6 kg',birthday:'2024-06-15',note:'亲人、爱玩球,对鸡肉过敏'},{id:2,name:'奶盖',emoji:'🐈',type:'猫猫',breed:'英短',gender:'女孩',age:'1岁8个月',weight:'4.2 kg',birthday:'2025-01-18',note:'喜欢晒太阳,晚上要梳毛'}],activePet:1,tasks:[{id:1,petId:1,name:'早餐喂食',icon:'🍚',time:'08:00',category:'饮食',done:false},{id:2,petId:1,name:'更换饮水',icon:'💧',time:'10:00',category:'饮水',done:false},{id:3,petId:1,name:'晚间散步',icon:'🦮',time:'18:30',category:'活动',done:false},{id:4,petId:1,name:'睡前梳毛',icon:'🪮',time:'21:00',category:'护理',done:false},{id:5,petId:2,name:'早餐喂食',icon:'🍚',time:'08:30',category:'饮食',done:false},{id:6,petId:2,name:'更换饮水',icon:'💧',time:'10:00',category:'饮水',done:false}],meetups:[{id:1,title:'周末草坪放电局',place:'世纪公园 · 3号门',time:'周六 09:30',distance:'1.2 km',people:4,emoji:'🐶🌿',theme:'green',tag:'小型犬友好',joined:false},{id:2,title:'日落河边慢慢走',place:'滨江步道 · 亲水平台',time:'周六 17:20',distance:'2.4 km',people:3,emoji:'🐕🌅',theme:'peach',tag:'轻松慢走',joined:false},{id:3,title:'晨间社交散步',place:'中央绿地 · 南入口',time:'周日 08:00',distance:'3.6 km',people:6,emoji:'🐩🌳',theme:'blue',tag:'新朋友',joined:false}],walks:12,totalKm:18.6,share:false,walkStatus:'idle',walkSeconds:0,walkDistance:0};
|
||||
let state;try{state={...defaultState,...JSON.parse(localStorage.getItem('pawly-prototype')||'{}')}}catch{state=structuredClone(defaultState)}let tab='home',modal='',filter='全部',taskDay=0,walkTicker=null,toastTimer=null;
|
||||
const save=()=>localStorage.setItem('pawly-prototype',JSON.stringify(state));const pet=()=>state.pets.find(p=>p.id===state.activePet)||state.pets[0];let petTasks=()=>state.tasks.filter(t=>t.petId===state.activePet);let counts=()=>({done:petTasks().filter(t=>t.done).length,total:petTasks().length});
|
||||
function toast(msg){document.getElementById('toastRoot').innerHTML=`<div class="toast">${esc(msg)}</div>`;clearTimeout(toastTimer);toastTimer=setTimeout(()=>document.getElementById('toastRoot').innerHTML='',2600)}
|
||||
function header(kicker,title,action){return `<header class="screen-head"><div class="topline"><div><div class="date-kicker">${kicker}</div><h1 class="screen-title">${title}</h1></div>${action||`<button class="icon-btn" onclick="showModal('notifications')" aria-label="通知">${icon('bell')}</button>`}</div></header>`}
|
||||
function section(title,action='',fn=''){return `<div class="section-heading"><h3>${title}</h3>${action?`<button onclick="${fn}">${action} ›</button>`:''}</div>`}
|
||||
function renderHome(){let p=pet(),c=counts();return `${header('你好,今天也一起好好生活','早安,铲屎官 ☀️',`<button class="avatar-mini" onclick="setTab('pets')" aria-label="宠物档案">${p.emoji}</button>`)}<div class="content"><div class="hero card"><div class="hero-top"><div><div class="hero-label">今日的主角</div><h2>${esc(p.name)},出发吧!</h2><p>${esc(p.breed)} · ${esc(p.age)}</p><button class="pet-picker" onclick="showModal('switchPet')">切换宠物 ⌄</button></div><div class="hero-pet"><span>${p.emoji}</span></div></div><div class="hero-bottom"><i class="hero-dot"></i>陪伴它的每一天,都是好日子</div></div>${section('带它出去走走','遛狗记录',"setTab('walk')")}<div class="card walk-card"><div class="walk-illustration"><span>🦮</span></div><div class="walk-info"><h4>今天的风刚刚好</h4><p>带${esc(p.name)}出门转转,记录每一步的小快乐。</p><button class="primary" onclick="setTab('walk')">开始遛狗 →</button></div></div><div class="stats"><div class="stat card"><small>本月遛狗</small><strong>${state.walks}<i>次</i></strong></div><div class="stat card"><small>累计距离</small><strong>${Number(state.totalKm).toFixed(1)}<i>km</i></strong></div><div class="stat card"><small>今日任务</small><strong>${c.done}/${c.total}</strong></div></div>${section('今天要做的事','全部任务',"setTab('tasks')")}<div class="progress"><div class="progress-track"><span style="width:${c.total?c.done/c.total*100:0}%"></span></div>${c.done}/${c.total} 已完成</div><div class="card tasks-card">${petTasks().slice(0,4).map(taskHTML).join('')||'<div class="empty">还没有任务,去添加第一项吧</div>'}</div>${section('附近有约','看看更多',"setTab('meet')")}<div class="card meet-mini" onclick="setTab('meet')" style="cursor:pointer"><div class="meet-photo">🐶</div><div><div class="meet-title">周末草坪放电局</div><div class="meet-meta">📍世纪公园 · 3号门<br>周六 09:30 · 距你 1.2 km</div><span class="pill">已有 4 位小伙伴</span></div></div></div>`}
|
||||
function taskHTML(t){return `<div class="task-row ${t.done?'done':''}"><button class="check" onclick="toggleTask(${t.id})" aria-label="${t.done?'取消完成':'完成'}${esc(t.name)}">${t.done?icon('check'):''}</button><div class="task-icon">${t.icon}</div><div class="task-copy"><div class="task-name">${esc(t.name)}</div><div class="task-sub">${esc(pet().name)} · ${esc(t.category)}</div></div><div class="task-time">${esc(t.time)}</div></div>`}
|
||||
const mapSVG=`<svg viewBox="0 0 420 365" preserveAspectRatio="xMidYMid slice" aria-label="公园路线示意地图"><rect width="420" height="365" fill="#e9efe7"/><path d="M-20 80 Q85 108 140 58 T300 26 T440 65M-20 265 Q108 243 192 286 T440 254" stroke="#fdfcf6" stroke-width="32" fill="none"/><path d="M64-10 Q105 101 73 189 T112 388M324-10 Q289 100 338 182 T304 390" stroke="#fdfcf6" stroke-width="22" fill="none"/><path d="M-10 186Q110 173 160 152T435 182" stroke="#fdfcf6" stroke-width="15" fill="none"/><path d="M-10 80 Q85 108 140 58 T300 26 T440 65M-20 265 Q108 243 192 286 T440 254" stroke="#d3dfd1" stroke-width="1.5" fill="none"/><path d="M170 62Q242 36 283 77T284 169Q233 192 184 165T170 62" fill="#cfe2c7"/><path d="M-4 302Q47 295 89 318T160 365H-4" fill="#c6dec3"/><path d="M334 292Q396 265 430 291V365H350" fill="#c6dec3"/><path d="M133 54c14-7 27-8 34-6M175 218c23-8 40-8 54-5" stroke="#9bc4a3" stroke-width="5" stroke-linecap="round"/><path d="M140 244 Q162 206 205 209 T274 176 Q301 155 294 113" stroke="#2c9570" stroke-width="6" stroke-linecap="round" stroke-dasharray="2 1" fill="none"/><circle cx="141" cy="244" r="15" fill="#fff"/><circle cx="141" cy="244" r="8" fill="#2c9570"/><circle cx="294" cy="113" r="21" fill="#fff"/><text x="294" y="120" text-anchor="middle" font-size="20">🐕</text><text x="183" y="114" font-size="12" fill="#83a48b" font-weight="bold">中心草坪</text><text x="25" y="143" font-size="11" fill="#94a69b">银杏大道</text><text x="282" y="323" font-size="11" fill="#94a69b">咖啡角</text></svg>`;
|
||||
function timeDisplay(){let m=Math.floor(state.walkSeconds/60),s=state.walkSeconds%60;return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`}
|
||||
function renderWalk(){let running=state.walkStatus==='running',paused=state.walkStatus==='paused';return `${header('和毛孩子一起探索城市','一起去遛狗',`<button class="icon-btn" onclick="showModal('walkHistory')" aria-label="遛狗记录">${icon('walk')}</button>`)}<div class="map-wrap">${mapSVG}<div class="map-overlay"><div class="map-badge"><i></i>${running?'正在记录路线':paused?'路线已暂停':'附近适合散步'}</div><button class="map-control" onclick="toast('已定位到附近公园(原型示意地图)')" aria-label="定位">⌖</button></div></div><div class="card walk-panel"><h3>${running?'正在和'+esc(pet().name)+'散步':paused?'休息一下,等你回来':'出门走走吧'}</h3><p class="subtitle">${running?'每一步都值得记录':'打开位置分享,让朋友知道你们在哪里'}</p><div class="walk-numbers"><div><strong id="walkTime">${timeDisplay()}</strong><small>时长</small></div><div><strong id="walkDistance">${state.walkDistance.toFixed(2)}</strong><small>公里</small></div><div><strong id="walkPace">${state.walkSeconds?Math.max(4,Math.round(state.walkSeconds/60/Math.max(state.walkDistance,.01))):'--'}</strong><small>分钟 / 公里</small></div></div><div class="walk-actions">${state.walkStatus==='idle'?`<button class="primary" onclick="startWalk()">${icon('play')} 开始遛狗</button>`:`<button class="soft-button" onclick="pauseWalk()">${icon(running?'pause':'play')} ${running?'暂停':'继续'}</button><button class="primary" onclick="finishWalk()">结束并保存</button>`}</div><div class="share-line"><div>分享实时位置<small>${state.share?'已开启 · 仅在本次遛狗中分享':'让约遛的朋友更容易找到你'}</small></div><button class="switch ${state.share?'on':''}" onclick="toggleShare()" aria-label="切换位置分享" aria-pressed="${state.share}"></button></div><div class="tip-card">${state.share?'🟢 位置分享已开启。你可以随时关闭,结束遛狗后会自动停止。':'🔒 位置仅在你主动开启时分享。这个原型使用示意地图,不会读取真实定位。'}</div></div><div class="content">${section('附近好去处')}<div class="card meet-mini" onclick="setTab('meet')" style="cursor:pointer"><div class="meet-photo">🌳</div><div><div class="meet-title">世纪公园 · 中心草坪</div><div class="meet-meta">开阔草地 · 适合小型犬<br>步行约 12 分钟</div><span class="pill">发现附近约遛活动 →</span></div></div></div>`}
|
||||
function renderMeet(){let ms=state.meetups.filter(m=>filter==='全部'||(filter==='附近'&&parseFloat(m.distance)<=2.5)||(filter==='周末'&&m.time.includes('周'))||(filter==='我参加的'&&m.joined));return `${header('和新朋友一起出门','约个伴,一起遛',`<button class="icon-btn" onclick="showModal('newMeet')" aria-label="发起约遛">${icon('plus')}</button>`)}<div class="content"><div class="filters">${['全部','附近','周末','我参加的'].map(f=>`<button class="filter ${filter===f?'active':''}" onclick="setFilter('${f}')">${f}</button>`).join('')}</div>${ms.map(m=>`<article class="card meet-card"><div class="meet-banner ${m.theme}"><span class="tag">${esc(m.tag)}</span><span class="scene">${m.emoji}</span></div><h4>${esc(m.title)}</h4><p>📍 ${esc(m.place)} · ${esc(m.distance)}</p><div class="meet-footer"><div class="participants"><span class="face">🐶</span><span class="face">🐕</span><span class="face">🐩</span><span>${esc(m.time)} · ${m.people} 人</span></div><button class="join-btn ${m.joined?'joined':''}" onclick="joinMeet(${m.id})">${m.joined?'已参加 ✓':'加入约遛'}</button></div></article>`).join('')||'<div class="card empty">这里暂时没有活动,发起一场吧 🐾</div>'}<button class="add-dashed" onclick="showModal('newMeet')">+ 发起一场约遛</button><p class="small-note">约遛活动为原型演示数据,加入后会保存在此浏览器中。</p></div>`}
|
||||
function renderTasks(){let c=counts(), days=['日','一','二','三','四','五','六'],base=new Date();let arr=Array.from({length:7},(_,i)=>{let d=new Date(base);d.setDate(d.getDate()+i);return d});return `${header('细心照顾每一个小日子','今日照护计划',`<button class="icon-btn" onclick="showModal('newTask')" aria-label="添加任务">${icon('plus')}</button>`)}<div class="content"><div class="day-strip">${arr.map((d,i)=>`<button class="day ${taskDay===i?'active':''}" onclick="setTaskDay(${i})"><span>周${days[d.getDay()]}</span><strong>${d.getDate()}</strong></button>`).join('')}</div><div class="card" style="padding:17px 18px;background:#e2f0e5"><div style="display:flex;align-items:center;justify-content:space-between"><div><div style="font-size:11px;color:#71947a;font-weight:750">${esc(pet().name)}的照护进度</div><div style="font-size:24px;font-weight:850;margin-top:4px">${c.done}/${c.total} <span style="font-size:12px;color:#6a9775">项已完成</span></div></div><div style="font-size:39px">${c.done===c.total&&c.total?'🎉':'🌱'}</div></div><div class="progress-track" style="margin-top:15px;background:#cae1d0"><span style="width:${c.total?c.done/c.total*100:0}%"></span></div></div>${taskDay?'<div class="tip-card">这是未来日期的计划预览。原型中的完成状态按当前清单演示。</div>':''}<div class="task-group-title"><strong>日常任务</strong><span>${c.total} 项任务</span></div><div class="card task-card">${petTasks().map(taskHTML).join('')||'<div class="empty">还没有任务,添加一项吧</div>'}</div><button class="add-dashed" onclick="showModal('newTask')">+ 添加任务</button></div>`}
|
||||
function renderPets(){let p=pet();return `${header('每只毛孩子都独一无二','我的毛孩子',`<button class="icon-btn" onclick="showModal('newPet')" aria-label="添加宠物">${icon('plus')}</button>`)}<div class="content"><div class="card pet-list-card">${state.pets.map(x=>`<div class="pet-list-row ${x.id===p.id?'active':''}" onclick="switchPet(${x.id})"><div class="pet-avatar">${x.emoji}</div><div><h4>${esc(x.name)} ${x.id===p.id?'· 当前':''}</h4><p>${esc(x.breed)} · ${esc(x.age)}</p></div><div class="arrow">${icon('chev')}</div></div>`).join('')}</div>${section('宠物档案')}<div class="card profile-card"><div class="profile-top"><div class="profile-big">${p.emoji}</div><div><div class="profile-name">${esc(p.name)}</div><div class="profile-desc">${esc(p.breed)} · ${esc(p.gender)}</div></div></div><div class="profile-grid"><div class="profile-field"><small>年龄</small><strong>${esc(p.age)}</strong></div><div class="profile-field"><small>体重</small><strong>${esc(p.weight)}</strong></div><div class="profile-field"><small>生日</small><strong>${esc(p.birthday||'未填写')}</strong></div></div><div class="profile-note">🐾 ${esc(p.note||'还没有备注,记录它的小习惯吧。')}</div><button class="outline-button" onclick="showModal('editPet')">${icon('edit')} 编辑档案</button></div>${section('一起生活')}<div class="stats"><div class="stat card"><small>遛狗次数</small><strong>${state.walks}</strong></div><div class="stat card"><small>累计路程</small><strong>${Number(state.totalKm).toFixed(1)}<i>km</i></strong></div></div><button class="add-dashed" onclick="showModal('newPet')">+ 添加另一只毛孩子</button></div>`}
|
||||
function renderNav(){let items=[['home','首页','home'],['meet','约遛','meet'],['walk','遛狗','walk'],['tasks','任务','tasks'],['pets','宠物','pets']];document.getElementById('tabbar').innerHTML=items.map(([id,label,ic])=>`<button class="nav-item ${tab===id?'active':''} ${id==='walk'?'walk-nav':''}" onclick="setTab('${id}')" aria-label="${label}">${id==='walk'?`<span class="nav-bubble">${icon(ic)}</span>`:icon(ic)}<span>${label}</span></button>`).join('')}
|
||||
function render(){document.getElementById('screen').innerHTML=({home:renderHome,walk:renderWalk,meet:renderMeet,tasks:renderTasks,pets:renderPets})[tab]();renderNav();renderModal()}
|
||||
function setTab(t){tab=t;document.getElementById('screen').scrollTop=0;render()}function setFilter(f){filter=f;render()}function setTaskDay(n){taskDay=n;render()}function toggleTask(id){let t=state.tasks.find(x=>x.id===id);if(t){t.done=!t.done;save();render();toast(t.done?'任务完成,做得好!':'已取消完成')}}function switchPet(id){state.activePet=id;save();modal='';render();toast('已切换到 '+pet().name)}
|
||||
function tick(){if(state.walkStatus!=='running')return;state.walkSeconds++;state.walkDistance=+(state.walkSeconds*0.0018).toFixed(2);let tm=document.getElementById('walkTime'),ds=document.getElementById('walkDistance'),pc=document.getElementById('walkPace');if(tm)tm.textContent=timeDisplay();if(ds)ds.textContent=state.walkDistance.toFixed(2);if(pc)pc.textContent=Math.max(4,Math.round(state.walkSeconds/60/Math.max(state.walkDistance,.01)));if(state.walkSeconds%5===0)save()}
|
||||
function startWalk(){state.walkStatus='running';state.walkSeconds=0;state.walkDistance=0;clearInterval(walkTicker);walkTicker=setInterval(tick,1000);save();render();toast('开始记录遛狗路线')}
|
||||
function pauseWalk(){if(state.walkStatus==='running'){state.walkStatus='paused';clearInterval(walkTicker)}else{state.walkStatus='running';walkTicker=setInterval(tick,1000)}save();render()}
|
||||
function finishWalk(){if(state.walkStatus==='idle')return;clearInterval(walkTicker);let mins=Math.max(1,Math.ceil(state.walkSeconds/60)),dist=state.walkDistance;state.walks++;state.totalKm=+(Number(state.totalKm)+dist).toFixed(2);state.walkStatus='idle';state.walkSeconds=0;state.walkDistance=0;state.share=false;save();render();toast(`已保存:${mins} 分钟 · ${dist.toFixed(2)} km`)}
|
||||
function toggleShare(){state.share=!state.share;save();render();toast(state.share?'位置分享已开启':'位置分享已关闭')}
|
||||
function joinMeet(id){let m=state.meetups.find(x=>x.id===id);if(!m)return;m.joined=!m.joined;m.people+=m.joined?1:-1;save();render();toast(m.joined?'加入成功,记得准时出发!':'已退出约遛')}
|
||||
function showModal(type){modal=type;renderModal()}function closeModal(){modal='';renderModal()}
|
||||
function renderModal(){const root=document.getElementById('modalRoot');if(!modal){root.innerHTML='';return}let p=pet(),title='',body='';if(modal==='switchPet'){title='选择毛孩子';body=`<div class="pet-list-card">${state.pets.map(x=>`<div class="pet-list-row ${x.id===p.id?'active':''}" onclick="switchPet(${x.id})"><div class="pet-avatar">${x.emoji}</div><div><h4>${esc(x.name)}</h4><p>${esc(x.breed)} · ${esc(x.age)}</p></div></div>`).join('')}</div><button class="add-dashed" onclick="showModal('newPet')">+ 添加宠物</button>`}else if(modal==='newPet'||modal==='editPet'){let edit=modal==='editPet';title=edit?'编辑宠物档案':'添加毛孩子';body=`<form onsubmit="savePet(event,${edit})"><label class="field"><span>名字</span><input name="name" required maxlength="20" value="${edit?esc(p.name):''}" placeholder="它叫什么?"></label><div class="form-grid"><label class="field"><span>宠物类型</span><select name="type"><option ${edit&&p.type==='狗狗'?'selected':''}>狗狗</option><option ${edit&&p.type==='猫猫'?'selected':''}>猫猫</option><option ${edit&&p.type==='其他'?'selected':''}>其他</option></select></label><label class="field"><span>性别</span><select name="gender"><option ${edit&&p.gender==='男孩'?'selected':''}>男孩</option><option ${edit&&p.gender==='女孩'?'selected':''}>女孩</option><option ${edit&&p.gender==='不确定'?'selected':''}>不确定</option></select></label></div><div class="form-grid"><label class="field"><span>品种</span><input name="breed" maxlength="25" value="${edit?esc(p.breed):''}" placeholder="如:柯基"></label><label class="field"><span>体重</span><input name="weight" maxlength="12" value="${edit?esc(p.weight):''}" placeholder="如:12 kg"></label></div><div class="form-grid"><label class="field"><span>年龄</span><input name="age" maxlength="20" value="${edit?esc(p.age):''}" placeholder="如:2岁"></label><label class="field"><span>生日</span><input name="birthday" type="date" value="${edit?esc(p.birthday):''}"></label></div><label class="field"><span>小习惯 / 注意事项</span><input name="note" maxlength="100" value="${edit?esc(p.note):''}" placeholder="记录它独特的小习惯"></label><button class="primary" type="submit">${edit?'保存档案':'添加宠物'}</button></form>`}else if(modal==='newTask'){title='添加日常任务';body=`<form onsubmit="addTask(event)"><label class="field"><span>任务名称</span><input name="name" required maxlength="30" placeholder="例如:晚餐喂食"></label><div class="form-grid"><label class="field"><span>分类</span><select name="category"><option>饮食</option><option>饮水</option><option>活动</option><option>护理</option><option>其他</option></select></label><label class="field"><span>提醒时间</span><input name="time" type="time" value="18:00" required></label></div><div class="tip-card">任务会添加到当前宠物「${esc(p.name)}」的每日清单。</div><button class="primary" type="submit">添加任务</button></form>`}else if(modal==='newMeet'){title='发起约遛';body=`<form onsubmit="addMeet(event)"><label class="field"><span>活动名称</span><input name="title" required maxlength="30" placeholder="例如:周末公园散步"></label><label class="field"><span>集合地点</span><input name="place" required maxlength="40" placeholder="例如:世纪公园 3 号门"></label><div class="form-grid"><label class="field"><span>时间</span><input name="time" required maxlength="25" placeholder="周六 09:30"></label><label class="field"><span>距你多远</span><input name="distance" required maxlength="12" placeholder="1.2 km"></label></div><label class="field"><span>活动标签</span><input name="tag" maxlength="20" placeholder="例如:小型犬友好"></label><button class="primary" type="submit">发布约遛</button></form>`}else if(modal==='notifications'){title='消息提醒';body='<div class="empty">今天还没有新消息。带毛孩子出门,或参加一场约遛吧 🐾</div>'}else if(modal==='walkHistory'){title='遛狗记录';body=`<div class="profile-grid"><div class="profile-field"><small>累计次数</small><strong>${state.walks} 次</strong></div><div class="profile-field"><small>累计里程</small><strong>${Number(state.totalKm).toFixed(1)} km</strong></div><div class="profile-field"><small>最近状态</small><strong>${state.walkStatus==='idle'?'已完成':'进行中'}</strong></div></div><p class="small-note">原型目前只展示汇总数字;正式版本可在这里查看每次路线与时长。</p>`}root.innerHTML=`<div class="modal-backdrop" onclick="if(event.target===this)closeModal()"><div class="modal"><div class="modal-handle"></div><div class="modal-head"><h3>${title}</h3><button class="close" onclick="closeModal()" aria-label="关闭">×</button></div>${body}</div></div>`}
|
||||
function savePet(e,edit){e.preventDefault();let x=Object.fromEntries(new FormData(e.target));let emoji=x.type==='猫猫'?'🐈':x.type==='狗狗'?'🐕':'🐾';if(edit){Object.assign(pet(),x,{emoji})}else{let id=Date.now();state.pets.push({id,...x,emoji});state.activePet=id}save();modal='';setTab('pets');toast(edit?'宠物档案已更新':'新伙伴加入啦!')}
|
||||
function addTask(e){e.preventDefault();let x=Object.fromEntries(new FormData(e.target));let emoji={饮食:'🍚',饮水:'💧',活动:'🦮',护理:'🪮',其他:'✨'}[x.category];state.tasks.push({id:Date.now(),petId:state.activePet,name:x.name,category:x.category,time:x.time,icon:emoji,done:false});save();modal='';setTab('tasks');toast('任务已添加')}
|
||||
function addMeet(e){e.preventDefault();let x=Object.fromEntries(new FormData(e.target));state.meetups.unshift({id:Date.now(),title:x.title,place:x.place,time:x.time,distance:x.distance,tag:x.tag||'新活动',people:1,emoji:'🐕🌿',theme:'green',joined:true});filter='全部';save();modal='';setTab('meet');toast('约遛已发布')}
|
||||
if(state.walkStatus==='running')walkTicker=setInterval(tick,1000);render();
|
||||
</script>
|
||||
<style>
|
||||
.detail-link{border:0;background:none;padding:0;text-align:left;color:inherit;font-weight:inherit;font-size:inherit;cursor:pointer}.detail-link:hover{color:var(--green2)}
|
||||
.task-edit{border:0;background:#f3f7f2;color:#789281;border-radius:8px;width:27px;height:27px;display:grid;place-items:center;padding:0;margin-left:5px}.task-edit svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||
.timeline-card{padding:14px 16px}.walk-record{display:flex;align-items:center;gap:13px;padding:11px 0;border-bottom:1px solid var(--line)}.walk-record:last-child{border-bottom:0}.record-badge{width:44px;height:44px;background:#eaf3e8;border-radius:14px;display:grid;place-items:center;font-size:22px;flex:none}.record-main{flex:1}.record-main strong{display:block;font-size:12px}.record-main small{display:block;color:#9ba99e;font-size:11px;margin-top:4px}.record-distance{font-size:14px;font-weight:850;color:var(--green)}
|
||||
.detail-hero{border-radius:18px;background:linear-gradient(135deg,#dcebdd,#add2b1);height:130px;display:grid;place-items:center;font-size:58px;margin-bottom:16px}.detail-tags{display:flex;gap:7px;flex-wrap:wrap;margin:12px 0 16px}.detail-tags span{background:#ebf3e9;color:#5d8b6a;font-size:11px;font-weight:750;border-radius:8px;padding:7px 9px}.detail-row{display:flex;gap:12px;padding:11px 0;border-bottom:1px solid var(--line);font-size:12px}.detail-row b{min-width:62px;color:#94a49a}.detail-row span{line-height:1.5}.detail-copy{font-size:12px;color:#6e8375;line-height:1.8;margin:16px 0}.detail-attendees{background:#f5f8f3;border-radius:14px;padding:13px 14px;font-size:11px;color:#6c8272}.detail-attendees strong{display:block;color:var(--ink);font-size:12px;margin-bottom:6px}.danger-button{width:100%;border:0;background:#fff0ec;color:#bf6559;border-radius:12px;padding:12px;font-weight:800;font-size:12px;margin-top:10px}.two-actions{display:flex;gap:8px}.two-actions>*{flex:1}.section-caption{font-size:11px;color:var(--muted);margin:-5px 2px 11px}.health-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.health-item{background:#fff;padding:15px;border-radius:17px;box-shadow:0 3px 22px rgba(27,63,47,.035)}.health-item small{display:block;color:#97a79c;font-size:10px}.health-item strong{display:block;margin-top:6px;font-size:12px;line-height:1.45}.history-empty{background:#fff;border-radius:16px;padding:20px;color:#92a398;text-align:center;font-size:11px}
|
||||
/* 所有通过 icon() 生成的图标都必须有明确尺寸,防止 SVG 默认 300×150 撑坏按钮。 */
|
||||
button svg,.pet-list-row .arrow svg{width:20px;height:20px;flex:none;display:block;fill:none;stroke:currentColor;stroke-width:1.85;stroke-linecap:round;stroke-linejoin:round}
|
||||
.walk-actions button,.outline-button{display:flex;align-items:center;justify-content:center;gap:7px}
|
||||
.walk-actions button svg,.outline-button svg{width:18px;height:18px}
|
||||
.pet-list-row .arrow{display:grid;place-items:center;flex:none;width:20px;height:20px}
|
||||
.walk-party{padding:13px 0 16px;border-bottom:1px solid var(--line);margin-bottom:16px}.walk-party-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.walk-party-head strong{font-size:12px}.walk-party-head small{font-size:10px;color:var(--muted)}.walk-party-list{display:flex;flex-wrap:wrap;gap:7px}.walk-dog{display:flex;align-items:center;gap:5px;border:1.5px solid #e2ece4;background:#f8faf7;color:#84988a;border-radius:11px;padding:7px 10px;font-size:11px;font-weight:800}.walk-dog.selected{background:#e7f3e8;border-color:#86be9b;color:var(--green)}.walk-dog:disabled{opacity:.85;cursor:default}.walk-dog b{font-size:18px;line-height:1}.walk-dog span{font-size:11px}.walk-party-note{font-size:10px;color:#9aab9c;margin-top:8px;line-height:1.5}
|
||||
.home-walk-card{display:block;padding:15px}.home-walk-intro{display:flex;align-items:center;gap:13px}.home-walk-intro .walk-illustration{width:70px;height:70px;font-size:35px}.home-walk-intro h4{font-size:15px;margin:0 0 5px}.home-walk-intro p{font-size:11px;line-height:1.5;color:var(--muted);margin:0}.home-walk-pets{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0 12px}.home-walk-pets .walk-dog{padding:7px 9px}.home-walk-pets .add-dog{border-style:dashed;background:white;color:var(--green2)}.home-walk-start{width:100%;height:42px;display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px}.home-walk-start svg{width:16px;height:16px}
|
||||
.walk-choice-top{padding:15px 16px;margin-bottom:12px;border:2px solid #c8e0ce}.walk-choice-top .walk-party{border:0;padding:0;margin:0}.walk-choice-top .walk-party-head strong{font-size:14px}.walk-choice-top .walk-party-note{margin-top:7px}.walk-choice-top .walk-dog{min-height:38px}.walk-choice-top:before{content:"可多选";display:inline-block;background:#e8f4e9;color:#478668;border-radius:7px;padding:4px 7px;font-size:10px;font-weight:850;margin-bottom:8px}.map-wrap{height:320px}
|
||||
.brand-logo{width:44px;height:44px;border-radius:14px;object-fit:cover;box-shadow:0 7px 18px #174e4230}.hero{min-height:296px}.hero:after{left:10px;right:10px;top:auto;bottom:0;width:calc(100% - 20px);height:152px;border:0;border-radius:0;background:url('pets-fe/assets/corgi-family-five.png') center bottom/contain no-repeat;z-index:0}.hero-pet{display:none}.hero-bottom{margin-top:11px}.home-walk-card .walk-illustration{background:#e1eedc url('pets-fe/assets/corgi-family-five.png') 0% top/500% auto no-repeat;font-size:0}.home-walk-card .walk-illustration:before{display:none}.home-walk-card .walk-illustration span{display:none}.corgi-crop{display:inline-block;flex:none;background:#f1dfbe url('pets-fe/assets/corgi-family-five.png') var(--corgi-x,0%) top/500% auto no-repeat}.walk-dog b .corgi-crop{width:25px;height:25px;border-radius:8px}.pet-avatar.corgi-crop{width:57px;height:57px;border-radius:18px}.profile-big.corgi-crop{width:76px;height:76px;border-radius:24px}.avatar-mini .corgi-crop{width:36px;height:36px;border-radius:50%}.avatar-mini{overflow:hidden;padding:0}.pet-picker{position:relative;z-index:2}
|
||||
.cat-hero{min-height:194px}.cat-hero:after{background:none;border:1px solid #ffffff2b;border-radius:50%;width:184px;height:184px;right:-35px;left:auto;top:-55px;bottom:auto}.cat-hero .hero-pet{display:grid}
|
||||
</style>
|
||||
<script>
|
||||
// 第二轮原型:日期化任务、活动详情、遛狗记录与更完整的宠物档案。
|
||||
const localDay = d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
||||
const dayAt = offset => {const d=new Date();d.setHours(12,0,0,0);d.setDate(d.getDate()+offset);return d};
|
||||
const dayKey = offset => localDay(dayAt(offset));
|
||||
state.taskCompletions ||= {};
|
||||
state.walkHistory ||= [];
|
||||
for(const t of state.tasks){if(t.done && !state.taskCompletions[`${t.id}:${dayKey(0)}`])state.taskCompletions[`${t.id}:${dayKey(0)}`]=true;delete t.done;t.repeat ||= '每天'}
|
||||
for(const p of state.pets){p.allergy ??= p.id===1?'鸡肉':'无';p.care ??= p.id===1?'出门前检查牵引绳,回来擦脚':'每周梳毛 2 次';p.vaccine ??= '待记录'}
|
||||
for(const m of state.meetups){m.maxPeople ||= 8;m.description ||= '一起轻松散步,让毛孩子交新朋友。请自带牵引绳和饮水,集合后按大家的节奏出发。'}
|
||||
save();
|
||||
|
||||
function dueTasks(offset){const weekday=dayAt(offset).getDay();return state.tasks.filter(t=>t.petId===state.activePet && (t.repeat==='每天'||!t.repeat||(t.repeat==='工作日'&&weekday>0&&weekday<6)||(t.repeat==='周末'&&(weekday===0||weekday===6)))).sort((a,b)=>a.time.localeCompare(b.time))}
|
||||
function doneFor(t,offset){return !!state.taskCompletions[`${t.id}:${dayKey(offset)}`]}
|
||||
function taskStats(offset){const a=dueTasks(offset);return {done:a.filter(t=>doneFor(t,offset)).length,total:a.length}}
|
||||
petTasks=()=>dueTasks(tab==='tasks'?taskDay:0);
|
||||
counts=()=>taskStats(0);
|
||||
taskHTML=function(t){const offset=tab==='tasks'?taskDay:0,done=doneFor(t,offset);return `<div class="task-row ${done?'done':''}"><button class="check" onclick="toggleTask(${t.id})" aria-label="${done?'取消完成':'完成'}${esc(t.name)}">${done?icon('check'):''}</button><div class="task-icon">${t.icon}</div><div class="task-copy"><div class="task-name">${esc(t.name)}</div><div class="task-sub">${esc(pet().name)} · ${esc(t.category)} · ${esc(t.repeat||'每天')}</div></div><div class="task-time">${esc(t.time)}</div>${tab==='tasks'?`<button class="task-edit" onclick="showModal('editTask:${t.id}')" aria-label="编辑${esc(t.name)}">${icon('edit')}</button>`:''}</div>`};
|
||||
toggleTask=function(id){const t=state.tasks.find(x=>x.id===id);if(!t)return;const offset=tab==='tasks'?taskDay:0,key=`${id}:${dayKey(offset)}`;state.taskCompletions[key]=!state.taskCompletions[key];save();render();toast(state.taskCompletions[key]?'任务完成,做得好!':'已取消完成')};
|
||||
renderTasks=function(){const c=taskStats(taskDay),days=['日','一','二','三','四','五','六'],arr=Array.from({length:7},(_,i)=>dayAt(i));return `${header('细心照顾每一个小日子','日常照护计划',`<button class="icon-btn" onclick="showModal('newTask')" aria-label="添加任务">${icon('plus')}</button>`)}<div class="content"><div class="day-strip">${arr.map((d,i)=>`<button class="day ${taskDay===i?'active':''}" onclick="setTaskDay(${i})"><span>${i===0?'今天':`周${days[d.getDay()]}`}</span><strong>${d.getDate()}</strong></button>`).join('')}</div><div class="card" style="padding:17px 18px;background:#e2f0e5"><div style="display:flex;align-items:center;justify-content:space-between"><div><div style="font-size:11px;color:#71947a;font-weight:750">${esc(pet().name)} · ${taskDay===0?'今天':`${dayAt(taskDay).getMonth()+1}月${dayAt(taskDay).getDate()}日`}的照护</div><div style="font-size:24px;font-weight:850;margin-top:4px">${c.done}/${c.total} <span style="font-size:12px;color:#6a9775">项已完成</span></div></div><div style="font-size:39px">${c.done===c.total&&c.total?'🎉':'🌱'}</div></div><div class="progress-track" style="margin-top:15px;background:#cae1d0"><span style="width:${c.total?c.done/c.total*100:0}%"></span></div></div><div class="task-group-title"><strong>${taskDay?'当日计划':'今天要做的事'}</strong><span>${c.total} 项 · 点击右侧可编辑</span></div><div class="card task-card">${dueTasks(taskDay).map(taskHTML).join('')||'<div class="empty">这天没有安排任务,给毛孩子放个假吧 ☁️</div>'}</div><button class="add-dashed" onclick="showModal('newTask')">+ 添加任务</button><p class="small-note">每天、工作日、周末三种重复规则;每一天的完成状态分别保存。</p></div>`};
|
||||
|
||||
const firstWalkRender=renderWalk;
|
||||
renderWalk=function(){const latest=state.walkHistory.filter(w=>w.petId===state.activePet).slice(0,2);return firstWalkRender()+`<div class="content">${section('最近的脚印','全部记录',"showModal('walkHistory')")}${latest.length?`<div class="card timeline-card">${latest.map(w=>`<div class="walk-record"><div class="record-badge">🐾</div><div class="record-main"><strong>${esc(w.petName)}的散步</strong><small>${esc(w.date)} · ${Math.max(1,Math.ceil(w.seconds/60))} 分钟</small></div><div class="record-distance">${Number(w.distance).toFixed(2)} km</div></div>`).join('')}</div>`:'<div class="history-empty">完成一次遛狗,脚印就会出现在这里 🐾</div>'}</div>`};
|
||||
finishWalk=function(){if(state.walkStatus==='idle')return;clearInterval(walkTicker);const w={id:Date.now(),petId:state.activePet,petName:pet().name,date:new Date().toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}),seconds:state.walkSeconds,distance:state.walkDistance};state.walkHistory.unshift(w);state.walks++;state.totalKm=+(Number(state.totalKm)+w.distance).toFixed(2);state.walkStatus='idle';state.walkSeconds=0;state.walkDistance=0;state.share=false;save();render();toast(`已保存:${Math.max(1,Math.ceil(w.seconds/60))} 分钟 · ${w.distance.toFixed(2)} km`)};
|
||||
toggleShare=function(){if(state.walkStatus==='idle'){toast('开始遛狗后才能分享位置');return}state.share=!state.share;save();render();toast(state.share?'位置分享已开启':'位置分享已关闭')};
|
||||
|
||||
renderMeet=function(){const ms=state.meetups.filter(m=>filter==='全部'||(filter==='附近'&&parseFloat(m.distance)<=2.5)||(filter==='周末'&&m.time.includes('周'))||(filter==='我参加的'&&m.joined));return `${header('和新朋友一起出门','约个伴,一起遛',`<button class="icon-btn" onclick="showModal('newMeet')" aria-label="发起约遛">${icon('plus')}</button>`)}<div class="content"><div class="filters">${['全部','附近','周末','我参加的'].map(f=>`<button class="filter ${filter===f?'active':''}" onclick="setFilter('${f}')">${f}</button>`).join('')}</div>${ms.map(m=>`<article class="card meet-card"><div class="meet-banner ${m.theme}" onclick="showModal('meetDetail:${m.id}')" style="cursor:pointer"><span class="tag">${esc(m.tag)}</span><span class="scene">${m.emoji}</span></div><h4><button class="detail-link" onclick="showModal('meetDetail:${m.id}')">${esc(m.title)} ›</button></h4><p>📍 ${esc(m.place)} · ${esc(m.distance)}</p><div class="meet-footer"><div class="participants"><span class="face">🐶</span><span class="face">🐕</span><span class="face">🐩</span><span>${esc(m.time)} · ${m.people}/${m.maxPeople} 人</span></div><button class="join-btn ${m.joined?'joined':''}" onclick="showModal('meetDetail:${m.id}')">${m.joined?'查看活动':'查看详情'}</button></div></article>`).join('')||'<div class="card empty">这里暂时没有活动,发起一场吧 🐾</div>'}<button class="add-dashed" onclick="showModal('newMeet')">+ 发起一场约遛</button><p class="small-note">活动和距离为原型演示数据;正式版本需接入地图与真实活动信息。</p></div>`};
|
||||
joinMeet=function(id){const m=state.meetups.find(x=>x.id===id);if(!m)return;if(!m.joined&&m.people>=m.maxPeople){toast('这场活动已经满员');return}m.joined=!m.joined;m.people+=m.joined?1:-1;save();render();toast(m.joined?`已为${pet().name}报名,记得准时出发!`:'已退出约遛')};
|
||||
|
||||
renderPets=function(){const p=pet(),ownWalks=state.walkHistory.filter(w=>walkIds(w).includes(p.id));return `${header('每只毛孩子都独一无二','我的毛孩子',`<button class="icon-btn" onclick="showModal('newPet')" aria-label="添加宠物">${icon('plus')}</button>`)}<div class="content"><div class="card pet-list-card">${state.pets.map(x=>`<div class="pet-list-row ${x.id===p.id?'active':''}" onclick="switchPet(${x.id})"><div class="pet-avatar">${x.emoji}</div><div><h4>${esc(x.name)} ${x.id===p.id?'· 当前':''}</h4><p>${esc(x.breed||x.type)} · ${esc(x.age||'年龄待填写')}</p></div><div class="arrow">${icon('chev')}</div></div>`).join('')}</div>${section('基本档案')}<div class="card profile-card"><div class="profile-top"><div class="profile-big">${p.emoji}</div><div><div class="profile-name">${esc(p.name)}</div><div class="profile-desc">${esc(p.breed||p.type)} · ${esc(p.gender)}</div></div></div><div class="profile-grid"><div class="profile-field"><small>年龄</small><strong>${esc(p.age||'待填写')}</strong></div><div class="profile-field"><small>体重</small><strong>${esc(p.weight||'待填写')}</strong></div><div class="profile-field"><small>生日</small><strong>${esc(p.birthday||'待填写')}</strong></div></div><div class="profile-note">🐾 ${esc(p.note||'还没有备注,记录它的小习惯吧。')}</div><button class="outline-button" onclick="showModal('editPet')">${icon('edit')} 编辑档案</button></div>${section('健康与照护')}<div class="health-grid"><div class="health-item"><small>过敏 / 忌口</small><strong>${esc(p.allergy||'暂无记录')}</strong></div><div class="health-item"><small>疫苗情况</small><strong>${esc(p.vaccine||'暂无记录')}</strong></div></div><div class="card profile-note" style="margin-top:8px;background:#fff;color:#688170">💚 照护提醒:${esc(p.care||'暂无特别提醒')}</div>${section('一起生活')}<div class="stats"><div class="stat card"><small>已记录散步</small><strong>${ownWalks.length}<i>次</i></strong></div><div class="stat card"><small>记录里程</small><strong>${ownWalks.reduce((sum,w)=>sum+Number(w.distance),0).toFixed(2)}<i>km</i></strong></div></div><button class="add-dashed" onclick="showModal('newPet')">+ 添加另一只毛孩子</button></div>`};
|
||||
|
||||
const firstModalRender=renderModal;
|
||||
renderModal=function(){const root=document.getElementById('modalRoot');if(!modal){root.innerHTML='';return}let title='',body='',p=pet();if(modal.startsWith('meetDetail:')){const m=state.meetups.find(x=>x.id===Number(modal.split(':')[1]));if(!m){closeModal();return}title='约遛详情';body=`<div class="detail-hero">${m.emoji}</div><h3 style="margin:0;font-size:19px">${esc(m.title)}</h3><div class="detail-tags"><span>${esc(m.tag)}</span><span>${m.people}/${m.maxPeople} 位伙伴</span><span>距你 ${esc(m.distance)}</span></div><div class="detail-row"><b>集合时间</b><span>${esc(m.time)}</span></div><div class="detail-row"><b>集合地点</b><span>${esc(m.place)}</span></div><div class="detail-row"><b>带谁参加</b><span>${esc(p.name)} · ${esc(p.breed||p.type)} <a href="#" onclick="event.preventDefault();showModal('switchPet')" style="color:var(--green2)">切换</a></span></div><p class="detail-copy">${esc(m.description)}</p><div class="detail-attendees"><strong>出发前的小提醒</strong>带好牵引绳、拾便袋和饮水;毛孩子不舒服时,请及时退出活动。</div><button class="primary" onclick="joinMeet(${m.id})">${m.joined?'退出这场约遛':m.people>=m.maxPeople?'活动已满员':'带'+esc(p.name)+'加入约遛'}</button>`}
|
||||
else if(modal==='walkHistory'){title='遛狗记录';const records=state.walkHistory.filter(w=>w.petId===state.activePet);body=`<div class="profile-grid"><div class="profile-field"><small>已记录</small><strong>${records.length} 次</strong></div><div class="profile-field"><small>记录里程</small><strong>${records.reduce((sum,w)=>sum+Number(w.distance),0).toFixed(2)} km</strong></div><div class="profile-field"><small>正在进行</small><strong>${state.walkStatus==='idle'?'没有':'1 次'}</strong></div></div><div class="task-group-title"><strong>${esc(p.name)}的脚印</strong></div>${records.length?`<div class="timeline-card">${records.map(w=>`<div class="walk-record"><div class="record-badge">🐾</div><div class="record-main"><strong>${esc(w.date)}</strong><small>${Math.max(1,Math.ceil(w.seconds/60))} 分钟</small></div><div class="record-distance">${Number(w.distance).toFixed(2)} km</div></div>`).join('')}</div>`:'<div class="history-empty">还没有记录,完成第一次散步吧 🐾</div>'}`}
|
||||
else if(modal==='newTask'||modal.startsWith('editTask:')){const edit=modal.startsWith('editTask:'),t=edit?state.tasks.find(x=>x.id===Number(modal.split(':')[1])):null;if(edit&&!t){closeModal();return}title=edit?'编辑日常任务':'添加日常任务';body=`<form onsubmit="saveTask(event,${edit?t.id:'null'})"><label class="field"><span>任务名称</span><input name="name" required maxlength="30" value="${edit?esc(t.name):''}" placeholder="例如:晚餐喂食"></label><div class="form-grid"><label class="field"><span>分类</span><select name="category">${['饮食','饮水','活动','护理','其他'].map(v=>`<option ${edit&&t.category===v?'selected':''}>${v}</option>`).join('')}</select></label><label class="field"><span>提醒时间</span><input name="time" type="time" value="${edit?esc(t.time):'18:00'}" required></label></div><label class="field"><span>重复规则</span><select name="repeat">${['每天','工作日','周末'].map(v=>`<option ${edit&&t.repeat===v?'selected':''}>${v}</option>`).join('')}</select></label><div class="tip-card">这项任务属于「${esc(p.name)}」。修改重复规则后,已完成的日期仍会保留。</div><button class="primary" type="submit">${edit?'保存修改':'添加任务'}</button></form>${edit?`<button class="danger-button" onclick="showModal('deleteTask:${t.id}')">删除这项任务</button>`:''}`}
|
||||
else if(modal.startsWith('deleteTask:')){const id=Number(modal.split(':')[1]),t=state.tasks.find(x=>x.id===id);title='删除任务';body=`<p class="detail-copy">确定删除「${esc(t?.name||'这项任务')}」吗?它会从后续计划中移除。</p><div class="two-actions"><button class="soft-button" onclick="closeModal()">取消</button><button class="danger-button" style="margin:0" onclick="deleteTask(${id})">确认删除</button></div>`}
|
||||
else if(modal==='newPet'||modal==='editPet'){const edit=modal==='editPet';title=edit?'编辑宠物档案':'添加毛孩子';body=`<form onsubmit="savePet(event,${edit})"><label class="field"><span>名字</span><input name="name" required maxlength="20" value="${edit?esc(p.name):''}" placeholder="它叫什么?"></label><div class="form-grid"><label class="field"><span>宠物类型</span><select name="type">${['狗狗','猫猫','其他'].map(v=>`<option ${edit&&p.type===v?'selected':''}>${v}</option>`).join('')}</select></label><label class="field"><span>性别</span><select name="gender">${['男孩','女孩','不确定'].map(v=>`<option ${edit&&p.gender===v?'selected':''}>${v}</option>`).join('')}</select></label></div><div class="form-grid"><label class="field"><span>品种</span><input name="breed" maxlength="25" value="${edit?esc(p.breed):''}" placeholder="如:柯基"></label><label class="field"><span>体重</span><input name="weight" maxlength="12" value="${edit?esc(p.weight):''}" placeholder="如:12 kg"></label></div><div class="form-grid"><label class="field"><span>年龄</span><input name="age" maxlength="20" value="${edit?esc(p.age):''}" placeholder="如:2岁"></label><label class="field"><span>生日</span><input name="birthday" type="date" value="${edit?esc(p.birthday):''}"></label></div><label class="field"><span>性格和习惯</span><input name="note" maxlength="100" value="${edit?esc(p.note):''}" placeholder="它喜欢什么?"></label><div class="form-grid"><label class="field"><span>过敏 / 忌口</span><input name="allergy" maxlength="50" value="${edit?esc(p.allergy):''}" placeholder="如:鸡肉"></label><label class="field"><span>疫苗情况</span><input name="vaccine" maxlength="50" value="${edit?esc(p.vaccine):''}" placeholder="如:已接种"></label></div><label class="field"><span>照护提醒</span><input name="care" maxlength="100" value="${edit?esc(p.care):''}" placeholder="如:每天擦脚"></label><button class="primary" type="submit">${edit?'保存档案':'添加宠物'}</button></form>`}
|
||||
else if(modal==='newMeet'){title='发起约遛';body=`<form onsubmit="addMeet(event)"><label class="field"><span>活动名称</span><input name="title" required maxlength="30" placeholder="例如:周末公园散步"></label><label class="field"><span>集合地点</span><input name="place" required maxlength="40" placeholder="例如:世纪公园 3 号门"></label><div class="form-grid"><label class="field"><span>集合时间</span><input name="time" required maxlength="25" placeholder="周六 09:30"></label><label class="field"><span>距离展示</span><input name="distance" required maxlength="12" placeholder="1.2 km"></label></div><div class="form-grid"><label class="field"><span>人数上限</span><input name="maxPeople" type="number" min="2" max="30" value="8" required></label><label class="field"><span>活动标签</span><input name="tag" maxlength="20" placeholder="小型犬友好"></label></div><label class="field"><span>活动说明</span><input name="description" maxlength="120" placeholder="路线、节奏和需要准备的东西"></label><button class="primary" type="submit">发布约遛</button></form><p class="small-note">原型中的活动仅保存在本地浏览器,不会发布给其他人。</p>`}
|
||||
else {firstModalRender();return}
|
||||
root.innerHTML=`<div class="modal-backdrop" onclick="if(event.target===this)closeModal()"><div class="modal"><div class="modal-handle"></div><div class="modal-head"><h3>${title}</h3><button class="close" onclick="closeModal()" aria-label="关闭">×</button></div>${body}</div></div>`};
|
||||
function saveTask(e,id){e.preventDefault();const x=Object.fromEntries(new FormData(e.target)),emojis={饮食:'🍚',饮水:'💧',活动:'🦮',护理:'🪮',其他:'✨'};if(id){Object.assign(state.tasks.find(t=>t.id===id),x,{icon:emojis[x.category]})}else{state.tasks.push({id:Date.now(),petId:state.activePet,...x,icon:emojis[x.category]})}save();modal='';setTab('tasks');toast(id?'任务已更新':'任务已添加')}
|
||||
function deleteTask(id){state.tasks=state.tasks.filter(t=>t.id!==id);for(const key of Object.keys(state.taskCompletions))if(key.startsWith(`${id}:`))delete state.taskCompletions[key];save();modal='';render();toast('任务已删除')}
|
||||
addMeet=function(e){e.preventDefault();const x=Object.fromEntries(new FormData(e.target));const m={id:Date.now(),title:x.title,place:x.place,time:x.time,distance:x.distance,tag:x.tag||'新活动',description:x.description||'轻松散步,一起交朋友。',maxPeople:Number(x.maxPeople),people:1,emoji:'🐕🌿',theme:'green',joined:true};state.meetups.unshift(m);filter='全部';save();modal=`meetDetail:${m.id}`;setTab('meet');toast('约遛已创建,仅供本地预览')};
|
||||
// 一次出行可带多只狗;当前查看的宠物档案不决定遛狗参与者。
|
||||
state.walkPets=Array.isArray(state.walkPets)?state.walkPets:state.pets.filter(p=>p.type==='狗狗').map(p=>p.id);
|
||||
const allDogs=()=>state.pets.filter(p=>p.type==='狗狗');
|
||||
const walkSelectionIds=()=>state.walkStatus==='idle'?state.walkPets:(state.walkActivePetIds||state.walkPets);
|
||||
const selectedDogs=()=>walkSelectionIds().map(id=>state.pets.find(p=>p.id===id)).filter(Boolean);
|
||||
const walkIds=w=>Array.isArray(w.petIds)?w.petIds:(w.petId==null?[]:[w.petId]);
|
||||
const walkNames=w=>Array.isArray(w.petNames)?w.petNames.join('、'):(w.petName||walkIds(w).map(id=>state.pets.find(p=>p.id===id)?.name||'毛孩子').join('、'));
|
||||
function toggleWalkDog(id){if(state.walkStatus!=='idle')return;if(!allDogs().some(p=>p.id===id))return;state.walkPets=state.walkPets.includes(id)?state.walkPets.filter(x=>x!==id):[...state.walkPets,id];save();render()}
|
||||
renderWalk=function(){const running=state.walkStatus==='running',active=state.walkStatus!=='idle',dogs=allDogs(),names=selectedDogs().map(p=>p.name).join('、'),latest=state.walkHistory.slice(0,2);return `${header('和毛孩子一起探索城市','一起去遛狗',`<button class="icon-btn" onclick="showModal('walkHistory')" aria-label="遛狗记录">${icon('walk')}</button>`)}<div class="map-wrap">${mapSVG}<div class="map-overlay"><div class="map-badge"><i></i>${running?'正在记录路线':active?'路线已暂停':'附近适合散步'}</div><button class="map-control" onclick="toast('已定位到附近公园(原型示意地图)')" aria-label="定位">⌖</button></div></div><div class="card walk-panel"><h3>${running?`和${esc(names)}在路上!`:active?'休息一下,等你们回来':'一起出门走走吧'}</h3><p class="subtitle">${active?'这一段路,属于所有同行的狗狗':'出发前选好这次同行的狗狗,可以多选'}</p><div class="walk-party"><div class="walk-party-head"><strong>这次带谁?</strong><small>已选 ${selectedDogs().length}/${dogs.length} 只狗狗</small></div><div class="walk-party-list">${dogs.map(d=>`<button class="walk-dog ${walkSelectionIds().includes(d.id)?'selected':''}" onclick="toggleWalkDog(${d.id})" ${active?'disabled':''} aria-pressed="${walkSelectionIds().includes(d.id)}"><b>${d.emoji}</b>${esc(d.name)}<span>${walkSelectionIds().includes(d.id)?'✓':'+'}</span></button>`).join('')||'<div class="walk-party-note">先到宠物档案添加一只狗狗吧。</div>'}</div><div class="walk-party-note">${active?'散步中已锁定同行成员,结束后可以重新选择':'一次散步只记一条路线,记录会关联所有同行狗狗。'}</div></div><div class="walk-numbers"><div><strong id="walkTime">${timeDisplay()}</strong><small>时长</small></div><div><strong id="walkDistance">${state.walkDistance.toFixed(2)}</strong><small>公里</small></div><div><strong id="walkPace">${state.walkDistance?Math.max(4,Math.round(state.walkSeconds/60/state.walkDistance)):'--'}</strong><small>分钟 / 公里</small></div></div><div class="walk-actions">${active?`<button class="soft-button" onclick="pauseWalk()">${icon(running?'pause':'play')} ${running?'暂停':'继续'}</button><button class="primary" onclick="finishWalk()">结束并保存</button>`:`<button class="primary" onclick="startWalk()">${icon('play')} 带${selectedDogs().length||''}只狗狗出发</button>`}</div><div class="share-line"><div>分享实时位置<small>${state.share?'已开启 · 结束后自动关闭':'仅在遛狗时可主动开启'}</small></div><button class="switch ${state.share?'on':''}" onclick="toggleShare()" aria-label="切换位置分享" aria-pressed="${state.share}"></button></div><div class="tip-card">${state.share?'🟢 位置分享已开启,结束遛狗后会自动停止。':'🔒 原型使用示意地图,不读取真实位置。'}</div></div><div class="content">${section('最近的脚印','全部记录',"showModal('walkHistory')")}${latest.length?`<div class="card timeline-card">${latest.map(w=>`<div class="walk-record"><div class="record-badge">🐾</div><div class="record-main"><strong>${esc(walkNames(w))}一起散步</strong><small>${esc(w.date)} · ${Math.max(1,Math.ceil(w.seconds/60))} 分钟</small></div><div class="record-distance">${Number(w.distance).toFixed(2)} km</div></div>`).join('')}</div>`:'<div class="history-empty">完成一次遛狗,脚印就会出现在这里 🐾</div>'}${section('附近好去处')}<div class="card meet-mini" onclick="setTab('meet')" style="cursor:pointer"><div class="meet-photo">🌳</div><div><div class="meet-title">世纪公园 · 中心草坪</div><div class="meet-meta">开阔草地 · 适合小型犬<br>步行约 12 分钟</div><span class="pill">发现附近约遛活动 →</span></div></div></div>`};
|
||||
startWalk=function(){if(!selectedDogs().length){toast('请先选择至少一只狗狗');return}state.walkActivePetIds=[...state.walkPets];state.walkStatus='running';state.walkSeconds=0;state.walkDistance=0;state.share=false;clearInterval(walkTicker);walkTicker=setInterval(tick,1000);save();render();toast(`${selectedDogs().map(p=>p.name).join('、')}出发啦!`)};
|
||||
finishWalk=function(){if(state.walkStatus==='idle')return;clearInterval(walkTicker);const dogs=selectedDogs(),w={id:Date.now(),petIds:dogs.map(p=>p.id),petNames:dogs.map(p=>p.name),date:new Date().toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}),seconds:state.walkSeconds,distance:state.walkDistance};state.walkHistory.unshift(w);state.walks++;state.totalKm=+(Number(state.totalKm)+w.distance).toFixed(2);state.walkStatus='idle';state.walkActivePetIds=[];state.walkSeconds=0;state.walkDistance=0;state.share=false;save();render();toast(`已给 ${dogs.length} 只狗狗保存同一次散步`)};
|
||||
const modalWithDetails=renderModal;
|
||||
renderModal=function(){if(modal!=='walkHistory'){modalWithDetails();return}const records=state.walkHistory;const body=`<div class="profile-grid"><div class="profile-field"><small>已记录</small><strong>${records.length} 次</strong></div><div class="profile-field"><small>记录里程</small><strong>${records.reduce((sum,w)=>sum+Number(w.distance),0).toFixed(2)} km</strong></div><div class="profile-field"><small>正在进行</small><strong>${state.walkStatus==='idle'?'没有':'1 次'}</strong></div></div><div class="task-group-title"><strong>一起走过的路</strong></div>${records.length?`<div class="timeline-card">${records.map(w=>`<div class="walk-record"><div class="record-badge">🐾</div><div class="record-main"><strong>${esc(walkNames(w))}</strong><small>${esc(w.date)} · ${Math.max(1,Math.ceil(w.seconds/60))} 分钟</small></div><div class="record-distance">${Number(w.distance).toFixed(2)} km</div></div>`).join('')}</div>`:'<div class="history-empty">还没有记录,带狗狗们出门走走吧 🐾</div>'}`;document.getElementById('modalRoot').innerHTML=`<div class="modal-backdrop" onclick="if(event.target===this)closeModal()"><div class="modal"><div class="modal-handle"></div><div class="modal-head"><h3>遛狗记录</h3><button class="close" onclick="closeModal()" aria-label="关闭">×</button></div>${body}</div></div>`};
|
||||
const savePetBeforeWalkSelection=savePet;
|
||||
savePet=function(e,edit){const beforeType=edit?pet().type:null;savePetBeforeWalkSelection(e,edit);if(pet().type==='狗狗'&&(!edit||beforeType!=='狗狗')&&!state.walkPets.includes(pet().id))state.walkPets.push(pet().id);if(state.walkStatus==='idle')state.walkPets=state.walkPets.filter(id=>allDogs().some(p=>p.id===id));save();render()};
|
||||
const homeBeforeWalkChoice=renderHome;
|
||||
renderHome=function(){const p=pet(),dogs=allDogs(),selected=selectedDogs();let html=homeBeforeWalkChoice();html=html.replace('带它出去走走','带狗狗们出去走走');if(p.type!=='狗狗')html=html.replace(`<h2>${esc(p.name)},出发吧!</h2>`,`<h2>${esc(p.name)},今天也开心!</h2>`);const start=html.indexOf('<div class="card walk-card">'),end=html.indexOf('<div class="stats">',start);if(start<0||end<0)return html;const card=`<div class="card home-walk-card"><div class="home-walk-intro"><div class="walk-illustration"><span>🦮</span></div><div><h4>这次带谁一起遛?</h4><p>可以同时选多只狗,一次出门只记一条路线。</p></div></div><div class="home-walk-pets">${dogs.map(d=>`<button class="walk-dog ${state.walkPets.includes(d.id)?'selected':''}" onclick="toggleWalkDog(${d.id})" aria-pressed="${state.walkPets.includes(d.id)}"><b>${d.emoji}</b>${esc(d.name)}<span>${state.walkPets.includes(d.id)?'✓':'+'}</span></button>`).join('')}<button class="walk-dog add-dog" onclick="showModal('newPet')">+ 添加狗狗</button></div><button class="primary home-walk-start" onclick="beginWalkFromHome()">${icon('walk')} 带 ${selected.length} 只狗狗出发</button></div>`;return html.slice(0,start)+card+html.slice(end)};
|
||||
function beginWalkFromHome(){if(state.walkStatus!=='idle'){setTab('walk');return}if(!selectedDogs().length){toast('请先选一只狗狗,或添加新狗狗');return}setTab('walk');startWalk()}
|
||||
const savePetBeforeHomeReturn=savePet;
|
||||
savePet=function(e,edit){const fromHome=tab==='home';savePetBeforeHomeReturn(e,edit);if(fromHome&&!edit)setTab('home')};
|
||||
const walkWithLowerChoice=renderWalk;
|
||||
renderWalk=function(){let html=walkWithLowerChoice(),start=html.indexOf('<div class="walk-party">'),end=html.indexOf('<div class="walk-numbers">',start);if(start<0||end<0)return html;const chooser=html.slice(start,end);html=html.slice(0,start)+html.slice(end);return html.replace('<div class="map-wrap">',`<div class="content"><div class="card walk-choice-top">${chooser}</div></div><div class="map-wrap">`)};
|
||||
const homeWithWalkChoice=renderHome;
|
||||
renderHome=function(){let html=homeWithWalkChoice();if(state.walkStatus!=='idle')html=html.replace(`带 ${selectedDogs().length} 只狗狗出发`,'查看正在进行的遛狗');return html};
|
||||
// 同一张五柯基插画按位置裁出头像,保持首页、选择器和档案形象一致。
|
||||
function corgiIndex(p){const dogs=state.pets.filter(x=>x.type==='狗狗');return Math.max(0,dogs.findIndex(x=>x.id===p.id))%5}
|
||||
function corgiThumb(p,extra=''){return `<span class="corgi-crop ${extra}" style="--corgi-x:${corgiIndex(p)*25}%" aria-label="${esc(p.name)}的柯基头像"></span>`}
|
||||
function decorateWalkChips(html){return html.replace(/(<button class="walk-dog[^\"]*" onclick="toggleWalkDog\((\d+)\)"[^>]*><b>)[^<]*(<\/b>)/g,(all,left,id,right)=>{const dog=state.pets.find(p=>p.id===Number(id));return dog?left+corgiThumb(dog)+right:all})}
|
||||
const homeBeforeCorgis=renderHome;
|
||||
renderHome=function(){let html=decorateWalkChips(homeBeforeCorgis());if(pet().type==='狗狗')html=html.replace(/(<button class="avatar-mini"[^>]*>)[^<]*(<\/button>)/,(all,left,right)=>left+corgiThumb(pet())+right);else html=html.replace('class="hero card"','class="hero card cat-hero"');return html};
|
||||
const walkBeforeCorgis=renderWalk;
|
||||
renderWalk=function(){return decorateWalkChips(walkBeforeCorgis())};
|
||||
const petsBeforeCorgis=renderPets;
|
||||
renderPets=function(){let index=0,html=petsBeforeCorgis();html=html.replace(/<div class="pet-avatar">[^<]*<\/div>/g,all=>{const p=state.pets[index++];return p?.type==='狗狗'?`<div class="pet-avatar corgi-crop" style="--corgi-x:${corgiIndex(p)*25}%" aria-label="${esc(p.name)}的柯基头像"></div>`:all});if(pet().type==='狗狗')html=html.replace(/<div class="profile-big">[^<]*<\/div>/,`<div class="profile-big corgi-crop" style="--corgi-x:${corgiIndex(pet())*25}%" aria-label="${esc(pet().name)}的柯基头像"></div>`);return html};
|
||||
const modalBeforeCorgis=renderModal;
|
||||
renderModal=function(){modalBeforeCorgis();if(modal!=='switchPet')return;let index=0,root=document.getElementById('modalRoot');root.innerHTML=root.innerHTML.replace(/<div class="pet-avatar">[^<]*<\/div>/g,all=>{const p=state.pets[index++];return p?.type==='狗狗'?`<div class="pet-avatar corgi-crop" style="--corgi-x:${corgiIndex(p)*25}%"></div>`:all})};
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user