Feat/dev #5
+28
-54
@@ -1,5 +1,6 @@
|
|||||||
const { secure, toast } = require("../../utils/api");
|
const { secure, toast } = require("../../utils/api");
|
||||||
const { localDate } = require("../../utils/date");
|
const { localDate } = require("../../utils/date");
|
||||||
|
const environment = require("../../utils/environment");
|
||||||
function dateLabel() {
|
function dateLabel() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return `${now.getMonth() + 1}月${now.getDate()}日 周${"日一二三四五六"[now.getDay()]}`;
|
return `${now.getMonth() + 1}月${now.getDate()}日 周${"日一二三四五六"[now.getDay()]}`;
|
||||||
@@ -19,7 +20,7 @@ Page({
|
|||||||
error: "",
|
error: "",
|
||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
this.updateEnvironmentLabel();
|
this.applyEnvironment(environment.cached());
|
||||||
this.load();
|
this.load();
|
||||||
},
|
},
|
||||||
onUnload() {
|
onUnload() {
|
||||||
@@ -31,62 +32,40 @@ Page({
|
|||||||
dateLabel() + " · " + (this.weatherLabel || "轻触获取天气"),
|
dateLabel() + " · " + (this.weatherLabel || "轻触获取天气"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
applyEnvironment(value) {
|
||||||
|
if (this.disposed) return;
|
||||||
|
if (value) {
|
||||||
|
this.weatherLabel = value.weatherLabel;
|
||||||
|
this.setData({ locationName: value.locationName });
|
||||||
|
}
|
||||||
|
this.updateEnvironmentLabel();
|
||||||
|
},
|
||||||
async refreshEnvironment(event) {
|
async refreshEnvironment(event) {
|
||||||
if (this.environmentBusy) return;
|
if (this.environmentBusy) return;
|
||||||
this.environmentBusy = true;
|
this.environmentBusy = true;
|
||||||
const tapped = !!event;
|
const tapped = !!event;
|
||||||
let positioned = false;
|
const cached = environment.cached();
|
||||||
try {
|
if (!cached) {
|
||||||
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");
|
|
||||||
}
|
|
||||||
this.weatherLabel = "正在获取天气";
|
this.weatherLabel = "正在获取天气";
|
||||||
this.updateEnvironmentLabel();
|
this.updateEnvironmentLabel();
|
||||||
const point = await new Promise((resolve, reject) =>
|
}
|
||||||
wx.getLocation({
|
try {
|
||||||
type: "gcj02",
|
this.applyEnvironment(await environment.refresh(tapped));
|
||||||
success: resolve,
|
|
||||||
fail: reject,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
positioned = true;
|
|
||||||
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) {
|
} catch (error) {
|
||||||
const denied = /denied|deny|auth/.test(
|
if (this.disposed) return;
|
||||||
error.errMsg || error.message || "",
|
if (cached) {
|
||||||
);
|
this.applyEnvironment(cached);
|
||||||
this.setData({
|
} else {
|
||||||
locationName: positioned
|
const denied = /denied|deny|auth/.test(
|
||||||
? "位置暂不可用"
|
error.errMsg || error.message || "",
|
||||||
: denied
|
);
|
||||||
? "开启定位"
|
this.setData({ locationName: denied ? "开启定位" : "定位暂不可用" });
|
||||||
: "定位暂不可用",
|
this.weatherLabel = denied ? "轻触开启定位" : "轻触重试";
|
||||||
});
|
this.updateEnvironmentLabel();
|
||||||
this.weatherLabel = denied ? "轻触开启定位" : "轻触重试";
|
}
|
||||||
if (tapped && !denied)
|
if (tapped) toast(error);
|
||||||
wx.showToast({
|
|
||||||
title: positioned ? "位置和天气获取失败,请重试" : "定位失败,请重试",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
this.environmentBusy = false;
|
this.environmentBusy = false;
|
||||||
this.updateEnvironmentLabel();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async load() {
|
async load() {
|
||||||
@@ -129,12 +108,7 @@ Page({
|
|||||||
phase: "ready",
|
phase: "ready",
|
||||||
});
|
});
|
||||||
// Location permission and weather never hold up the first usable screen.
|
// Location permission and weather never hold up the first usable screen.
|
||||||
if (
|
this.refreshEnvironment();
|
||||||
!this.environmentAt ||
|
|
||||||
Date.now() - this.environmentAt > 15 * 60 * 1000
|
|
||||||
) {
|
|
||||||
this.refreshEnvironment();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.disposed) return;
|
if (this.disposed) return;
|
||||||
this.setData({
|
this.setData({
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
const { secure } = require("./api");
|
||||||
|
|
||||||
|
const STORAGE_KEY = "pet_environment_v1";
|
||||||
|
let value;
|
||||||
|
let attempted = false;
|
||||||
|
let pending;
|
||||||
|
let failure;
|
||||||
|
|
||||||
|
function cached() {
|
||||||
|
if (value === undefined) {
|
||||||
|
const saved = wx.getStorageSync(STORAGE_KEY);
|
||||||
|
value =
|
||||||
|
saved &&
|
||||||
|
typeof saved.locationName === "string" &&
|
||||||
|
typeof saved.weatherLabel === "string" &&
|
||||||
|
Number.isFinite(saved.updatedAt)
|
||||||
|
? saved
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchEnvironment(tapped) {
|
||||||
|
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 point = await new Promise((resolve, reject) =>
|
||||||
|
wx.getLocation({ type: "gcj02", success: resolve, fail: reject }),
|
||||||
|
);
|
||||||
|
const result = await secure("/environment", "GET", {
|
||||||
|
latitude: point.latitude,
|
||||||
|
longitude: point.longitude,
|
||||||
|
});
|
||||||
|
if (!result || !result.location_name)
|
||||||
|
throw new Error("位置和天气获取失败,请重试");
|
||||||
|
const previous = cached();
|
||||||
|
value = {
|
||||||
|
locationName: result.location_name,
|
||||||
|
weatherLabel: result.weather
|
||||||
|
? `${result.weather.description} ${result.weather.temperature}°C`
|
||||||
|
: previous && previous.locationName === result.location_name
|
||||||
|
? previous.weatherLabel
|
||||||
|
: "天气暂不可用",
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
// Store display data only; precise coordinates are not persisted here.
|
||||||
|
try {
|
||||||
|
wx.setStorageSync(STORAGE_KEY, value);
|
||||||
|
} catch (_) {}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh(tapped = false) {
|
||||||
|
if (pending) return pending;
|
||||||
|
// Module lifetime follows the app, including recreated pages and warm resumes.
|
||||||
|
// Failed automatic attempts also wait for a tap or the next cold start.
|
||||||
|
if (attempted && !tapped)
|
||||||
|
return failure ? Promise.reject(failure) : Promise.resolve(cached());
|
||||||
|
attempted = true;
|
||||||
|
pending = fetchEnvironment(tapped)
|
||||||
|
.then(
|
||||||
|
(result) => {
|
||||||
|
failure = null;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
failure = error;
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.finally(() => {
|
||||||
|
pending = null;
|
||||||
|
});
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { cached, refresh };
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const vm = require("node:vm");
|
||||||
|
|
||||||
|
function runtime(storage = new Map()) {
|
||||||
|
const counts = { location: 0, request: 0 };
|
||||||
|
let fail = false;
|
||||||
|
let release;
|
||||||
|
const module = { exports: {} };
|
||||||
|
vm.runInNewContext(fs.readFileSync("pets-fe/utils/environment.js", "utf8"), {
|
||||||
|
module,
|
||||||
|
wx: {
|
||||||
|
getStorageSync: (key) => storage.get(key),
|
||||||
|
setStorageSync: (key, value) => storage.set(key, value),
|
||||||
|
getSetting: (opts) => opts.success({ authSetting: {} }),
|
||||||
|
getLocation: (opts) => {
|
||||||
|
counts.location++;
|
||||||
|
opts.success({ latitude: 25, longitude: 102 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
require: () => ({
|
||||||
|
secure: async () => {
|
||||||
|
counts.request++;
|
||||||
|
if (fail) throw new Error("offline");
|
||||||
|
if (release) await release;
|
||||||
|
return {
|
||||||
|
location_name: "昆明市 · 五华区",
|
||||||
|
weather: { description: "晴", temperature: "22" },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
api: module.exports,
|
||||||
|
counts,
|
||||||
|
storage,
|
||||||
|
fail: () => {
|
||||||
|
fail = true;
|
||||||
|
},
|
||||||
|
hold: (promise) => {
|
||||||
|
release = promise;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("页面重建与返回复用同一轮天气请求,点击可以刷新", async () => {
|
||||||
|
const r = runtime();
|
||||||
|
let finish;
|
||||||
|
r.hold(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
finish = resolve;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const first = r.api.refresh();
|
||||||
|
assert.equal(r.api.refresh(), first);
|
||||||
|
finish();
|
||||||
|
await first;
|
||||||
|
await r.api.refresh();
|
||||||
|
await r.api.refresh();
|
||||||
|
assert.equal(r.counts.location, 1);
|
||||||
|
assert.equal(r.counts.request, 1);
|
||||||
|
await r.api.refresh(true);
|
||||||
|
assert.equal(r.counts.request, 2);
|
||||||
|
assert.equal(r.api.cached().weatherLabel, "晴 22°C");
|
||||||
|
const saved = r.storage.get("pet_environment_v1");
|
||||||
|
assert.equal("latitude" in saved, false);
|
||||||
|
assert.equal("longitude" in saved, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("冷启动先读取上次缓存,再请求一次;失败保留缓存且不会返回就重试", async () => {
|
||||||
|
const previous = runtime();
|
||||||
|
await previous.api.refresh();
|
||||||
|
const next = runtime(previous.storage);
|
||||||
|
assert.equal(next.api.cached().locationName, "昆明市 · 五华区");
|
||||||
|
assert.equal(next.counts.request, 0);
|
||||||
|
next.fail();
|
||||||
|
await assert.rejects(next.api.refresh(), /offline/);
|
||||||
|
await assert.rejects(next.api.refresh(), /offline/);
|
||||||
|
assert.equal(next.counts.request, 1);
|
||||||
|
assert.equal(next.api.cached().weatherLabel, "晴 22°C");
|
||||||
|
await assert.rejects(next.api.refresh(true), /offline/);
|
||||||
|
assert.equal(next.counts.request, 2);
|
||||||
|
});
|
||||||
@@ -5,6 +5,9 @@ const path = require("node:path");
|
|||||||
const apiPath = require.resolve("../pets-fe/utils/api");
|
const apiPath = require.resolve("../pets-fe/utils/api");
|
||||||
|
|
||||||
function page(name, secure, wx = {}) {
|
function page(name, secure, wx = {}) {
|
||||||
|
// Each test represents a fresh app session; page navigation within a test
|
||||||
|
// still shares the environment module.
|
||||||
|
delete require.cache[require.resolve("../pets-fe/utils/environment")];
|
||||||
global.wx = {
|
global.wx = {
|
||||||
getStorageSync: () => [],
|
getStorageSync: () => [],
|
||||||
setStorageSync: () => {},
|
setStorageSync: () => {},
|
||||||
|
|||||||
@@ -99,9 +99,11 @@ function home(login, secure) {
|
|||||||
definition = value;
|
definition = value;
|
||||||
},
|
},
|
||||||
require: (name) =>
|
require: (name) =>
|
||||||
name.includes("date")
|
name.includes("environment")
|
||||||
? { localDate: () => "2026-10-05" }
|
? { cached: () => null }
|
||||||
: { secure, toast: () => {} },
|
: name.includes("date")
|
||||||
|
? { localDate: () => "2026-10-05" }
|
||||||
|
: { secure, toast: () => {} },
|
||||||
});
|
});
|
||||||
const page = {
|
const page = {
|
||||||
...definition,
|
...definition,
|
||||||
|
|||||||
Reference in New Issue
Block a user