Files
sundynix-pets/pet-walk-prototype.html
T

164 lines
78 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>