This commit is contained in:
2026-06-25 18:24:29 +08:00
commit a1190d7d9a
90 changed files with 5459 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
// pages/dog-detail/dog-detail — 4 狗狗主页
// 累计只用 次数/公里/连续天数成就墙露灰锁docs/05 R8, R10
const { call } = require('../../utils/cloud.js');
const { ACHIEVEMENTS, TOTAL } = require('../../utils/achievements.js');
const { formatDurationCN, formatDistance, toDateStr } = require('../../utils/format.js');
const GENDER_TEXT = { male: '公', female: '母' };
Page({
data: {
loading: true,
dog: null,
stats: null,
metaText: '',
wall: [],
unlockedCount: 0,
total: TOTAL,
recent: [],
},
onLoad(o) { this.dogId = o.id; },
onShow() { if (this.dogId) this.load(); },
async load() {
const res = await call('getDogDetail', { dogId: this.dogId });
if (!res.ok) return;
const { dog, achievements, recentWalks } = res.data;
const metaParts = [dog.breed];
if (dog.ageText) metaParts.push(dog.ageText);
if (GENDER_TEXT[dog.gender]) metaParts.push(GENDER_TEXT[dog.gender]);
const unlockedKeys = new Set(achievements.unlocked.map((u) => u.achievementKey));
const wall = ACHIEVEMENTS.map((a) => ({
key: a.key, name: a.name, unlocked: unlockedKeys.has(a.key),
}));
const recent = recentWalks.map((w) => ({
_id: w._id,
title: `${toDateStr(w.startAt)}${w.isManual ? ' · 补记' : ''}`,
sub: `${formatDurationCN(w.duration)} · ${formatDistance(w.distance)}` +
(w.dogSteps != null ? ` · ${w.dogSteps}狗步` : ''),
}));
this.setData({
loading: false,
dog,
stats: dog.stats || {},
metaText: metaParts.join(' · '),
wall,
unlockedCount: achievements.unlocked.length,
total: achievements.total,
recent,
});
},
goEdit() { wx.navigateTo({ url: `/pages/dog-edit/dog-edit?id=${this.dogId}` }); },
goAchievements() { wx.navigateTo({ url: `/pages/achievements/achievements?dogId=${this.dogId}` }); },
});

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "狗狗主页"
}

View File

@@ -0,0 +1,42 @@
<view wx:if="{{!loading}}" class="page">
<!-- 头部 -->
<view class="header">
<view class="edit" bindtap="goEdit">编辑</view>
<image wx:if="{{dog.avatar}}" class="dog-avatar" src="{{dog.avatar}}" mode="aspectFill" />
<view wx:else class="dog-avatar">🐶</view>
<view class="dog-name">{{dog.name}}</view>
<view class="dog-meta">{{metaText}}</view>
</view>
<!-- 累计:次数/公里/连续 -->
<view class="grid3">
<view class="g"><view class="g-v">{{stats.totalWalks || 0}}</view><view class="g-k">总次数</view></view>
<view class="g"><view class="g-v">{{stats.totalDistance || 0}}</view><view class="g-k">总公里</view></view>
<view class="g"><view class="g-v streak">{{stats.currentStreak || 0}}</view><view class="g-k">连续天数</view></view>
</view>
<!-- 成就墙 -->
<view class="row-head" bindtap="goAchievements">
<text class="rh-title">成就墙</text>
<text class="rh-more">{{unlockedCount}} / {{total}} </text>
</view>
<view class="wall">
<view wx:for="{{wall}}" wx:key="key" class="medal-item {{item.unlocked?'':'locked'}}">
<view class="medal">{{item.unlocked ? '🏅' : '🔒'}}</view>
<view class="medal-name">{{item.name}}</view>
</view>
</view>
<!-- 近期记录 -->
<view class="row-head"><text class="rh-title">近期记录</text></view>
<view wx:if="{{recent.length}}" class="recent">
<view wx:for="{{recent}}" wx:key="_id" class="walk-row">
<view class="wr-ico">🐾</view>
<view class="wr-body">
<view class="wr-title">{{item.title}}</view>
<view class="wr-sub">{{item.sub}}</view>
</view>
</view>
</view>
<view wx:else class="empty">还没有遛狗记录,点底部按钮带它出门吧</view>
</view>

View File

@@ -0,0 +1,33 @@
.page { min-height: 100vh; padding-bottom: calc(180rpx + env(safe-area-inset-bottom)); }
.header { position: relative; text-align: center; padding: 32rpx; border-bottom: 1rpx solid var(--line); }
.edit { position: absolute; top: 28rpx; right: 32rpx; font-size: 26rpx; color: var(--accent-deep); }
.dog-avatar { width: 124rpx; height: 124rpx; border-radius: 50%; background: var(--surface-2); display: flex; align-items: center; justify-content: center; font-size: 60rpx; margin: 0 auto 14rpx; }
.dog-name { font-size: 34rpx; font-weight: 600; }
.dog-meta { font-size: 24rpx; color: var(--ink-2); margin-top: 6rpx; }
.grid3 { display: flex; padding: 28rpx 12rpx; border-bottom: 1rpx solid var(--line); }
.g { flex: 1; text-align: center; }
.g-v { font-size: 40rpx; font-weight: 600; }
.g-v.streak { color: var(--success); }
.g-k { font-size: 22rpx; color: var(--ink-2); margin-top: 4rpx; }
.row-head { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx 12rpx; }
.rh-title { font-size: 26rpx; font-weight: 600; }
.rh-more { font-size: 22rpx; color: var(--accent-deep); }
.wall { display: flex; flex-wrap: wrap; padding: 0 24rpx 20rpx; border-bottom: 1rpx solid var(--line); }
.medal-item { width: 25%; display: flex; flex-direction: column; align-items: center; gap: 8rpx; padding: 12rpx 0; }
.medal { width: 84rpx; height: 84rpx; border-radius: 50%; background: var(--surface-2); display: flex; align-items: center; justify-content: center; font-size: var(--icon-size); line-height: 1; }
.medal-item.locked .medal { opacity: 0.55; }
.medal-name { font-size: 20rpx; color: var(--ink-2); }
.medal-item.locked .medal-name { color: var(--ink-3); }
.recent { padding: 0 32rpx; display: flex; flex-direction: column; gap: 16rpx; }
.walk-row { display: flex; align-items: center; gap: 18rpx; padding: 22rpx; background: var(--surface-2); border-radius: var(--radius-sm); }
.wr-ico { width: var(--icon-size); font-size: var(--icon-size); line-height: 1; text-align: center; }
.wr-body { flex: 1; }
.wr-title { font-size: 26rpx; }
.wr-sub { font-size: 22rpx; color: var(--ink-2); margin-top: 4rpx; }
.empty { text-align: center; font-size: 24rpx; color: var(--ink-3); padding: 40rpx 32rpx; }