Add immediate startup login and loading states for mini program home
ci-cd / verify (push) Successful in 5s
ci-cd / deploy (push) Skipped

This commit is contained in:
2026-10-05 11:03:33 +08:00
parent 4916c31edd
commit 42aea10101
15 changed files with 623 additions and 44 deletions
+83 -21
View File
@@ -14,16 +14,22 @@ Page({
walkCount: 0,
name: "铲屎官",
loading: true,
ready: false,
phase: "auth",
error: "",
},
onShow() {
this.load();
this.updateEnvironmentLabel();
if (!this.environmentAt || Date.now() - this.environmentAt > 15 * 60 * 1000) {
this.refreshEnvironment();
}
this.load();
},
onUnload() {
this.disposed = true;
},
updateEnvironmentLabel() {
this.setData({ headerSubtitle: dateLabel() + " · " + (this.weatherLabel || "轻触获取天气") });
this.setData({
headerSubtitle:
dateLabel() + " · " + (this.weatherLabel || "轻触获取天气"),
});
},
async refreshEnvironment(event) {
if (this.environmentBusy) return;
@@ -31,44 +37,84 @@ Page({
const tapped = !!event;
let positioned = false;
try {
const settings = await new Promise((resolve, reject) => wx.getSetting({ success: resolve, fail: reject }));
const settings = await new Promise((resolve, reject) =>
wx.getSetting({ success: resolve, fail: reject }),
);
if (settings.authSetting["scope.userLocation"] === false) {
if (!tapped) throw new Error("location-denied");
const enabled = await new Promise((resolve, reject) => wx.openSetting({ success: resolve, fail: reject }));
if (!enabled.authSetting["scope.userLocation"]) throw new Error("location-denied");
const enabled = await new Promise((resolve, reject) =>
wx.openSetting({ success: resolve, fail: reject }),
);
if (!enabled.authSetting["scope.userLocation"])
throw new Error("location-denied");
}
this.weatherLabel = "正在获取天气";
this.updateEnvironmentLabel();
const point = await new Promise((resolve, reject) => wx.getLocation({
type: "gcj02",
success: resolve,
fail: reject,
}));
const point = await new Promise((resolve, reject) =>
wx.getLocation({
type: "gcj02",
success: resolve,
fail: reject,
}),
);
positioned = true;
const result = await secure("/environment", "GET", { latitude: point.latitude, longitude: point.longitude });
const result = await secure("/environment", "GET", {
latitude: point.latitude,
longitude: point.longitude,
});
this.setData({ locationName: result.location_name });
this.weatherLabel = result.weather
? `${result.weather.description} ${result.weather.temperature}°C`
: "天气暂不可用";
this.environmentAt = result.weather ? Date.now() : 0;
} catch (error) {
const denied = /denied|deny|auth/.test(error.errMsg || error.message || "");
this.setData({ locationName: positioned ? "位置暂不可用" : denied ? "开启定位" : "定位暂不可用" });
const denied = /denied|deny|auth/.test(
error.errMsg || error.message || "",
);
this.setData({
locationName: positioned
? "位置暂不可用"
: denied
? "开启定位"
: "定位暂不可用",
});
this.weatherLabel = denied ? "轻触开启定位" : "轻触重试";
if (tapped && !denied) wx.showToast({ title: positioned ? "位置和天气获取失败,请重试" : "定位失败,请重试", icon: "none" });
if (tapped && !denied)
wx.showToast({
title: positioned ? "位置和天气获取失败,请重试" : "定位失败,请重试",
icon: "none",
});
} finally {
this.environmentBusy = false;
this.updateEnvironmentLabel();
}
},
async load() {
if (this.loadPromise) return this.loadPromise;
this.loadPromise = this.loadHome().finally(() => {
this.loadPromise = null;
});
return this.loadPromise;
},
async loadHome() {
this.setData({
loading: true,
error: "",
phase: this.data.ready ? "data" : "auth",
});
let authenticated = false;
try {
await getApp().login();
authenticated = true;
if (this.disposed) return;
this.setData({ phase: "data" });
const [pets, tasks, walks, me] = await Promise.all([
secure("/pets"),
secure("/tasks"),
secure("/walks"),
secure("/me"),
]);
if (this.disposed) return;
const chosen = wx.getStorageSync("walk_pet_ids") || [],
month = localDate().slice(0, 7);
this.setData({
@@ -77,14 +123,30 @@ Page({
tasks: tasks.items,
walkCount: walks.filter((w) => w.started_at.slice(0, 7) === month)
.length,
name: me.nickname || "铲屎官",
name: me.nickname || "毛孩家长",
loading: false,
ready: true,
phase: "ready",
});
// Location permission and weather never hold up the first usable screen.
if (
!this.environmentAt ||
Date.now() - this.environmentAt > 15 * 60 * 1000
) {
this.refreshEnvironment();
}
} catch (error) {
if (this.disposed) return;
this.setData({
loading: false,
phase: !authenticated && !this.data.ready ? "auth-error" : "data-error",
error: error.message || "暂时没能连接,请稍后重试",
});
} catch (e) {
this.setData({ loading: false });
toast(e);
}
},
addPet() {
wx.navigateTo({ url: "/pages/pet-edit/pet-edit" });
},
pick(e) {
this.setData({ selected: e.detail.selected });
wx.setStorageSync("walk_pet_ids", e.detail.selected);
+3 -1
View File
@@ -2,6 +2,8 @@
"disableScroll": true,
"usingComponents": {
"tab-bar": "/components/tab-bar/index",
"pet-picker": "/components/pet-picker/index"
"pet-picker": "/components/pet-picker/index",
"startup-state": "/components/startup-state/index",
"home-skeleton": "/components/home-skeleton/index"
}
}
+21 -2
View File
@@ -1,5 +1,16 @@
<page-layout id="page-layout" title="{{locationName}}" subtitle="{{headerSubtitle}}" location="{{true}}" bind:locationtap="refreshEnvironment">
<startup-state wx:if="{{phase==='auth' || phase==='auth-error'}}" error="{{error}}" bind:retry="load" />
<page-layout wx:else id="page-layout" title="{{locationName}}" subtitle="{{headerSubtitle}}" location="{{true}}" bind:locationtap="refreshEnvironment">
<view class="screen">
<home-skeleton wx:if="{{loading && !ready}}" />
<view wx:elif="{{!ready && error}}" class="card home-error">
<app-icon kind="paw" size="54" />
<view class="error-title">暂时没能准备好首页</view>
<view class="muted">{{error}}</view>
<button class="primary" bindtap="load">重新加载</button>
</view>
<block wx:else>
<view wx:if="{{error}}" class="refresh-error"><text>更新失败,保留上次的日常记录</text><text class="link" bindtap="load">重试</text></view>
<view wx:elif="{{loading}}" class="refresh-hint">正在更新今天的小日常…</view>
<view class="hero">
<view class="hero-copy">
<text class="hero-small">今日的主角</text>
@@ -21,9 +32,16 @@
<view class="muted">选择同行的狗狗,一起出门转转。</view>
</view>
</view>
<view wx:if="{{!pets.length}}" class="first-pet">
<view class="first-pet-title">认识你的第一位毛孩子</view>
<view class="muted">先建立档案,再一起出门收集小快乐。</view>
<button class="primary start" bindtap="addPet">添加毛孩子</button>
</view>
<block wx:else>
<view class="picker-label">这次带谁?可以多选</view>
<pet-picker pets="{{pets}}" selected="{{selected}}" bind:change="pick" />
<button class="primary start" bindtap="start">开始遛狗 →</button>
</block>
</view>
<view class="stats">
<view>
@@ -75,6 +93,7 @@
今天还没有任务,给狗狗安排一件小事吧
</view>
</view>
</block>
</view>
</page-layout>
<tab-bar current="/pages/home/home" />
<tab-bar wx:if="{{ready}}" current="/pages/home/home" />
+50
View File
@@ -157,3 +157,53 @@
.task-main .muted {
font-size: 22rpx;
}
.home-error {
text-align: center;
padding: 70rpx 35rpx;
display: flex;
align-items: center;
flex-direction: column;
gap: 26rpx;
margin-top: 40rpx;
}
.error-title {
font-size: 30rpx;
font-weight: 600;
}
.home-error .muted {
font-size: 24rpx;
line-height: 1.8;
}
.home-error button {
margin-top: 15rpx;
min-width: 300rpx;
}
.refresh-error {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 22rpx;
background: #fff0e2;
color: #886c5f;
padding: 20rpx 24rpx;
border-radius: 22rpx;
margin-bottom: 20rpx;
}
.refresh-hint {
text-align: center;
font-size: 22rpx;
color: #a48d7e;
margin-bottom: 20rpx;
}
.first-pet {
padding-top: 30rpx;
}
.first-pet-title {
font-size: 27rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.first-pet .muted {
font-size: 23rpx;
line-height: 1.8;
}