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,62 @@
// components/manual-add-sheet — 手动补记弹层docs/05 R7
// 用法:父页放 <manual-add-sheet id="addSheet" dogs="{{dogs}}" bind:saved="onSaved"/>
// this.selectComponent('#addSheet').open()
const { call } = require('../../utils/cloud.js');
function pad(n) { return String(n).padStart(2, '0'); }
Component({
properties: {
dogs: { type: Array, value: [] },
},
data: {
show: false,
dogId: '',
date: '',
time: '18:00',
durationMin: '',
distance: '',
submitting: false,
},
methods: {
open() {
const dogs = this.data.dogs;
const t = new Date();
const date = `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`;
this.setData({
show: true,
dogId: dogs[0] ? dogs[0]._id : '',
date, time: '18:00', durationMin: '', distance: '',
});
},
close() { this.setData({ show: false }); },
noop() {},
pickDog(e) { this.setData({ dogId: e.currentTarget.dataset.id }); },
onDate(e) { this.setData({ date: e.detail.value }); },
onTime(e) { this.setData({ time: e.detail.value }); },
onDuration(e) { this.setData({ durationMin: e.detail.value }); },
onDistance(e) { this.setData({ distance: e.detail.value }); },
async save() {
const { dogId, date, time, durationMin, distance, submitting } = this.data;
if (submitting) return;
if (!dogId) return wx.showToast({ title: '选择狗狗', icon: 'none' });
if (!durationMin || Number(durationMin) <= 0) return wx.showToast({ title: '填写时长', icon: 'none' });
const startAt = new Date(`${date}T${time || '00:00'}:00`).getTime();
this.setData({ submitting: true });
const res = await call('walkManual', {
dogId,
startAt,
duration: Math.round(Number(durationMin) * 60),
distance: distance ? Number(distance) : null,
}, { loading: true, loadingText: '保存中' });
this.setData({ submitting: false });
if (!res.ok) return;
this.setData({ show: false });
wx.showToast({ title: '补记成功', icon: 'success' });
this.triggerEvent('saved');
},
},
});

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,47 @@
<view class="overlay {{show?'show':''}}" bindtap="close">
<view class="sheet" catchtap="noop">
<view class="handle"></view>
<view class="title">手动补记</view>
<view class="subtitle">忘记开 App 也能补上这次遛狗</view>
<scroll-view scroll-y class="sheet-body">
<view class="field">
<view class="flabel">哪只狗</view>
<view class="chips">
<view wx:for="{{dogs}}" wx:key="_id" class="chip {{dogId===item._id?'sel':''}}" data-id="{{item._id}}" bindtap="pickDog">{{item.name}}</view>
</view>
</view>
<view class="field">
<view class="flabel">日期</view>
<picker mode="date" value="{{date}}" bindchange="onDate">
<view class="fpick">{{date}} <text class="chev"></text></view>
</picker>
</view>
<view class="field">
<view class="flabel">时间</view>
<picker mode="time" value="{{time}}" bindchange="onTime">
<view class="fpick">{{time}} <text class="chev"></text></view>
</picker>
</view>
<view class="field">
<view class="flabel">时长(分钟)</view>
<input class="finput" type="digit" placeholder="如 15" value="{{durationMin}}" bindinput="onDuration" />
</view>
<view class="field last">
<view class="flabel">距离(公里,可选)</view>
<input class="finput" type="digit" placeholder="可不填" value="{{distance}}" bindinput="onDistance" />
</view>
<view class="rule">补记不影响连续打卡,但不计入排行榜。</view>
</scroll-view>
<view class="btns">
<button class="btn ghost" bindtap="close">取消</button>
<button class="btn primary" bindtap="save" loading="{{submitting}}">保存补记</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,59 @@
.overlay {
font-family: var(--font);
position: fixed; inset: 0; z-index: 60;
background: rgba(46,42,36,0.42);
display: flex; align-items: flex-end;
opacity: 0; pointer-events: none; transition: opacity .22s;
}
.overlay.show { opacity: 1; pointer-events: auto; }
.sheet {
width: 100%;
background: var(--surface);
border-radius: 44rpx 44rpx 0 0;
padding: 16rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
transform: translateY(40rpx); transition: transform .26s;
max-height: 88vh;
display: flex; flex-direction: column; /* 头部/底部固定,中间表单滚动 */
}
.overlay.show .sheet { transform: translateY(0); }
.handle { flex-shrink: 0; width: 72rpx; height: 8rpx; border-radius: 4rpx; background: var(--line-2); margin: 12rpx auto 24rpx; }
.title { flex-shrink: 0; font-size: 32rpx; font-weight: 600; text-align: center; }
.subtitle { flex-shrink: 0; font-size: 22rpx; color: var(--ink-2); text-align: center; margin: 8rpx 0 20rpx; }
/* 可滚动表单区:内容再多,底部按钮也不会被挤出可视区 */
.sheet-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.field { padding: 22rpx 0; border-bottom: 1rpx solid var(--line); }
.field.last { border-bottom: none; }
.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; }
.fpick { min-height: var(--input-height); font-size: var(--input-font-size); background: var(--surface-2); border-radius: var(--input-radius); padding: var(--input-padding-y) var(--input-padding-x); line-height: 1.4; display: flex; align-items: center; justify-content: space-between; }
.fpick .chev { color: var(--ink-3); font-size: var(--icon-size-sm); line-height: 1; }
.finput { height: var(--input-height); min-height: var(--input-height); font-size: var(--input-font-size); background: var(--surface-2); border-radius: var(--input-radius); padding: var(--input-padding-y) var(--input-padding-x); line-height: 1.4; }
.rule {
margin: 16rpx 0;
font-size: 22rpx; color: #7a5a1e;
background: #FFF7E8; border: 1rpx dashed var(--accent);
border-radius: 12rpx; padding: 16rpx 18rpx; line-height: 1.5;
}
.btns { flex-shrink: 0; display: flex; gap: 20rpx; padding-top: 16rpx; }
/* 组件样式隔离app.wxss 的 .btn 不会透传进来,按本组件已有做法在此本地补全 */
.btn {
height: var(--btn-height);
border-radius: var(--radius-pill);
border: 1rpx solid var(--line-2);
background: var(--surface);
color: var(--ink);
font-size: 28rpx; font-weight: 600;
display: flex; align-items: center; justify-content: center; gap: 12rpx;
width: 100%; line-height: 1;
}
.btn::after { border: none; }
.btn.primary { background: var(--accent); color: #fff; border-color: transparent; }
.btn.ghost { background: transparent; }
.btn:active { opacity: 0.96; }