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}} + + + + + + 整体指数 + {{fortuneDate || '历史记录'}} + + {{overallScore}} + + {{summary}} + + + + + 吉祥元素 + {{luckyGuide.element}} + + + 幸运色 + {{luckyGuide.colorText}} + + + 方位 + {{luckyGuide.directionText}} + + + 建议 + {{luckyGuide.activitiesText}} + + + + + {{item.label}} + {{item.period}} + {{item.reason}} + + + + + + + + 六维运势 + + + + + + {{item.icon}} + + {{item.title}} + {{item.trendText}} + + {{item.score}} + + {{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.displayTime}} + {{item.city}} + + {{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 @@ + + + + + + + 今日整体指数 + {{fortuneDate || '今日'}} + + {{overallScore}} + + {{summary}} + + + + + 吉祥元素 + {{luckyGuide.element}} + + + 幸运色 + {{luckyGuide.colorText}} + + + 顺势方位 + {{luckyGuide.directionText}} + + + 随身物件 + {{luckyGuide.propsText}} + + + 行动建议 + {{luckyGuide.activitiesText}} + + + + + {{item.label}} + {{item.period}} + {{item.reason}} + + + + + + + + 六维运势 + 结合趋势安排节奏 + + + + + + {{item.icon}} + + {{item.title}} + {{item.trendText}} + + {{item.score}} + + {{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`:每日运势指南,结合八字命盘与当日天象提供六维运势、吉祥元素与历史记录功能。 +