feat: add multi-source breed encyclopedia and two-hour refresh sessions
ci-cd / verify (push) Successful in 1m59s
ci-cd / deploy (push) Skipped

This commit is contained in:
2026-10-05 13:38:24 +08:00
parent 607d709100
commit 9d7cf720a5
73 changed files with 25771 additions and 264 deletions
+143
View File
@@ -0,0 +1,143 @@
const { secure, toast } = require("../../utils/api");
Component({
options: { styleIsolation: "apply-shared" },
lifetimes: {
attached() {
this.load();
},
},
pageLifetimes: {
show() {
this.load();
},
},
data: {
meetups: [],
pets: [],
selected: [],
active: null,
userId: "",
joined: false,
busy: false,
},
methods: {
async load() {
if (this.loading) return;
this.loading = true;
try {
const [meetups, pets, me] = await Promise.all([
secure("/meetups"),
secure("/pets"),
secure("/me"),
]);
this.setData({
meetups: meetups.map((m) => ({
...m,
date: new Date(m.starts_at).toLocaleString("zh-CN"),
})),
pets,
userId: me.id,
});
} catch (e) {
toast(e);
} finally {
this.loading = false;
}
},
create() {
wx.navigateTo({ url: "/pages/meetup-edit/meetup-edit" });
},
async open(e) {
const summary = this.data.meetups[e.currentTarget.dataset.index];
try {
const active = await secure("/meetups/" + summary.id);
const participant = (active.people || []).find(
(person) => person.user_id === this.data.userId,
);
this.setData({
active: {
...active,
date: new Date(active.starts_at).toLocaleString("zh-CN"),
},
joined: !!participant,
selected: participant ? participant.pet_ids : [],
});
} catch (error) {
toast(error);
}
},
close() {
this.setData({ active: null, joined: false, selected: [] });
},
pick(e) {
this.setData({ selected: e.detail.selected });
},
async join() {
if (this.data.busy) return;
if (!this.data.selected.length) {
wx.showToast({ title: "请选择同行的狗狗", icon: "none" });
return;
}
this.setData({ busy: true });
try {
await secure("/meetups/" + this.data.active.id + "/join", "POST", {
pet_ids: this.data.selected,
});
wx.showToast({ title: "报名成功", icon: "success" });
this.close();
await this.load();
} catch (e) {
toast(e);
} finally {
this.setData({ busy: false });
}
},
async leave() {
if (this.data.busy) return;
const result = await wx.showModal({
title: "退出约遛",
content: "确定不参加这场约遛了吗?",
});
if (!result.confirm) return;
this.setData({ busy: true });
try {
await secure("/meetups/" + this.data.active.id + "/join", "DELETE");
this.close();
await this.load();
} catch (error) {
toast(error);
} finally {
this.setData({ busy: false });
}
},
location() {
const active = this.data.active;
if (active && active.latitude && active.longitude) {
wx.openLocation({
latitude: active.latitude,
longitude: active.longitude,
name: active.place_name,
scale: 16,
});
}
},
async cancel() {
if (this.data.busy) return;
const r = await wx.showModal({
title: "取消约遛",
content: "确定取消这场约遛吗?",
});
if (!r.confirm) return;
this.setData({ busy: true });
try {
await secure("/meetups/" + this.data.active.id + "/cancel", "POST");
this.close();
await this.load();
} catch (e) {
toast(e);
} finally {
this.setData({ busy: false });
}
},
},
});
@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"pet-picker": "/components/pet-picker/index"
}
}
@@ -0,0 +1,60 @@
<view class="screen">
<view class="banner">
<view class="banner-copy">
<text>好天气,和新朋友一起走走</text>
<view class="banner-title first">下一次快乐</view>
<view class="banner-title">从这里出发</view>
</view>
<image src="/assets/corgi-family-five.png" mode="aspectFit" />
</view>
<view class="row">
<text class="section-title">即将开始的约遛</text>
<text class="add" bindtap="create">+ 发起约遛</text>
</view>
<view
wx:for="{{meetups}}"
wx:key="id"
class="card meetup"
data-index="{{index}}"
bindtap="open"
>
<view class="row">
<text class="badge">
{{item.participants}} / {{item.max_people}} 人
</text>
<text class="arrow">›</text>
</view>
<strong>{{item.title}}</strong>
<view class="meta">地点 · {{item.place_name}}</view>
<view class="meta">时间 · {{item.date}}</view>
<view class="desc">{{item.description}}</view>
</view>
<view wx:if="{{!meetups.length}}" class="empty">
还没有约遛,发起第一场吧
</view>
<button class="primary" bindtap="create">+ 发起一场约遛</button>
</view>
<root-portal wx:if="{{active}}"><view class="overlay">
<view class="sheet">
<view class="row">
<strong>{{active.title}}</strong>
<text bindtap="close">×</text>
</view>
<view class="detail">
<view bindtap="location">地点 · {{active.place_name}} {{active.latitude?'· 点开地图':''}}</view>
<view>时间 · {{active.date}}</view>
<view> {{active.participants}} / {{active.max_people}} 人已报名</view>
</view>
<view class="desc">{{active.description}}</view>
<view wx:if="{{active.host_user_id!==userId&&!joined}}">
<view class="select-title">带哪几只?可以多选</view>
<pet-picker pets="{{pets}}" selected="{{selected}}" bind:change="pick" />
<button class="primary" bindtap="join" disabled="{{busy||active.participants>=active.max_people}}">{{active.participants>=active.max_people?'人数已满':'带狗狗报名'}}</button>
</view>
<button wx:elif="{{joined&&active.host_user_id!==userId}}" class="secondary" bindtap="leave" disabled="{{busy}}">已报名 · 退出约遛</button>
<button wx:else class="secondary" bindtap="cancel" disabled="{{busy}}">取消这场约遛</button>
</view>
</view>
</root-portal>
+101
View File
@@ -0,0 +1,101 @@
.banner {
background: #edf2fb;
border-radius: 34rpx;
min-height: 245rpx;
padding: 30rpx;
display: flex;
align-items: center;
gap: 12rpx;
}
.banner-copy {
flex: 1;
min-width: 0;
}
.banner-copy text,
.banner-title {
display: block;
}
.banner-copy text {
color: #576c88;
font-size: 22rpx;
}
.banner-title {
font-size: 35rpx;
line-height: 1.3;
font-weight: 600;
white-space: nowrap;
}
.banner-title.first {
margin-top: 16rpx;
}
.banner image {
width: 52%;
height: 190rpx;
flex-shrink: 0;
}
.add {
font-size: 24rpx;
color: #b95738;
font-weight: 600;
}
.meetup strong {
display: block;
font-size: 32rpx;
margin: 15rpx 0;
}
.badge {
background: #edf2fb;
color: #b95738;
border-radius: 99rpx;
padding: 8rpx 15rpx;
font-size: 22rpx;
}
.arrow {
font-size: 39rpx;
color: #9d8779;
}
.meta {
font-size: 24rpx;
color: #886c5f;
margin-top: 9rpx;
}
.desc {
font-size: 24rpx;
color: #886c5f;
margin-top: 15rpx;
}
.overlay {
position: fixed;
inset: 0;
background: #604a40a0;
z-index: 30;
display: flex;
align-items: flex-end;
}
.sheet {
width: 100%;
background: #fff;
border-radius: 36rpx 36rpx 0 0;
padding: 35rpx 32rpx calc(env(safe-area-inset-bottom) + 35rpx);
}
.sheet > .row strong {
font-size: 34rpx;
}
.sheet > .row text {
font-size: 45rpx;
}
.detail {
font-size: 25rpx;
color: #886c5f;
line-height: 2;
margin: 25rpx 0;
}
.select-title {
font-size: 25rpx;
font-weight: 600;
margin: 30rpx 0 15rpx;
}
.sheet button {
margin-top: 26rpx;
width: 100%;
}