每日运势小程序

This commit is contained in:
cjd
2025-11-09 18:41:07 +08:00
parent 1cc0241cda
commit abd82782af
34 changed files with 2204 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
const config = require("../config/index");
function showRewardedVideoAd() {
return new Promise((resolve, reject) => {
if (!config.adUnitId) {
console.warn("adUnitId 未配置,跳过广告流程(仅开发阶段)");
resolve("ad_skipped");
return;
}
if (typeof tt === "undefined" || !tt.createRewardedVideoAd) {
reject(new Error("rewarded_ad_unavailable"));
return;
}
const ad = tt.createRewardedVideoAd({
adUnitId: config.adUnitId
});
const cleanup = () => {
if (!ad) {
return;
}
if (ad.offLoad) {
ad.offLoad();
}
if (ad.offError) {
ad.offError();
}
if (ad.offClose) {
ad.offClose();
}
};
ad.onError((error) => {
cleanup();
reject(error);
});
ad.onClose((result = {}) => {
cleanup();
if (result.isEnded) {
resolve();
} else {
const err = new Error("ad_not_completed");
err.code = "ad_not_completed";
reject(err);
}
});
ad
.load()
.then(() => ad.show())
.catch((error) => {
cleanup();
reject(error);
});
});
}
module.exports = {
showRewardedVideoAd
};

View File

@@ -0,0 +1,27 @@
function pad(num) {
return `${num}`.padStart(2, "0");
}
function formatDate(date) {
const target = date instanceof Date ? date : new Date(date);
return `${target.getFullYear()}-${pad(target.getMonth() + 1)}-${pad(target.getDate())}`;
}
function formatDisplayDateTime(value) {
if (!value) {
return "";
}
try {
const target = new Date(value);
return `${target.getFullYear()}-${pad(target.getMonth() + 1)}-${pad(target.getDate())} ${pad(target.getHours())}:${pad(
target.getMinutes()
)}`;
} catch (error) {
return value;
}
}
module.exports = {
formatDate,
formatDisplayDateTime
};

View File

@@ -0,0 +1,150 @@
const dimensionMeta = require("../constants/dimensions");
const { formatDisplayDateTime } = require("./date");
const dimensionMap = dimensionMeta.reduce((acc, item) => {
acc[item.key] = item;
return acc;
}, {});
function pickValue(source, keys, fallback) {
if (!source) {
return fallback;
}
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
const value = source[key];
if (value !== undefined && value !== null) {
return value;
}
}
}
return fallback;
}
function mapTrendText(trend) {
if (trend === "up") {
return "回升";
}
if (trend === "down") {
return "波动";
}
return "平稳";
}
function normalizePillars(pillars) {
if (!Array.isArray(pillars)) {
return [];
}
return pillars
.map((item) => ({
label: pickValue(item, ["label", "Label"], ""),
value: pickValue(item, ["value", "Value"], "")
}))
.filter((item) => item.label || item.value);
}
function formatListText(list, separator = " · ") {
if (!Array.isArray(list) || !list.length) {
return "";
}
return list.join(separator);
}
function normalizeFortunePayload(fortune) {
if (!fortune) {
return null;
}
const dimensionListRaw = pickValue(fortune, ["dimensions", "Dimensions"], []);
const dimensionList = Array.isArray(dimensionListRaw) ? dimensionListRaw : [];
const dimensions = dimensionList.map((item) => {
const key = pickValue(item, ["key", "Key"], "");
const meta = dimensionMap[key] || {};
const trend = pickValue(item, ["trend", "Trend"], "steady");
return {
key,
title: pickValue(item, ["title", "Title"], meta.title || key),
score: pickValue(item, ["score", "Score"], 0),
trend,
insight: pickValue(item, ["insight", "Insight"], ""),
suggestion: pickValue(item, ["suggestion", "Suggestion"], ""),
icon: meta.icon || "⭐",
accent: meta.accent || "#ffb85c",
trendText: mapTrendText(trend)
};
});
const total = dimensions.reduce((acc, current) => acc + (current.score || 0), 0);
const overallScore = dimensions.length ? Math.round(total / dimensions.length) : 0;
return {
fortuneDate: pickValue(fortune, ["fortuneDate", "FortuneDate"], ""),
summary: pickValue(fortune, ["summary", "Summary"], ""),
narrative: pickValue(fortune, ["narrative", "Narrative"], ""),
dimensions,
overallScore,
luckyGuide: normalizeGuide(pickValue(fortune, ["luckyGuide", "LuckyGuide"], null)),
profile: normalizeProfile(pickValue(fortune, ["profile", "Profile"], null))
};
}
function normalizeProfile(profile) {
if (!profile) {
return null;
}
const fiveElements = pickValue(profile, ["fiveElementDistribution", "FiveElementDistribution"], []);
const total = fiveElements.reduce((acc, item) => acc + (pickValue(item, ["count", "Count"], 0) || 0), 0) || 1;
const distribution = fiveElements.map((item) => {
const count = pickValue(item, ["count", "Count"], 0);
const percent = Math.round(((count || 0) / total) * 100);
return {
element: pickValue(item, ["element", "Element"], "未知"),
count,
percent,
width: `${Math.max(percent, 6)}%`
};
});
const birthDateTimeRaw = pickValue(profile, ["birthDateTime", "BirthDateTime"], "");
return {
birthCity: pickValue(profile, ["birthCity", "BirthCity"], ""),
birthProvince: pickValue(profile, ["birthProvince", "BirthProvince"], ""),
birthDateTime: birthDateTimeRaw ? formatDisplayDateTime(birthDateTimeRaw) : "",
birthPillars: normalizePillars(pickValue(profile, ["birthPillars", "BirthPillars"], [])),
todayPillars: normalizePillars(pickValue(profile, ["todayPillars", "TodayPillars"], [])),
distribution,
weakElements: pickValue(profile, ["weakElements", "WeakElements"], []),
strongElements: pickValue(profile, ["strongElements", "StrongElements"], [])
};
}
function normalizeGuide(guide) {
if (!guide) {
return null;
}
const slots = pickValue(guide, ["bestTimeSlots", "BestTimeSlots"], []);
const colors = pickValue(guide, ["colors", "Colors"], []);
const directions = pickValue(guide, ["directions", "Directions"], []);
const props = pickValue(guide, ["props", "Props"], []);
const activities = pickValue(guide, ["activities", "Activities"], []);
return {
element: pickValue(guide, ["element", "Element"], "木"),
colors,
colorText: formatListText(colors),
directions,
directionText: formatListText(directions),
props,
propsText: formatListText(props),
activities,
activitiesText: formatListText(activities, " / "),
bestTimeSlots: Array.isArray(slots)
? slots.map((slot) => ({
label: pickValue(slot, ["label", "Label"], "时段"),
period: pickValue(slot, ["period", "Period"], ""),
reason: pickValue(slot, ["reason", "Reason"], "")
}))
: []
};
}
module.exports = {
normalizeFortunePayload
};

View File

@@ -0,0 +1,81 @@
const MAX_REGION_LENGTH = 3;
function sanitizeSegment(value) {
if (typeof value !== "string") {
return "";
}
const trimmed = value.trim();
if (!trimmed || trimmed === "全部") {
return "";
}
return trimmed;
}
function normalizeRegionArray(region) {
if (!Array.isArray(region)) {
return [];
}
const sanitized = region.map(sanitizeSegment);
const hasValue = sanitized.some(Boolean);
if (!hasValue) {
return [];
}
const normalized = sanitized.slice(0, MAX_REGION_LENGTH);
while (normalized.length < MAX_REGION_LENGTH) {
normalized.push("");
}
return normalized;
}
function deriveRegionFromParts(parts = {}) {
const segments = [];
if (parts.province) {
segments.push(parts.province);
}
if (parts.city) {
segments.push(parts.city);
}
if (parts.district && parts.district !== parts.city) {
segments.push(parts.district);
}
if (!segments.length && parts.city) {
segments.push(parts.city);
}
return normalizeRegionArray(segments);
}
function getRegionParts(region, fallback = {}) {
const normalized = normalizeRegionArray(region);
if (!normalized.length) {
return {
province: sanitizeSegment(fallback.province),
city: sanitizeSegment(fallback.city),
district: sanitizeSegment(fallback.district)
};
}
const [province = "", city = "", district = ""] = normalized;
return {
province: province || sanitizeSegment(fallback.province),
city: city || province || sanitizeSegment(fallback.city),
district: district || sanitizeSegment(fallback.district)
};
}
function formatRegionDisplay(region, fallbackList = []) {
const normalized = normalizeRegionArray(region);
const source = normalized.length ? normalized : fallbackList;
if (!source || !source.length) {
return "";
}
return source
.filter(sanitizeSegment)
.filter((item, index, arr) => arr.indexOf(item) === index)
.join(" · ");
}
module.exports = {
normalizeRegionArray,
deriveRegionFromParts,
getRegionParts,
formatRegionDisplay
};

View File

@@ -0,0 +1,52 @@
const DEFAULT_PADDING = 64;
const EXTRA_SPACING = 12;
function getMenuPadding(extraSpacing) {
if (typeof tt === "undefined" || typeof tt.getMenuButtonBoundingClientRect !== "function") {
return 0;
}
try {
const rect = tt.getMenuButtonBoundingClientRect();
if (!rect) {
return 0;
}
if (typeof rect.bottom === "number") {
return rect.bottom + extraSpacing;
}
if (typeof rect.top === "number" && typeof rect.height === "number") {
return rect.top + rect.height + extraSpacing;
}
} catch (error) {
console.warn("safe-area: menu rect unavailable", error);
}
return 0;
}
function getStatusBarPadding(extraSpacing) {
if (typeof tt === "undefined" || typeof tt.getSystemInfoSync !== "function") {
return 0;
}
try {
const info = tt.getSystemInfoSync();
return (info && info.statusBarHeight ? info.statusBarHeight : 0) + extraSpacing;
} catch (error) {
console.warn("safe-area: system info unavailable", error);
}
return 0;
}
function getPageSafeTop(extraSpacing = EXTRA_SPACING, fallback = DEFAULT_PADDING) {
const menuPadding = getMenuPadding(extraSpacing);
if (menuPadding) {
return Math.round(menuPadding);
}
const statusPadding = getStatusBarPadding(extraSpacing);
if (statusPadding) {
return Math.round(statusPadding);
}
return fallback;
}
module.exports = {
getPageSafeTop
};

View File

@@ -0,0 +1,31 @@
function safeGetStorage(key, fallback) {
if (typeof tt === "undefined" || !tt.getStorageSync) {
return fallback;
}
try {
const value = tt.getStorageSync(key);
if (value === undefined || value === null) {
return fallback;
}
return value;
} catch (error) {
console.warn(`getStorageSync failed: ${key}`, error);
return fallback;
}
}
function safeSetStorage(key, value) {
if (typeof tt === "undefined" || !tt.setStorageSync) {
return;
}
try {
tt.setStorageSync(key, value);
} catch (error) {
console.warn(`setStorageSync failed: ${key}`, error);
}
}
module.exports = {
safeGetStorage,
safeSetStorage
};