diff --git a/DailyFortuneGuide/Spec.md b/DailyFortuneGuide/Spec.md
new file mode 100644
index 0000000..0787e46
--- /dev/null
+++ b/DailyFortuneGuide/Spec.md
@@ -0,0 +1,41 @@
+# 每日运势指南产品规格(DailyFortuneGuide)
+
+## 1. 目标
+- 依据用户的出生日期、时间、城市生成基础命盘,结合当日天干地支与天象输出六维度运势、吉祥元素、最佳行动时机。
+- 指南内容在小程序内结构化展示,并保留最多 20 条本地历史记录。
+- 每日最多计算 10 次,观看激励广告后才允许触发计算。
+
+## 2. 页面与职责
+| 页面 | 路径 | 职责 |
+| --- | --- | --- |
+| 首页 | `pages/home/index` | 输入表单、校验、广告流程、触发生成、提示每日一次更准 |
+| 结果页 | `pages/result/index` | 展示六维度评分、吉祥元素、命盘摘要与 AI 解读,可跳转历史 |
+| 历史列表 | `pages/history/index` | 展示本地最多 20 条记录、支持查看详情 |
+| 历史详情 | `pages/history-detail/index` | 还原指定记录的运势内容 |
+
+## 3. 数据模型
+- `FortuneForm`: `{ birthDate, birthTime, birthCity }`
+- `FortuneResult`: 后端 `DailyFortuneResponse`
+- `HistoryItem`: `{ id, createdAt, formSnapshot, fortune }`
+- 本地存储键:
+ - `fortune:quota:YYYYMMDD`
+ - `fortune:history`
+
+## 4. 交互规则
+- 必填项缺失或城市长度 \< 2 时阻止提交。
+- 每次生成前必须成功播放激励视频广告;未完整观看给出提示。
+- 每日调用次数达到 `config.maxDailyQuota`(默认 10)后禁止继续生成。
+- 成功生成后强制写入历史,超出 20 条旧记录被淘汰。
+- 历史记录详情可脱网查看,上一次生成的结果缓存在 `store.currentFortune` 供结果页使用。
+
+## 5. 接口
+- `POST /api/daily-fortune/analyze`
+ - 请求:`{ birthDate, birthTime?, birthCity, birthProvince? }`
+ - 响应:`DailyFortuneResponse`(命盘、六维运势、吉祥元素、AI narrative)
+ - 错误码:`CONTENT_RISK`、`BIRTHDATE_INVALID`、`GENERATION_FAILED`
+
+## 6. 技术实现
+- 统一在 `store/fortuneStore.js` 管理表单、当前结果、配额和历史列表,遵循单一职责。
+- `services/fortuneService.js` 封装请求;`utils/adService.js` 复用激励广告逻辑;`utils/storage.js` 包装 `tt` 存储 API。
+- 所有提示文案集中在 `constants/messages.js`。
+- 页面样式遵循示例图的卡片化布局,移动端友好、对比度清晰。
diff --git a/DailyFortuneGuide/app.js b/DailyFortuneGuide/app.js
new file mode 100644
index 0000000..05612b1
--- /dev/null
+++ b/DailyFortuneGuide/app.js
@@ -0,0 +1,8 @@
+const { initStore } = require("./store/fortuneStore");
+
+App({
+ onLaunch() {
+ initStore();
+ },
+ globalData: {}
+});
diff --git a/DailyFortuneGuide/app.json b/DailyFortuneGuide/app.json
new file mode 100644
index 0000000..6eeaf0b
--- /dev/null
+++ b/DailyFortuneGuide/app.json
@@ -0,0 +1,15 @@
+{
+ "pages": [
+ "pages/home/index",
+ "pages/result/index",
+ "pages/history/index",
+ "pages/history-detail/index"
+ ],
+ "window": {
+ "navigationStyle": "custom",
+ "backgroundColor": "#090b1a",
+ "backgroundTextStyle": "light"
+ },
+ "style": "v2",
+ "sitemapLocation": "sitemap.json"
+}
diff --git a/DailyFortuneGuide/app.ttss b/DailyFortuneGuide/app.ttss
new file mode 100644
index 0000000..1ceb466
--- /dev/null
+++ b/DailyFortuneGuide/app.ttss
@@ -0,0 +1,29 @@
+page {
+ background: radial-gradient(120% 120% at 50% 0%, #111428 0%, #080a16 55%, #05060f 100%);
+ color: #f7f1df;
+ font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
+ min-height: 100vh;
+}
+
+text {
+ color: inherit;
+}
+
+button.primary {
+ background: linear-gradient(135deg, #ff9a62 0%, #ff6ac1 100%);
+ color: #1a0f1d;
+ border-radius: 28px;
+ font-weight: 600;
+ letter-spacing: 2px;
+}
+
+button.primary:disabled {
+ opacity: 0.6;
+}
+
+button.secondary {
+ background: rgba(255, 255, 255, 0.1);
+ color: #f7f1df;
+ border: 1px solid rgba(255, 255, 255, 0.24);
+ border-radius: 24px;
+}
diff --git a/DailyFortuneGuide/config/index.js b/DailyFortuneGuide/config/index.js
new file mode 100644
index 0000000..5c58145
--- /dev/null
+++ b/DailyFortuneGuide/config/index.js
@@ -0,0 +1,8 @@
+const config = {
+ apiBaseUrl: "http://localhost:9291",//"https://qm.shiciju.cn",
+ maxDailyQuota: 10,
+ historyLimit: 20,
+ adUnitId: ""//"69nupig8os5l4kcqpz"
+};
+
+module.exports = config;
diff --git a/DailyFortuneGuide/constants/dimensions.js b/DailyFortuneGuide/constants/dimensions.js
new file mode 100644
index 0000000..2c3e5a5
--- /dev/null
+++ b/DailyFortuneGuide/constants/dimensions.js
@@ -0,0 +1,10 @@
+module.exports = [
+ { key: "career", title: "事业运", icon: "💼", accent: "#ffb85c" },
+ { key: "wealth", title: "财务运", icon: "💰", accent: "#4fd1c5" },
+ { key: "relationship", title: "情感运", icon: "💗", accent: "#ff7eb6" },
+ { key: "health", title: "健康运", icon: "🌿", accent: "#7ddc88" },
+ { key: "social", title: "人际运", icon: "🤝", accent: "#76b7ff" },
+ { key: "inspiration", title: "灵感运", icon: "✨", accent: "#c097ff" },
+ { key: "emotion", title: "情绪状态", icon: "🧠", accent: "#fb8da0" },
+ { key: "opportunity", title: "机遇运势", icon: "🚀", accent: "#ffd166" }
+];
diff --git a/DailyFortuneGuide/constants/messages.js b/DailyFortuneGuide/constants/messages.js
new file mode 100644
index 0000000..9742dbc
--- /dev/null
+++ b/DailyFortuneGuide/constants/messages.js
@@ -0,0 +1,10 @@
+module.exports = {
+ FORM_INCOMPLETE: "请完整填写出生日期与城市",
+ CITY_INVALID: "出生城市至少需填写两个汉字",
+ WATCH_AD_TO_CONTINUE: "需完整观看广告才可计算航向",
+ QUOTA_EXCEEDED: "今日测算次数已达上限,请明日再来",
+ GENERATION_FAILED: "生成失败,请稍后重试",
+ CONTENT_RISK: "输入信息存在合规风险,请重新检查",
+ HISTORY_EMPTY: "暂无历史记录",
+ HISTORY_NOT_FOUND: "未找到对应的历史记录"
+};
diff --git a/DailyFortuneGuide/pages/history-detail/index.js b/DailyFortuneGuide/pages/history-detail/index.js
new file mode 100644
index 0000000..f9c4f29
--- /dev/null
+++ b/DailyFortuneGuide/pages/history-detail/index.js
@@ -0,0 +1,77 @@
+const fortuneStore = require("../../store/fortuneStore");
+const { normalizeFortunePayload } = require("../../utils/fortuneFormatter");
+const { formatDisplayDateTime } = require("../../utils/date");
+const messages = require("../../constants/messages");
+const { getPageSafeTop } = require("../../utils/safeArea");
+const { formatRegionDisplay, normalizeRegionArray } = require("../../utils/region");
+
+function showToast(title) {
+ if (typeof tt === "undefined" || !tt.showToast) {
+ console.warn("Toast:", title);
+ return;
+ }
+ tt.showToast({
+ title,
+ icon: "none",
+ duration: 2000
+ });
+}
+
+Page({
+ data: {
+ safeAreaTop: 64,
+ recordTime: "",
+ fortuneDate: "",
+ summary: "",
+ narrative: "",
+ dimensions: [],
+ luckyGuide: null,
+ profile: null,
+ overallScore: 0,
+ city: ""
+ },
+ onLoad(options) {
+ this.updateSafeAreaPadding();
+ fortuneStore.loadHistory();
+ const id = options && options.id ? options.id : "";
+ if (!id) {
+ showToast(messages.HISTORY_NOT_FOUND);
+ this.safeBack();
+ return;
+ }
+ const record = fortuneStore.getHistoryById(id);
+ if (!record) {
+ showToast(messages.HISTORY_NOT_FOUND);
+ this.safeBack();
+ return;
+ }
+ const normalized = normalizeFortunePayload(record.fortune);
+ if (!normalized) {
+ showToast(messages.HISTORY_NOT_FOUND);
+ this.safeBack();
+ return;
+ }
+ const snapshot = record.formSnapshot || {};
+ const regionValue = normalizeRegionArray(snapshot.birthRegion);
+ const fallbackSegments = [snapshot.birthProvince, snapshot.birthCity, snapshot.birthDistrict].filter(Boolean);
+ const cityLabel = formatRegionDisplay(regionValue, fallbackSegments);
+ this.setData(
+ Object.assign({}, normalized, {
+ recordTime: formatDisplayDateTime(record.createdAt),
+ city: cityLabel || snapshot.birthCity || ""
+ })
+ );
+ },
+ handleBack() {
+ this.safeBack();
+ },
+ updateSafeAreaPadding() {
+ const padding = getPageSafeTop();
+ this.setData({ safeAreaTop: padding });
+ },
+ safeBack() {
+ if (typeof tt !== "undefined" && tt.navigateBack) {
+ tt.navigateBack();
+ }
+ }
+});
diff --git a/DailyFortuneGuide/pages/history-detail/index.json b/DailyFortuneGuide/pages/history-detail/index.json
new file mode 100644
index 0000000..2da9865
--- /dev/null
+++ b/DailyFortuneGuide/pages/history-detail/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationStyle": "custom"
+}
diff --git a/DailyFortuneGuide/pages/history-detail/index.ttml b/DailyFortuneGuide/pages/history-detail/index.ttml
new file mode 100644
index 0000000..6115943
--- /dev/null
+++ b/DailyFortuneGuide/pages/history-detail/index.ttml
@@ -0,0 +1,112 @@
+
+
+
+ 保存时间
+ {{recordTime}}
+ 测算城市
+ {{city}}
+
+
+
+
+ {{summary}}
+
+
+
+
+ 吉祥元素
+ {{luckyGuide.element}}
+
+
+ 幸运色
+ {{luckyGuide.colorText}}
+
+
+ 方位
+ {{luckyGuide.directionText}}
+
+
+ 建议
+ {{luckyGuide.activitiesText}}
+
+
+
+
+ {{item.label}}
+ {{item.period}}
+ {{item.reason}}
+
+
+
+
+
+
+
+ 六维运势
+
+
+
+
+
+ {{item.insight}}
+ {{item.suggestion}}
+
+
+
+
+
+
+
+ 命盘摘要
+ {{profile.birthCity}}
+
+
+ 出生信息
+ {{profile.birthDateTime}}
+
+
+
+
+ {{item.label}}
+ {{item.value}}
+
+
+
+
+
+
+ {{item.element}}
+
+
+
+ {{item.percent}}%
+
+
+
+
+
+
+
+ 解读
+
+ {{narrative}}
+
+
+
+
+
+
+
diff --git a/DailyFortuneGuide/pages/history-detail/index.ttss b/DailyFortuneGuide/pages/history-detail/index.ttss
new file mode 100644
index 0000000..6c353d1
--- /dev/null
+++ b/DailyFortuneGuide/pages/history-detail/index.ttss
@@ -0,0 +1,21 @@
+@import "../result/index.ttss";
+
+.record-card {
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 24rpx;
+ padding: 28rpx;
+ margin-bottom: 28rpx;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.record-label {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.record-value {
+ display: block;
+ font-size: 30rpx;
+ color: #fff7e3;
+ margin-bottom: 16rpx;
+}
diff --git a/DailyFortuneGuide/pages/history/index.js b/DailyFortuneGuide/pages/history/index.js
new file mode 100644
index 0000000..0194456
--- /dev/null
+++ b/DailyFortuneGuide/pages/history/index.js
@@ -0,0 +1,51 @@
+const fortuneStore = require("../../store/fortuneStore");
+const messages = require("../../constants/messages");
+const { formatDisplayDateTime } = require("../../utils/date");
+const { formatRegionDisplay, normalizeRegionArray } = require("../../utils/region");
+const { getPageSafeTop } = require("../../utils/safeArea");
+
+Page({
+ data: {
+ safeAreaTop: 64,
+ history: [],
+ emptyText: messages.HISTORY_EMPTY
+ },
+ onLoad() {
+ this.updateSafeAreaPadding();
+ },
+ onShow() {
+ fortuneStore.loadHistory();
+ const { history } = fortuneStore.getState();
+ const normalized = history.map((item) => {
+ const snapshot = item.formSnapshot || {};
+ const fortune = item.fortune || {};
+ const regionValue = normalizeRegionArray(snapshot.birthRegion);
+ const fallbackSegments = [snapshot.birthProvince, snapshot.birthCity, snapshot.birthDistrict].filter(Boolean);
+ const regionLabel = formatRegionDisplay(regionValue, fallbackSegments);
+ const fortuneDate = fortune.fortuneDate || fortune.FortuneDate || "";
+ const summary = fortune.summary || fortune.Summary || "";
+ return {
+ id: item.id,
+ createdAt: item.createdAt,
+ displayTime: formatDisplayDateTime(item.createdAt),
+ city: regionLabel || snapshot.birthCity || snapshot.birthProvince || "",
+ fortuneDate,
+ summary
+ };
+ });
+ this.setData({ history: normalized });
+ },
+ handleViewDetail(event) {
+ const { id } = event.currentTarget.dataset;
+ if (!id) {
+ return;
+ }
+ if (typeof tt !== "undefined" && tt.navigateTo) {
+ tt.navigateTo({ url: `/pages/history-detail/index?id=${id}` });
+ }
+ },
+ updateSafeAreaPadding() {
+ const padding = getPageSafeTop();
+ this.setData({ safeAreaTop: padding });
+ }
+});
diff --git a/DailyFortuneGuide/pages/history/index.json b/DailyFortuneGuide/pages/history/index.json
new file mode 100644
index 0000000..2da9865
--- /dev/null
+++ b/DailyFortuneGuide/pages/history/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationStyle": "custom"
+}
diff --git a/DailyFortuneGuide/pages/history/index.ttml b/DailyFortuneGuide/pages/history/index.ttml
new file mode 100644
index 0000000..294633a
--- /dev/null
+++ b/DailyFortuneGuide/pages/history/index.ttml
@@ -0,0 +1,19 @@
+
+
+
+ {{emptyText}}
+
+
+
+
+
+ {{item.summary}}
+ 鏈娴嬬畻宸蹭繚瀛橈紝鍙偣鍑绘煡鐪嬭鎯?/text>
+
+
+
+
+
diff --git a/DailyFortuneGuide/pages/history/index.ttss b/DailyFortuneGuide/pages/history/index.ttss
new file mode 100644
index 0000000..c4bf2e7
--- /dev/null
+++ b/DailyFortuneGuide/pages/history/index.ttss
@@ -0,0 +1,48 @@
+.page {
+ min-height: 100vh;
+ padding: 24rpx;
+ box-sizing: border-box;
+}
+
+.content {
+ min-height: 100vh;
+ box-sizing: border-box;
+}
+
+.empty {
+ margin-top: 200rpx;
+ text-align: center;
+ color: rgba(255, 255, 255, 0.5);
+ font-size: 28rpx;
+}
+
+.history-card {
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 22rpx;
+ padding: 28rpx;
+ margin-bottom: 24rpx;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12rpx;
+}
+
+.card-date {
+ font-size: 26rpx;
+ color: #ffead1;
+}
+
+.card-city {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.card-summary {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.72);
+ line-height: 40rpx;
+}
diff --git a/DailyFortuneGuide/pages/home/index.js b/DailyFortuneGuide/pages/home/index.js
new file mode 100644
index 0000000..d3aecab
--- /dev/null
+++ b/DailyFortuneGuide/pages/home/index.js
@@ -0,0 +1,304 @@
+const fortuneStore = require("../../store/fortuneStore");
+const messages = require("../../constants/messages");
+const { analyzeFortune } = require("../../services/fortuneService");
+const { showRewardedVideoAd } = require("../../utils/adService");
+const { getPageSafeTop } = require("../../utils/safeArea");
+const { formatRegionDisplay, deriveRegionFromParts, getRegionParts, normalizeRegionArray } = require("../../utils/region");
+
+const LOADING_HINTS = [
+ "正在推演今日天干地支,请稍候...",
+ "为你梳理六维度能量波动...",
+ "结合命盘与今日日运,编排最佳行动节奏..."
+];
+
+function showToast(title) {
+ if (typeof tt === "undefined" || !tt.showToast) {
+ console.warn("Toast:", title);
+ return;
+ }
+ tt.showToast({
+ title,
+ icon: "none",
+ duration: 2000
+ });
+}
+
+Page({
+ data: {
+ birthDate: "",
+ birthTime: "",
+ selectedCity: "",
+ selectedProvince: "",
+ selectedDistrict: "",
+ regionValue: [],
+ regionDisplay: "",
+ isSubmitting: false,
+ isLoading: false,
+ loadingMessage: "",
+ safeAreaTop: 64,
+ tips: [
+ "建议每天专注测算一次,深度分析更精准",
+ "填写精确城市,可获得更贴近地域气场的指引",
+ "保持心态稳定,才能更好接住好运"
+ ],
+ tipIndex: 0
+ },
+ onLoad() {
+ const { form } = fortuneStore.getState();
+ const persistedRegion = normalizeRegionArray(form.birthRegion);
+ const regionValue = persistedRegion.length ? persistedRegion : this.getInitialRegionValue(form);
+ const parts = getRegionParts(regionValue, {
+ province: form.birthProvince,
+ city: form.birthCity,
+ district: form.birthDistrict
+ });
+ this.setData({
+ birthDate: form.birthDate,
+ birthTime: form.birthTime,
+ selectedProvince: parts.province,
+ selectedCity: parts.city,
+ selectedDistrict: parts.district,
+ regionValue,
+ regionDisplay: formatRegionDisplay(regionValue, [parts.province, parts.city, parts.district]),
+ tipIndex: Math.floor(Math.random() * this.data.tips.length)
+ });
+ if (!persistedRegion.length && regionValue.length && parts.city) {
+ fortuneStore.setForm({
+ birthProvince: parts.province,
+ birthCity: parts.city,
+ birthDistrict: parts.district,
+ birthRegion: regionValue
+ });
+ }
+ this.updateSafeAreaPadding();
+ },
+ getInitialRegionValue(form) {
+ return deriveRegionFromParts({
+ province: form.birthProvince,
+ city: form.birthCity,
+ district: form.birthDistrict
+ });
+ },
+ updateSafeAreaPadding() {
+ const padding = getPageSafeTop();
+ this.setData({ safeAreaTop: padding });
+ },
+ resetRegionState() {
+ this.setData({
+ regionValue: [],
+ selectedProvince: "",
+ selectedCity: "",
+ selectedDistrict: "",
+ regionDisplay: ""
+ });
+ },
+ syncRegion(regionValue, fallbackParts = {}, options = {}) {
+ let normalized = normalizeRegionArray(regionValue);
+ if (!normalized.length) {
+ normalized = deriveRegionFromParts(fallbackParts);
+ }
+ const parts = getRegionParts(normalized, fallbackParts);
+ if (!parts.city) {
+ if (options.resetOnEmpty) {
+ this.resetRegionState();
+ }
+ return false;
+ }
+ this.setData({
+ regionValue: normalized,
+ selectedProvince: parts.province,
+ selectedCity: parts.city,
+ selectedDistrict: parts.district,
+ regionDisplay: formatRegionDisplay(normalized, [parts.province, parts.city, parts.district])
+ });
+ if (options.persist !== false) {
+ fortuneStore.setForm({
+ birthProvince: parts.province,
+ birthCity: parts.city,
+ birthDistrict: parts.district,
+ birthRegion: normalized
+ });
+ }
+ return true;
+ },
+ handleDateChange(event) {
+ const value = event.detail.value;
+ this.setData({ birthDate: value });
+ fortuneStore.setForm({ birthDate: value });
+ },
+ handleTimeChange(event) {
+ const value = event.detail.value;
+ this.setData({ birthTime: value });
+ fortuneStore.setForm({ birthTime: value });
+ },
+ handleRegionChange(event) {
+ const value = event.detail.value || [];
+ this.syncRegion(value, {}, { resetOnEmpty: true });
+ },
+ handleLocate() {
+ if (typeof tt === "undefined" || !tt.chooseLocation) {
+ showToast("当前端暂不支持定位");
+ return;
+ }
+ tt.chooseLocation({
+ success: (res) => {
+ const city = this.extractCity(res.address || res.name || "");
+ if (!city) {
+ showToast("未能识别所在城市,请通过选择器选择");
+ return;
+ }
+ const segments = this.parseAddressSegments(res);
+ const fallback = {
+ province: segments[0] || res.province || "",
+ city,
+ district: segments[2] || res.district || ""
+ };
+ this.syncRegion(segments, fallback, { resetOnEmpty: true });
+ },
+ fail: () => {
+ showToast("定位失败,请检查权限后重试");
+ }
+ });
+ },
+ extractCity(text) {
+ if (!text) {
+ return "";
+ }
+ const cityMatch = text.match(/([\u4e00-\u9fa5]+?(市|自治州|地区|盟))/);
+ if (cityMatch) {
+ return cityMatch[1];
+ }
+ const districtMatch = text.match(/([\u4e00-\u9fa5]+?(区|县))/);
+ if (districtMatch) {
+ return districtMatch[1].replace(/(区|县)$/, "市");
+ }
+ return "";
+ },
+ parseAddressSegments(locationResult) {
+ const segments = [];
+ const province = locationResult.province || "";
+ const city = locationResult.city || "";
+ const address = locationResult.address || "";
+ const text = `${province}${city}${address}`;
+ const provinceMatch = text.match(/([\u4e00-\u9fa5]+?(省|自治区|特别行政区))/);
+ const cityMatch = text.match(/([\u4e00-\u9fa5]+?(市|自治州|地区|盟))/);
+ const districtMatch = text.match(/([\u4e00-\u9fa5]+?(区|县|市))/);
+ if (provinceMatch) {
+ segments.push(provinceMatch[1]);
+ }
+ if (cityMatch) {
+ segments.push(cityMatch[1]);
+ }
+ if (districtMatch && (!segments.length || segments[segments.length - 1] !== districtMatch[1])) {
+ segments.push(districtMatch[1]);
+ }
+ return segments;
+ },
+ handleGoHistory() {
+ if (typeof tt !== "undefined" && tt.navigateTo) {
+ tt.navigateTo({ url: "/pages/history/index" });
+ }
+ },
+ validateForm() {
+ const { birthDate, selectedCity } = this.data;
+ if (!birthDate || !selectedCity) {
+ showToast(messages.FORM_INCOMPLETE);
+ return false;
+ }
+ if (selectedCity.length < 2) {
+ showToast(messages.CITY_INVALID);
+ return false;
+ }
+ if (!fortuneStore.canGenerateToday()) {
+ showToast(messages.QUOTA_EXCEEDED);
+ return false;
+ }
+ return true;
+ },
+ handleGenerate() {
+ if (this.data.isSubmitting) {
+ return;
+ }
+ if (!this.validateForm()) {
+ return;
+ }
+ this.setData({ isSubmitting: true });
+ showRewardedVideoAd()
+ .then(() => this.requestFortune())
+ .catch((error) => {
+ if (error && error.code === "ad_not_completed") {
+ showToast(messages.WATCH_AD_TO_CONTINUE);
+ } else {
+ showToast(messages.GENERATION_FAILED);
+ }
+ this.hideLoadingOverlay();
+ this.setData({ isSubmitting: false });
+ });
+ },
+ requestFortune() {
+ const payload = {
+ birthDate: this.data.birthDate,
+ birthTime: this.data.birthTime || "",
+ birthProvince: this.data.selectedProvince,
+ birthCity: this.data.selectedCity,
+ birthDistrict: this.data.selectedDistrict,
+ birthRegion: Array.isArray(this.data.regionValue) ? this.data.regionValue.slice() : []
+ };
+ this.showLoadingOverlay();
+ return analyzeFortune(payload)
+ .then((response) => {
+
+ if (!response) {
+ showToast(messages.GENERATION_FAILED);
+ return;
+ }
+ fortuneStore.setCurrentFortune(response);
+ fortuneStore.incrementQuota();
+ fortuneStore.appendHistory(
+ {
+ birthDate: payload.birthDate,
+ birthTime: payload.birthTime,
+ birthProvince: payload.birthProvince,
+ birthCity: payload.birthCity,
+ birthDistrict: payload.birthDistrict,
+ birthRegion: payload.birthRegion
+ },
+ response
+ );
+ if (typeof tt !== "undefined" && tt.navigateTo) {
+ tt.navigateTo({ url: "/pages/result/index" });
+ }
+ })
+ .catch((error) => {
+ console.log(error)
+ const message = error && error.data && error.data.message;
+ if (message === "CONTENT_RISK") {
+ showToast(messages.CONTENT_RISK);
+ } else if (message === "BIRTHDATE_INVALID") {
+ showToast("出生日期格式异常,请重新选择");
+ } else {
+ showToast(messages.GENERATION_FAILED);
+ }
+ })
+ .finally(() => {
+ this.hideLoadingOverlay();
+ this.setData({ isSubmitting: false });
+ });
+ },
+ showLoadingOverlay() {
+ const hint = LOADING_HINTS[Math.floor(Math.random() * LOADING_HINTS.length)];
+ this.setData({
+ isLoading: true,
+ loadingMessage: hint
+ });
+ },
+ hideLoadingOverlay() {
+ if (!this.data.isLoading) {
+ return;
+ }
+ this.setData({
+ isLoading: false,
+ loadingMessage: ""
+ });
+ }
+});
diff --git a/DailyFortuneGuide/pages/home/index.json b/DailyFortuneGuide/pages/home/index.json
new file mode 100644
index 0000000..2da9865
--- /dev/null
+++ b/DailyFortuneGuide/pages/home/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationStyle": "custom"
+}
diff --git a/DailyFortuneGuide/pages/home/index.ttml b/DailyFortuneGuide/pages/home/index.ttml
new file mode 100644
index 0000000..1577544
--- /dev/null
+++ b/DailyFortuneGuide/pages/home/index.ttml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+ {{loadingMessage}}
+ 深度推算无需久等,请保持耐心
+
+
+
+
+
+
+ 行知节奏
+ 每日运势指南
+ 结合八字命盘与当日天象,规划六维行动节奏
+
+
+ 八字推演
+ 吉时提醒
+ 专属建议
+
+
+
+
+
+
+ 出生日期
+
+ {{birthDate}}
+
+
+
+ 出生时间(可选)
+
+
+ {{birthTime || '不清楚时间'}}
+
+
+
+
+ 出生城市
+
+
+
+ {{regionDisplay || '请选择出生城市'}}
+
+
+
+
+
+
+
+
+
+ 结果生成
+ 完成激励广告后立即开启
+
+
+
+
+
+
+ 灵感提示
+ {{tips[tipIndex]}}
+
+
+
diff --git a/DailyFortuneGuide/pages/home/index.ttss b/DailyFortuneGuide/pages/home/index.ttss
new file mode 100644
index 0000000..742c7c1
--- /dev/null
+++ b/DailyFortuneGuide/pages/home/index.ttss
@@ -0,0 +1,259 @@
+.page {
+ position: relative;
+ min-height: 100vh;
+ padding: 0 24rpx 80rpx;
+ box-sizing: border-box;
+}
+
+.glow-layer {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+}
+
+.glow {
+ position: absolute;
+ border-radius: 999px;
+ filter: blur(120rpx);
+ opacity: 0.6;
+}
+
+.glow-one {
+ width: 320rpx;
+ height: 320rpx;
+ top: 60rpx;
+ right: -80rpx;
+ background: #ff6ac1;
+}
+
+.glow-two {
+ width: 260rpx;
+ height: 260rpx;
+ bottom: 300rpx;
+ left: -100rpx;
+ background: #6c7bff;
+}
+
+.glow-three {
+ width: 220rpx;
+ height: 220rpx;
+ bottom: 80rpx;
+ right: 20rpx;
+ background: #48d2c0;
+}
+
+.content {
+ position: relative;
+ z-index: 1;
+ min-height: 100vh;
+ box-sizing: border-box;
+}
+
+.hero {
+ margin-bottom: 32rpx;
+}
+
+.hero-text {
+ margin-bottom: 20rpx;
+}
+
+.hero-label {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.6);
+ letter-spacing: 4rpx;
+}
+
+.hero-title {
+ display: block;
+ margin-top: 12rpx;
+ font-size: 48rpx;
+ font-weight: 700;
+ color: #fff3d6;
+}
+
+.hero-desc {
+ display: block;
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.7);
+ margin-top: 12rpx;
+}
+
+.hero-tags {
+ display: flex;
+ gap: 16rpx;
+}
+
+.tag {
+ padding: 10rpx 26rpx;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.12);
+ color: #fff;
+ font-size: 24rpx;
+}
+
+.card {
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 26rpx;
+ padding: 32rpx;
+ margin-bottom: 28rpx;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(8px);
+}
+
+.card-header {
+ display: flex;
+ flex-direction: column;
+ gap: 6rpx;
+ margin-bottom: 20rpx;
+}
+
+.card-title {
+ font-size: 30rpx;
+ color: #fff9ea;
+ font-weight: 600;
+}
+
+.card-subtitle {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.form-item + .form-item {
+ margin-top: 28rpx;
+}
+
+.label {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.65);
+ margin-bottom: 12rpx;
+ display: block;
+}
+
+.picker-value {
+ width: 100%;
+ padding: 24rpx;
+ min-height: 72rpx;
+ border-radius: 20rpx;
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ font-size: 30rpx;
+ color: #fff7e3;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.placeholder {
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.city-row {
+ /* display: flex; */
+ align-items: center;
+ gap: 16rpx;
+}
+
+.locate-btn {
+ padding: 0 28rpx;
+ height: 68rpx;
+ line-height: 64rpx;
+ border-radius: 22rpx;
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ color: #fff;
+ font-size: 24rpx;
+ background: transparent;
+}
+
+.cta-card .primary {
+ margin-top: 24rpx;
+}
+
+.cta-card .secondary {
+ margin-top: 16rpx;
+}
+
+.cta-text {
+ display: flex;
+ flex-direction: column;
+ gap: 6rpx;
+}
+
+.cta-title {
+ font-size: 30rpx;
+ color: #fff9ea;
+ font-weight: 600;
+}
+
+.cta-subtitle {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.tips-card {
+ margin-bottom: 80rpx;
+}
+
+.tips-label {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.tips-text {
+ display: block;
+ margin-top: 12rpx;
+ font-size: 28rpx;
+ line-height: 42rpx;
+ color: #fff7e3;
+}
+
+.loading-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(5, 6, 15, 0.82);
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(4px);
+}
+
+.loading-dialog {
+ width: 520rpx;
+ padding: 48rpx 32rpx;
+ border-radius: 32rpx;
+ background: rgba(23, 27, 48, 0.92);
+ text-align: center;
+}
+
+.loading-spinner {
+ width: 88rpx;
+ height: 88rpx;
+ margin: 0 auto 24rpx;
+ border-radius: 50%;
+ border: 6rpx solid rgba(255, 255, 255, 0.12);
+ border-top-color: #ffb85c;
+ animation: spin 1.2s linear infinite;
+}
+
+.loading-text {
+ font-size: 30rpx;
+ color: #fff9eb;
+}
+
+.loading-subtext {
+ margin-top: 12rpx;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.55);
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/DailyFortuneGuide/pages/result/index.js b/DailyFortuneGuide/pages/result/index.js
new file mode 100644
index 0000000..1f168e0
--- /dev/null
+++ b/DailyFortuneGuide/pages/result/index.js
@@ -0,0 +1,63 @@
+const fortuneStore = require("../../store/fortuneStore");
+const { normalizeFortunePayload } = require("../../utils/fortuneFormatter");
+const { getPageSafeTop } = require("../../utils/safeArea");
+
+function showToast(title) {
+ if (typeof tt === "undefined" || !tt.showToast) {
+ console.warn("Toast:", title);
+ return;
+ }
+ tt.showToast({
+ title,
+ icon: "none",
+ duration: 2000
+ });
+}
+
+Page({
+ data: {
+ safeAreaTop: 64,
+ fortuneDate: "",
+ summary: "",
+ narrative: "",
+ dimensions: [],
+ luckyGuide: null,
+ profile: null,
+ overallScore: 0
+ },
+ onLoad() {
+ this.updateSafeAreaPadding();
+ const { currentFortune } = fortuneStore.getState();
+ if (!currentFortune) {
+ showToast("暂无结果,请返回重新生成");
+ setTimeout(() => {
+ if (typeof tt !== "undefined" && tt.navigateBack) {
+ tt.navigateBack();
+ }
+ }, 1000);
+ return;
+ }
+ const normalized = normalizeFortunePayload(currentFortune);
+ if (normalized) {
+ this.setData(normalized);
+ }
+ },
+ updateSafeAreaPadding() {
+ const padding = getPageSafeTop();
+ this.setData({ safeAreaTop: padding });
+ },
+ handleBack() {
+ if (typeof tt !== "undefined" && tt.navigateBack) {
+ tt.navigateBack();
+ return;
+ }
+ if (typeof tt !== "undefined" && tt.redirectTo) {
+ tt.redirectTo({ url: "/pages/home/index" });
+ }
+ },
+ handleGoHistory() {
+ if (typeof tt !== "undefined" && tt.navigateTo) {
+ tt.navigateTo({ url: "/pages/history/index" });
+ }
+ }
+});
diff --git a/DailyFortuneGuide/pages/result/index.json b/DailyFortuneGuide/pages/result/index.json
new file mode 100644
index 0000000..2da9865
--- /dev/null
+++ b/DailyFortuneGuide/pages/result/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationStyle": "custom"
+}
diff --git a/DailyFortuneGuide/pages/result/index.ttml b/DailyFortuneGuide/pages/result/index.ttml
new file mode 100644
index 0000000..5f7abe6
--- /dev/null
+++ b/DailyFortuneGuide/pages/result/index.ttml
@@ -0,0 +1,122 @@
+
+
+
+
+
+ {{summary}}
+
+
+
+
+ 吉祥元素
+ {{luckyGuide.element}}
+
+
+ 幸运色
+ {{luckyGuide.colorText}}
+
+
+ 顺势方位
+ {{luckyGuide.directionText}}
+
+
+ 随身物件
+ {{luckyGuide.propsText}}
+
+
+ 行动建议
+ {{luckyGuide.activitiesText}}
+
+
+
+
+ {{item.label}}
+ {{item.period}}
+ {{item.reason}}
+
+
+
+
+
+
+
+ 六维运势
+ 结合趋势安排节奏
+
+
+
+
+
+ {{item.insight}}
+ {{item.suggestion}}
+
+
+
+
+
+
+
+ 命盘摘要
+
+ {{(profile.birthProvince ? profile.birthProvince + ' · ' : '') + profile.birthCity}}
+
+
+
+ 出生信息
+ {{profile.birthDateTime}}
+
+
+
+
+ {{item.label}}
+ {{item.value}}
+
+
+
+
+
+
+ {{item.label}}
+ {{item.value}}
+
+
+
+
+
+
+ {{item.element}}
+
+
+
+ {{item.percent}}%
+
+
+
+
+
+
+
+ AI 深度解读
+
+ {{narrative}}
+
+
+
+
+
+
+
+
diff --git a/DailyFortuneGuide/pages/result/index.ttss b/DailyFortuneGuide/pages/result/index.ttss
new file mode 100644
index 0000000..7158447
--- /dev/null
+++ b/DailyFortuneGuide/pages/result/index.ttss
@@ -0,0 +1,284 @@
+.page {
+ position: relative;
+ min-height: 100vh;
+ padding: 24rpx;
+ box-sizing: border-box;
+}
+
+.glow-bg {
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(120% 120% at 50% 0%, rgba(255, 106, 193, 0.2), transparent 70%);
+ z-index: 0;
+}
+
+.content {
+ position: relative;
+ z-index: 1;
+ min-height: 100vh;
+ box-sizing: border-box;
+}
+
+.summary-card,
+.lucky-card,
+.dimension-card,
+.profile-card,
+.narrative-card {
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 24rpx;
+ padding: 28rpx;
+ margin-bottom: 28rpx;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(10px);
+}
+
+.summary-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.summary-title {
+ font-size: 30rpx;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.summary-date {
+ display: block;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.45);
+ margin-top: 6rpx;
+}
+
+.summary-score {
+ font-size: 64rpx;
+ font-weight: 700;
+ color: #ffb85c;
+}
+
+.summary-text {
+ margin-top: 24rpx;
+ font-size: 30rpx;
+ line-height: 44rpx;
+ color: #fff7e3;
+}
+
+.card-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20rpx;
+ font-size: 30rpx;
+ color: #fff7e3;
+}
+
+.card-subtitle {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.element-tag {
+ padding: 8rpx 20rpx;
+ border-radius: 20rpx;
+ background: rgba(255, 184, 92, 0.15);
+ color: #ffb85c;
+}
+
+.guide-row {
+ display: flex;
+ margin-bottom: 12rpx;
+}
+
+.guide-label {
+ width: 150rpx;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.guide-value {
+ flex: 1;
+ font-size: 28rpx;
+ color: #fff7e3;
+ line-height: 40rpx;
+}
+
+.time-slots {
+ margin-top: 16rpx;
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.time-slot {
+ padding: 20rpx;
+ border-radius: 18rpx;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.time-label {
+ font-size: 26rpx;
+ color: #ffead1;
+}
+
+.time-period {
+ display: block;
+ font-size: 32rpx;
+ font-weight: 600;
+ margin: 8rpx 0;
+}
+
+.time-reason {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.dimension-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20rpx;
+}
+
+.dimension-item {
+ background: rgba(255, 255, 255, 0.06);
+ border-radius: 20rpx;
+ padding: 20rpx;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.dimension-header {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+ margin-bottom: 12rpx;
+}
+
+.dimension-icon {
+ font-size: 36rpx;
+}
+
+.dimension-title {
+ font-size: 28rpx;
+ color: #fff7e3;
+}
+
+.dimension-trend {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.dimension-score {
+ margin-left: auto;
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #ffb85c;
+}
+
+.dimension-insight {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.75);
+ line-height: 36rpx;
+}
+
+.dimension-suggestion {
+ display: block;
+ margin-top: 8rpx;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.profile-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 16rpx;
+}
+
+.profile-label {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.profile-value {
+ font-size: 26rpx;
+ color: #fff7e3;
+}
+
+.pillars {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 12rpx;
+ margin-bottom: 16rpx;
+}
+
+.pillars.today {
+ margin-top: 8rpx;
+}
+
+.pillar {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 16rpx;
+ padding: 16rpx;
+ text-align: center;
+}
+
+.pillar-label {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.pillar-value {
+ font-size: 28rpx;
+ color: #fff7e3;
+ margin-top: 8rpx;
+}
+
+.elements {
+ display: flex;
+ flex-direction: column;
+ gap: 12rpx;
+ margin-top: 12rpx;
+}
+
+.element-bar {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+
+.element-label {
+ width: 60rpx;
+ font-size: 26rpx;
+}
+
+.bar-track {
+ flex: 1;
+ height: 14rpx;
+ border-radius: 8rpx;
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.bar-fill {
+ height: 100%;
+ border-radius: 8rpx;
+ background: linear-gradient(90deg, #ffb85c, #ff6ac1);
+}
+
+.element-percent {
+ width: 80rpx;
+ text-align: right;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.narrative-text {
+ font-size: 28rpx;
+ line-height: 44rpx;
+ color: rgba(255, 255, 255, 0.78);
+}
+
+.actions {
+ display: flex;
+ flex-direction: column;
+ gap: 20rpx;
+ margin: 40rpx 0 80rpx;
+}
diff --git a/DailyFortuneGuide/project.config.json b/DailyFortuneGuide/project.config.json
new file mode 100644
index 0000000..dcbf74f
--- /dev/null
+++ b/DailyFortuneGuide/project.config.json
@@ -0,0 +1 @@
+{"miniprogramRoot":"./","projectname":"DailyFortuneGuide","compileType":"miniprogram","appid":"testAppId","setting":{"urlCheck":false,"es6":true,"postcss":true,"minified":true,"newFeature":true},"simulatorType":"wechat","condition":{}}
\ No newline at end of file
diff --git a/DailyFortuneGuide/services/fortuneService.js b/DailyFortuneGuide/services/fortuneService.js
new file mode 100644
index 0000000..d08f2d6
--- /dev/null
+++ b/DailyFortuneGuide/services/fortuneService.js
@@ -0,0 +1,39 @@
+const config = require("../config/index");
+
+function post(endpoint, data) {
+ return new Promise((resolve, reject) => {
+ if (typeof tt === "undefined" || !tt.request) {
+ reject(new Error("request_api_unavailable"));
+ return;
+ }
+
+ tt.request({
+ url: `${config.apiBaseUrl}${endpoint}`,
+ method: "POST",
+ data,
+ timeout: 60000,
+ success(response) {
+ const { statusCode, data: payload } = response;
+ if (statusCode >= 200 && statusCode < 300) {
+ resolve(payload);
+ return;
+ }
+ const error = new Error("request_failed");
+ error.statusCode = statusCode;
+ error.data = payload;
+ reject(error);
+ },
+ fail(error) {
+ reject(error);
+ }
+ });
+ });
+}
+
+function analyzeFortune(payload) {
+ return post("/api/daily-fortune/analyze", payload);
+}
+
+module.exports = {
+ analyzeFortune
+};
diff --git a/DailyFortuneGuide/sitemap.json b/DailyFortuneGuide/sitemap.json
new file mode 100644
index 0000000..f94d4d3
--- /dev/null
+++ b/DailyFortuneGuide/sitemap.json
@@ -0,0 +1,4 @@
+{
+ "desc": "sitemap",
+ "rules": []
+}
diff --git a/DailyFortuneGuide/store/fortuneStore.js b/DailyFortuneGuide/store/fortuneStore.js
new file mode 100644
index 0000000..55fdafa
--- /dev/null
+++ b/DailyFortuneGuide/store/fortuneStore.js
@@ -0,0 +1,176 @@
+const config = require("../config/index");
+const { formatDate } = require("../utils/date");
+const { safeGetStorage, safeSetStorage } = require("../utils/storage");
+const { normalizeRegionArray, deriveRegionFromParts, getRegionParts } = require("../utils/region");
+
+const STORAGE_KEYS = {
+ history: "fortune:history",
+ quotaPrefix: "fortune:quota:",
+ form: "fortune:form"
+};
+
+function createDefaultForm() {
+ return {
+ birthDate: formatDate(new Date()),
+ birthTime: "",
+ birthProvince: "",
+ birthCity: "",
+ birthDistrict: "",
+ birthRegion: []
+ };
+}
+
+function normalizeForm(payload, fallback) {
+ const base = fallback || createDefaultForm();
+ const toStringValue = (value, fallbackValue = "") => (typeof value === "string" ? value : fallbackValue);
+ const normalized = {
+ birthDate: toStringValue(payload && payload.birthDate, base.birthDate),
+ birthTime: toStringValue(payload && payload.birthTime, base.birthTime || ""),
+ birthProvince: toStringValue(payload && payload.birthProvince, base.birthProvince || ""),
+ birthCity: toStringValue(payload && payload.birthCity, base.birthCity || ""),
+ birthDistrict: toStringValue(payload && payload.birthDistrict, base.birthDistrict || ""),
+ birthRegion: []
+ };
+ const persistedRegion = normalizeRegionArray(payload && payload.birthRegion);
+ const baseRegion = normalizeRegionArray(base.birthRegion);
+ if (persistedRegion.length) {
+ normalized.birthRegion = persistedRegion;
+ } else if (baseRegion.length) {
+ normalized.birthRegion = baseRegion;
+ } else {
+ normalized.birthRegion = deriveRegionFromParts({
+ province: normalized.birthProvince,
+ city: normalized.birthCity,
+ district: normalized.birthDistrict
+ });
+ }
+ const parts = getRegionParts(normalized.birthRegion, {
+ province: normalized.birthProvince,
+ city: normalized.birthCity,
+ district: normalized.birthDistrict
+ });
+ normalized.birthProvince = parts.province;
+ normalized.birthCity = parts.city;
+ normalized.birthDistrict = parts.district;
+ return normalized;
+}
+
+function loadPersistedForm() {
+ const stored = safeGetStorage(STORAGE_KEYS.form, null);
+ if (stored && typeof stored === "object") {
+ return normalizeForm(stored);
+ }
+ const defaults = createDefaultForm();
+ safeSetStorage(STORAGE_KEYS.form, defaults);
+ return defaults;
+}
+
+function persistForm(form) {
+ const normalized = normalizeForm(form);
+ safeSetStorage(STORAGE_KEYS.form, normalized);
+}
+
+const state = {
+ form: loadPersistedForm(),
+ currentFortune: null,
+ history: [],
+ quota: {
+ date: "",
+ count: 0
+ },
+ lastError: ""
+};
+
+function getQuotaStorageKey(dateStr) {
+ return `${STORAGE_KEYS.quotaPrefix}${dateStr.replace(/-/g, "")}`;
+}
+
+function syncQuota() {
+ const today = formatDate(new Date());
+ const storageKey = getQuotaStorageKey(today);
+ const stored = safeGetStorage(storageKey, 0);
+ const count = typeof stored === "number" ? stored : 0;
+ state.quota = {
+ date: today,
+ count
+ };
+}
+
+function incrementQuota() {
+ const today = formatDate(new Date());
+ if (state.quota.date !== today) {
+ syncQuota();
+ }
+ const nextCount = state.quota.count + 1;
+ state.quota = {
+ date: today,
+ count: nextCount
+ };
+ safeSetStorage(getQuotaStorageKey(today), nextCount);
+}
+
+function canGenerateToday() {
+ const today = formatDate(new Date());
+ if (state.quota.date !== today) {
+ syncQuota();
+ }
+ return state.quota.count < config.maxDailyQuota;
+}
+
+function loadHistory() {
+ const stored = safeGetStorage(STORAGE_KEYS.history, []);
+ state.history = Array.isArray(stored) ? stored : [];
+}
+
+function saveHistory() {
+ safeSetStorage(STORAGE_KEYS.history, state.history);
+}
+
+function appendHistory(formSnapshot, fortune) {
+ const snapshot = normalizeForm(formSnapshot, state.form);
+ const entry = {
+ id: `${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
+ createdAt: new Date().toISOString(),
+ formSnapshot: snapshot,
+ fortune
+ };
+ const next = [entry, ...state.history];
+ state.history = next.slice(0, config.historyLimit);
+ saveHistory();
+ return entry;
+}
+
+function getHistoryById(id) {
+ return state.history.find((item) => item.id === id);
+}
+
+function setForm(partial) {
+ const merged = Object.assign({}, state.form, partial);
+ state.form = normalizeForm(merged, state.form);
+ persistForm(state.form);
+}
+
+function setCurrentFortune(fortune) {
+ state.currentFortune = fortune;
+}
+
+function getState() {
+ return state;
+}
+
+function initStore() {
+ syncQuota();
+ loadHistory();
+}
+
+module.exports = {
+ initStore,
+ getState,
+ setForm,
+ setCurrentFortune,
+ appendHistory,
+ loadHistory,
+ getHistoryById,
+ canGenerateToday,
+ incrementQuota
+};
diff --git a/DailyFortuneGuide/utils/adService.js b/DailyFortuneGuide/utils/adService.js
new file mode 100644
index 0000000..e8bc6c8
--- /dev/null
+++ b/DailyFortuneGuide/utils/adService.js
@@ -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
+};
diff --git a/DailyFortuneGuide/utils/date.js b/DailyFortuneGuide/utils/date.js
new file mode 100644
index 0000000..c1e6818
--- /dev/null
+++ b/DailyFortuneGuide/utils/date.js
@@ -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
+};
diff --git a/DailyFortuneGuide/utils/fortuneFormatter.js b/DailyFortuneGuide/utils/fortuneFormatter.js
new file mode 100644
index 0000000..c7020b9
--- /dev/null
+++ b/DailyFortuneGuide/utils/fortuneFormatter.js
@@ -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
+};
+
diff --git a/DailyFortuneGuide/utils/region.js b/DailyFortuneGuide/utils/region.js
new file mode 100644
index 0000000..c7c2be1
--- /dev/null
+++ b/DailyFortuneGuide/utils/region.js
@@ -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
+};
diff --git a/DailyFortuneGuide/utils/safeArea.js b/DailyFortuneGuide/utils/safeArea.js
new file mode 100644
index 0000000..4863b04
--- /dev/null
+++ b/DailyFortuneGuide/utils/safeArea.js
@@ -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
+};
diff --git a/DailyFortuneGuide/utils/storage.js b/DailyFortuneGuide/utils/storage.js
new file mode 100644
index 0000000..a4ea0c5
--- /dev/null
+++ b/DailyFortuneGuide/utils/storage.js
@@ -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
+};
diff --git a/README.md b/README.md
index b0b86ea..81866c1 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,6 @@
# MiniProgram
+## 子项目
+- `NamingAssistant`:AI 取名与收藏工具。
+- `DailyFortuneGuide`:每日运势指南,结合八字命盘与当日天象提供六维运势、吉祥元素与历史记录功能。
+