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,198 @@
// pages/summary/summary — 3 成果卡
// 「狗狗走了」视角补充字段全选填R7Canvas 生成分享图;订阅提醒
const { call } = require('../../utils/cloud.js');
const store = require('../../utils/store.js');
const { formatDuration } = require('../../utils/format.js');
const { chooseAndUpload } = require('../../utils/media.js');
const { SUBSCRIBE_TEMPLATE_ID } = require('../../utils/config.js');
Page({
data: {
loaded: false,
walkId: '',
primaryDogId: '',
dogName: '',
dogAvatar: '',
headline: '',
bigValue: '',
bigUnit: '',
timeText: '',
distanceText: '—',
streak: 0,
newAchievements: [],
weathers: [{ k: 'sunny', l: '☀️ 晴' }, { k: 'cloudy', l: '☁️ 阴' }, { k: 'rainy', l: '🌧️ 雨' }],
moods: [{ k: 'happy', l: '遛得开心' }, { k: 'tired', l: '有点累' }, { k: 'excited', l: '很兴奋' }],
weather: '',
mood: '',
note: '',
photos: [],
submitting: false,
},
_attached: false,
onLoad() {
const sum = getApp().globalData.lastSummary;
if (!sum) {
wx.showToast({ title: '数据缺失', icon: 'none' });
setTimeout(() => wx.switchTab({ url: '/pages/home/home' }), 1000);
return;
}
const r = (sum.results && sum.results[0]) || {};
const hasSteps = r.dogSteps != null;
const dog = (store.getState().dogs || []).find((d) => d._id === r.dogId);
this.setData({
loaded: true,
walkId: sum.walkId,
primaryDogId: r.dogId || '',
dogName: r.name || '狗狗',
dogAvatar: (dog && dog.avatar) || '',
headline: hasSteps ? `${r.name}今天走了` : `${r.name}今天遛了`,
bigValue: hasSteps ? String(r.dogSteps) : formatDuration(r.duration || 0),
bigUnit: hasSteps ? '狗步' : '',
timeText: formatDuration(r.duration || 0),
distanceText: r.distance != null ? Number(r.distance).toFixed(1) : '—',
streak: r.streak || 0,
newAchievements: sum.newAchievements || [],
});
},
pickWeather(e) { const k = e.currentTarget.dataset.k; this.setData({ weather: this.data.weather === k ? '' : k }); },
pickMood(e) { const k = e.currentTarget.dataset.k; this.setData({ mood: this.data.mood === k ? '' : k }); },
onNote(e) { this.setData({ note: e.detail.value }); },
async onPickPhoto() {
const fileID = await chooseAndUpload('walk-photos');
if (fileID) this.setData({ photos: [fileID] });
},
// 选填字段回写(只回写一次;含雨天战士成就判定)
async ensureAttached() {
if (this._attached) return;
const { weather, mood, note, photos, walkId } = this.data;
// 无任何选填内容时直接返回,但不锁定:用户可能稍后再补填,待真正回写后才置位
if (!weather && !mood && !note && !photos.length) return;
const res = await call('walkSave', {
action: 'attach', walkId,
weather: weather || null, mood: mood || null, note: note || null, photos,
});
this._attached = true;
if (res && res.ok && res.data.newAchievements && res.data.newAchievements.length) {
wx.showToast({ title: `解锁 ${res.data.newAchievements[0].name}`, icon: 'none' });
}
},
// ===== Canvas 生成分享图 =====
drawShareCard() {
return new Promise((resolve) => {
const q = wx.createSelectorQuery();
q.select('#shareCanvas').fields({ node: true, size: true }).exec((res) => {
if (!res[0] || !res[0].node) return resolve(null);
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = (wx.getWindowInfo && wx.getWindowInfo().pixelRatio) || 2;
const W = 600, H = 820;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.scale(dpr, dpr);
// 背景
ctx.fillStyle = '#FBF8F3';
ctx.fillRect(0, 0, W, H);
// 顶部色带
ctx.fillStyle = '#E8A04B';
ctx.fillRect(0, 0, W, 12);
// 头像圆
ctx.fillStyle = '#F7E5CC';
ctx.beginPath();
ctx.arc(W / 2, 150, 70, 0, Math.PI * 2);
ctx.fill();
ctx.textAlign = 'center';
ctx.fillStyle = '#2E2A24';
ctx.font = '600 34px sans-serif';
ctx.fillText(this.data.headline, W / 2, 290);
ctx.fillStyle = '#C77E2E';
ctx.font = '700 96px sans-serif';
ctx.fillText(this.data.bigValue, W / 2, 400);
if (this.data.bigUnit) {
ctx.fillStyle = '#6E665B';
ctx.font = '500 28px sans-serif';
ctx.fillText(this.data.bigUnit, W / 2, 445);
}
// 三宫格
const cols = [
{ v: this.data.timeText, k: '时长' },
{ v: this.data.distanceText, k: '公里' },
{ v: `${this.data.streak}`, k: '连续' },
];
const baseY = 560;
cols.forEach((c, i) => {
const x = (W / 4) * (i + 1);
ctx.fillStyle = '#2E2A24';
ctx.font = '600 36px sans-serif';
ctx.fillText(c.v, x, baseY);
ctx.fillStyle = '#A89F92';
ctx.font = '400 24px sans-serif';
ctx.fillText(c.k, x, baseY + 40);
});
// 页脚
ctx.fillStyle = '#A89F92';
ctx.font = '500 26px sans-serif';
ctx.fillText('汪圈 · 每一次遛狗都值得被记录', W / 2, 760);
wx.canvasToTempFilePath({
canvas,
success: (r) => resolve(r.tempFilePath),
fail: () => resolve(null),
});
});
});
},
async onShare() {
if (this.data.submitting) return;
this.setData({ submitting: true });
await this.ensureAttached();
const path = await this.drawShareCard();
this.setData({ submitting: false });
if (!path) return wx.showToast({ title: '生成失败', icon: 'none' });
wx.showShareImageMenu({ path }); // 用户可分享到朋友圈/好友或保存
},
// 订阅「遛狗提醒」(模板未配置时静默跳过)
requestSubscribe() {
if (!SUBSCRIBE_TEMPLATE_ID) return Promise.resolve();
return new Promise((resolve) => {
wx.requestSubscribeMessage({ tmplIds: [SUBSCRIBE_TEMPLATE_ID], complete: resolve });
});
},
async onDone() {
if (this.data.submitting) return;
this.setData({ submitting: true });
await this.requestSubscribe(); // 需在用户点击手势内触发
await this.ensureAttached();
this.setData({ submitting: false });
this.sediment();
},
// 转发好友 / 朋友圈(小程序卡片)
onShareAppMessage() {
return { title: `${this.data.dogName}今天的遛狗成果`, path: '/pages/login/login' };
},
onShareTimeline() {
return { title: `${this.data.dogName}今天也出门遛了一圈` };
},
onClose() { wx.switchTab({ url: '/pages/home/home' }); },
sediment() {
const id = this.data.primaryDogId;
if (id) wx.redirectTo({ url: `/pages/dog-detail/dog-detail?id=${id}` });
else wx.switchTab({ url: '/pages/home/home' });
},
});

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "今日成果"
}

View File

@@ -0,0 +1,55 @@
<view wx:if="{{loaded}}" class="page">
<view class="close" bindtap="onClose">✕</view>
<!-- 头部成果 -->
<view class="hero">
<image wx:if="{{dogAvatar}}" class="hero-avatar" src="{{dogAvatar}}" mode="aspectFill" />
<view wx:else class="hero-avatar">🐶</view>
<view class="hero-headline">{{headline}}</view>
<view class="hero-big"><text class="big-v">{{bigValue}}</text><text wx:if="{{bigUnit}}" class="big-u"> {{bigUnit}}</text></view>
</view>
<!-- 三宫格 -->
<view class="grid3">
<view class="g"><view class="g-v">{{timeText}}</view><view class="g-k">时长</view></view>
<view class="g"><view class="g-v">{{distanceText}}</view><view class="g-k">公里</view></view>
<view class="g"><view class="g-v streak">第{{streak}}天</view><view class="g-k">连续</view></view>
</view>
<!-- 解锁成就 -->
<view wx:for="{{newAchievements}}" wx:key="key" class="ach-banner">
<text class="ach-medal">🏅</text>
<view><view class="ach-name">解锁成就 · {{item.name}}</view></view>
</view>
<!-- 选填补充 -->
<view class="section">补充(可选,跳过也能保存)</view>
<view class="note-row">
<view class="photo-add" bindtap="onPickPhoto">
<image wx:if="{{photos.length}}" class="photo-thumb" src="{{photos[0]}}" mode="aspectFill" />
<text wx:else>📷</text>
</view>
<input class="note-input" placeholder="给这次遛狗写点什么…" value="{{note}}" bindinput="onNote" />
</view>
<view class="field">
<view class="flabel">天气</view>
<view class="chips">
<view wx:for="{{weathers}}" wx:key="k" class="chip {{weather===item.k?'sel':''}}" data-k="{{item.k}}" bindtap="pickWeather">{{item.l}}</view>
</view>
</view>
<view class="field">
<view class="flabel">状态</view>
<view class="chips">
<view wx:for="{{moods}}" wx:key="k" class="chip {{mood===item.k?'sel':''}}" data-k="{{item.k}}" bindtap="pickMood">{{item.l}}</view>
</view>
</view>
<view class="actions">
<button class="btn primary" bindtap="onShare">分享图片到朋友圈</button>
<button class="btn ghost" bindtap="onDone" loading="{{submitting}}">完成</button>
</view>
<!-- 离屏 Canvas生成分享图用 -->
<canvas type="2d" id="shareCanvas" class="share-canvas"></canvas>
</view>

View File

@@ -0,0 +1,34 @@
.page { min-height: 100vh; padding-bottom: calc(48rpx + env(safe-area-inset-bottom)); position: relative; }
.close { position: absolute; top: 24rpx; right: 32rpx; font-size: 36rpx; color: var(--ink-3); z-index: 2; }
.hero { text-align: center; padding: 40rpx 32rpx 24rpx; border-bottom: 1rpx solid var(--line); }
.hero-avatar { width: 116rpx; height: 116rpx; border-radius: 50%; background: var(--surface-2); display: flex; align-items: center; justify-content: center; font-size: 56rpx; margin: 0 auto 16rpx; }
.hero-headline { font-size: 30rpx; font-weight: 600; }
.hero-big { margin-top: 8rpx; }
.big-v { font-size: 72rpx; font-weight: 600; font-variant-numeric: tabular-nums; }
.big-u { font-size: 30rpx; color: var(--ink-2); }
.grid3 { display: flex; padding: 28rpx 12rpx; }
.g { flex: 1; text-align: center; }
.g-v { font-size: 36rpx; font-weight: 600; }
.g-v.streak { color: var(--success); }
.g-k { font-size: 22rpx; color: var(--ink-2); margin-top: 4rpx; }
.ach-banner { display: flex; align-items: center; gap: 18rpx; margin: 0 32rpx 20rpx; padding: 22rpx 24rpx; background: var(--accent-soft); border-radius: var(--radius-sm); }
.ach-medal { font-size: var(--icon-size); line-height: 1; }
.ach-name { font-size: 26rpx; font-weight: 600; color: var(--accent-deep); }
.section { font-size: 24rpx; color: var(--ink-2); padding: 24rpx 32rpx 8rpx; }
.note-row { display: flex; align-items: center; gap: 16rpx; padding: 0 32rpx 16rpx; }
.photo-add { width: 128rpx; height: 128rpx; border-radius: 24rpx; border: 1rpx dashed var(--line-2); display: flex; align-items: center; justify-content: center; font-size: var(--icon-size); color: var(--ink-3); overflow: hidden; line-height: 1; }
.photo-thumb { width: 128rpx; height: 128rpx; }
.share-canvas { position: fixed; left: -9999rpx; top: 0; width: 600px; height: 820px; }
.note-input { flex: 1; height: var(--input-height); min-height: var(--input-height); background: var(--surface-2); border-radius: var(--input-radius); padding: var(--input-padding-y) var(--input-padding-x); font-size: var(--input-font-size); line-height: 1.4; }
.field { padding: 22rpx 32rpx; border-top: 1rpx solid var(--line); }
.flabel { font-size: var(--form-label-size); color: var(--ink-2); margin-bottom: 12rpx; }
.chips { display: flex; flex-wrap: wrap; gap: 16rpx; }
.chip { font-size: var(--chip-font-size); padding: var(--chip-padding-y) var(--chip-padding-x); border-radius: var(--radius-pill); border: 1rpx solid var(--line-2); background: var(--surface-2); line-height: 1.4; }
.chip.sel { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-deep); font-weight: 600; }
.actions { display: flex; flex-direction: column; gap: 20rpx; padding: 36rpx 32rpx 0; }