diff --git a/App.vue b/App.vue index 9e4ff62..533365c 100644 --- a/App.vue +++ b/App.vue @@ -47,6 +47,31 @@ diff --git a/components/yxg/yxg-page.vue b/components/yxg/yxg-page.vue new file mode 100644 index 0000000..05456ab --- /dev/null +++ b/components/yxg/yxg-page.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/components/yxg/yxg-tool-icon.vue b/components/yxg/yxg-tool-icon.vue new file mode 100644 index 0000000..9944b81 --- /dev/null +++ b/components/yxg/yxg-tool-icon.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/manifest.json b/manifest.json index dda9a5a..2441a37 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name" : "yuxingu-miniprogram", - "appid" : "__UNI__E47533F", + "appid" : "__UNI__29F79D0", "description" : "", "versionName" : "1.0.0", "versionCode" : "100", diff --git a/pages.json b/pages.json index 0f19f30..78c27dd 100644 --- a/pages.json +++ b/pages.json @@ -265,14 +265,28 @@ ] }, { - "root": "pages/webview-page", + "root": "pages/yxg", "pages": [ - { - "path": "webview-page", - "style": { - "navigationBarTitleText": "" - } - } + { "path": "explore", "style": { "navigationBarTitleText": "探索", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "explore-bank", "style": { "navigationBarTitleText": "题库", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "explore-category", "style": { "navigationBarTitleText": "探索", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "growth-plan", "style": { "navigationBarTitleText": "成长计划", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "ask", "style": { "navigationBarTitleText": "AI问答", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "companion", "style": { "navigationBarTitleText": "节气陪伴", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "mine", "style": { "navigationBarTitleText": "我的魔方", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "profile", "style": { "navigationBarTitleText": "个人档案", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "portrait", "style": { "navigationBarTitleText": "愈心解码", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "star", "style": { "navigationBarTitleText": "星座", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "synastry", "style": { "navigationBarTitleText": "合盘", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "synastry-invite", "style": { "navigationBarTitleText": "合盘邀请", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "rhythm", "style": { "navigationBarTitleText": "身心节律", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "cards", "style": { "navigationBarTitleText": "意象卡片", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "relation", "style": { "navigationBarTitleText": "人格匹配", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "membership", "style": { "navigationBarTitleText": "成长会员", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "scale", "style": { "navigationBarTitleText": "测评", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "share", "style": { "navigationBarTitleText": "分享", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "reports", "style": { "navigationBarTitleText": "成长报告", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } }, + { "path": "report", "style": { "navigationBarTitleText": "报告详情", "navigationBarBackgroundColor": "#ffd1c7", "backgroundColor": "#ffd1c7" } } ] } ], diff --git a/pages/index/index.vue b/pages/index/index.vue index 89eb8ff..dd7653c 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -495,7 +495,6 @@ - @@ -511,8 +510,6 @@ const consultList = ref([]) const newHealingList = ref([]) const newActivityList = ref([]) - const showWebView = ref(false) - const externalUrl = ref() const dotsStyles = ref({ backgroundColor: 'rgba(96, 98, 113, 1)', border: '1px rgba(96, 98, 113, 1) solid', @@ -596,16 +593,6 @@ }) } - const toTuTest=()=>{ - - const externalUrl = 'https://cps.jsbr.org.cn/login.html'; - uni.navigateTo({ - url: `/pages/webview-page/webview-page?url=${encodeURIComponent(externalUrl)}` - }); - - - } - const toHealingDetail=(item)=>{ dohttp.navigateTo("/pages/healing-detail/healing-detail?id=" + item.id) } diff --git a/pages/login-pop/login-pop.vue b/pages/login-pop/login-pop.vue index 23aa354..fc3a67d 100644 --- a/pages/login-pop/login-pop.vue +++ b/pages/login-pop/login-pop.vue @@ -154,25 +154,41 @@ } } + const afterLogin = () => { + if (type.value == 1) { + uni.reLaunch({ + url: "/pages/index/index" + }) + } else if (type.value == 3) { + uni.reLaunch({ + url: path.value + }) + } else { + uni.navigateBack({ + delta: 1 + }) + } + } const toLogin = (code, encryptedData, iv) => { - const params = { - grantType: "mini_app", - account: code, - } - - - let others = { - encryptedData: encryptedData, - iv: iv, - code: code - } - params.others = JSON.stringify(others) - dohttp.post(urls.loginUrl, params).then((result) => { - userInfo.value.token = result.accessToken - uni.setStorageSync('token', result.accessToken) + dohttp.post(urls.loginUrl, { code, encryptedData, iv }).then((result) => { + const token = result.token || result.accessToken + userInfo.value.token = token + uni.setStorageSync('token', token) + uni.removeStorageSync('yxg_token') + if (result.user) { + uni.setStorageSync('userinfo', { + id: result.user.id, + avatarUrl: result.user.avatar_url, + nickName: result.user.nickname + }) + } loginPop.value.close() - getUserInfo() + }).catch((e) => { + uni.showToast({ + title: (e && e.errMsg ? String(e.errMsg).replace('【request】', '') : '登录失败'), + icon: 'none' + }) }) } const getUserInfo = () => { @@ -182,23 +198,11 @@ avatarUrl: result.avatarUrl, nickName: result.nickName } - uni.setStorageSync('userinfo', userinfo) - if (type.value == 1) { - uni.reLaunch({ - url: "/pages/index/index" - }) - } else if (type.value == 3) { - uni.reLaunch({ - url: path.value - }) - } else { - uni.navigateBack({ - delta: 1 - }) - } + afterLogin() + }).catch(() => { + afterLogin() }) - } diff --git a/pages/webview-page/webview-page.vue b/pages/webview-page/webview-page.vue deleted file mode 100644 index 879bea1..0000000 --- a/pages/webview-page/webview-page.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/pages/yxg-magic/index.vue b/pages/yxg-magic/index.vue index eefd2d9..0f5fac1 100644 --- a/pages/yxg-magic/index.vue +++ b/pages/yxg-magic/index.vue @@ -1,140 +1,416 @@ diff --git a/pages/yxg/ask.vue b/pages/yxg/ask.vue new file mode 100644 index 0000000..a3fe0d2 --- /dev/null +++ b/pages/yxg/ask.vue @@ -0,0 +1,155 @@ + + + + + + diff --git a/pages/yxg/cards.vue b/pages/yxg/cards.vue new file mode 100644 index 0000000..c97124c --- /dev/null +++ b/pages/yxg/cards.vue @@ -0,0 +1,104 @@ + + + + + + diff --git a/pages/yxg/companion.vue b/pages/yxg/companion.vue new file mode 100644 index 0000000..e7027f1 --- /dev/null +++ b/pages/yxg/companion.vue @@ -0,0 +1,102 @@ + + + + + + diff --git a/pages/yxg/explore-bank.vue b/pages/yxg/explore-bank.vue new file mode 100644 index 0000000..f27033a --- /dev/null +++ b/pages/yxg/explore-bank.vue @@ -0,0 +1,85 @@ + + + + + + diff --git a/pages/yxg/explore-category.vue b/pages/yxg/explore-category.vue new file mode 100644 index 0000000..1325640 --- /dev/null +++ b/pages/yxg/explore-category.vue @@ -0,0 +1,77 @@ + + + + + + diff --git a/pages/yxg/explore.vue b/pages/yxg/explore.vue new file mode 100644 index 0000000..b8e3244 --- /dev/null +++ b/pages/yxg/explore.vue @@ -0,0 +1,120 @@ + + + + + + diff --git a/pages/yxg/growth-plan.vue b/pages/yxg/growth-plan.vue new file mode 100644 index 0000000..a02f917 --- /dev/null +++ b/pages/yxg/growth-plan.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/pages/yxg/membership.vue b/pages/yxg/membership.vue new file mode 100644 index 0000000..9403f78 --- /dev/null +++ b/pages/yxg/membership.vue @@ -0,0 +1,106 @@ + + + + + + diff --git a/pages/yxg/mine.vue b/pages/yxg/mine.vue new file mode 100644 index 0000000..7b7f94b --- /dev/null +++ b/pages/yxg/mine.vue @@ -0,0 +1,160 @@ + + + + + + diff --git a/pages/yxg/portrait.vue b/pages/yxg/portrait.vue new file mode 100644 index 0000000..013d136 --- /dev/null +++ b/pages/yxg/portrait.vue @@ -0,0 +1,128 @@ + + + + + + diff --git a/pages/yxg/profile.vue b/pages/yxg/profile.vue new file mode 100644 index 0000000..7346c4e --- /dev/null +++ b/pages/yxg/profile.vue @@ -0,0 +1,238 @@ + + + + + + diff --git a/pages/yxg/relation.vue b/pages/yxg/relation.vue new file mode 100644 index 0000000..cb456e2 --- /dev/null +++ b/pages/yxg/relation.vue @@ -0,0 +1,119 @@ + + + + + + diff --git a/pages/yxg/report.vue b/pages/yxg/report.vue new file mode 100644 index 0000000..f587eee --- /dev/null +++ b/pages/yxg/report.vue @@ -0,0 +1,83 @@ + + + + + + diff --git a/pages/yxg/reports.vue b/pages/yxg/reports.vue new file mode 100644 index 0000000..a93b1db --- /dev/null +++ b/pages/yxg/reports.vue @@ -0,0 +1,105 @@ + + + + + + diff --git a/pages/yxg/rhythm.vue b/pages/yxg/rhythm.vue new file mode 100644 index 0000000..b39f3a4 --- /dev/null +++ b/pages/yxg/rhythm.vue @@ -0,0 +1,104 @@ + + + + + + diff --git a/pages/yxg/scale.vue b/pages/yxg/scale.vue new file mode 100644 index 0000000..86b66fb --- /dev/null +++ b/pages/yxg/scale.vue @@ -0,0 +1,207 @@ + + + + + + diff --git a/pages/yxg/share.vue b/pages/yxg/share.vue new file mode 100644 index 0000000..f574bf6 --- /dev/null +++ b/pages/yxg/share.vue @@ -0,0 +1,77 @@ + + + + + + diff --git a/pages/yxg/star.vue b/pages/yxg/star.vue new file mode 100644 index 0000000..4534285 --- /dev/null +++ b/pages/yxg/star.vue @@ -0,0 +1,104 @@ + + + + + + diff --git a/pages/yxg/synastry-invite.vue b/pages/yxg/synastry-invite.vue new file mode 100644 index 0000000..39bd38a --- /dev/null +++ b/pages/yxg/synastry-invite.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/pages/yxg/synastry.vue b/pages/yxg/synastry.vue new file mode 100644 index 0000000..2cf715b --- /dev/null +++ b/pages/yxg/synastry.vue @@ -0,0 +1,141 @@ + + + + + + diff --git a/util/apiUrl.js b/util/apiUrl.js index 52cf71e..f9230f7 100644 --- a/util/apiUrl.js +++ b/util/apiUrl.js @@ -1,52 +1,48 @@ -//统一设置API接口地址 +// 统一打 Go /api/v1(ECR-050 咨询域已原生,不再反代 Java) let urls = { - // 七牛相关API + upload: `/api/v1/upload`, + newPageUrl: `/api/v1/psychic/news/page`, + newDetailUrl: `/api/v1/psychic/news/get`, + loginUrl: `/api/v1/auth/wechat`, + getBanner: `/api/v1/psychic/banner/all`, + psychicSave: `/api/v1/psychic/save`, + getPsychicListTest: `/api/v1/psychic/study/list`, + getSingleDetail: `/api/v1/psychic/study/getSingleDetail`, + userChoiceSave: `/api/v1/psychic/user-choice/save`, + getResult: `/api/v1/psychic/user-choice/getResult`, + myTest: `/api/v1/psychic/user-choice/my-test`, + businessScopeList: `/api/v1/psychic/doctor-info/business-scope-list`, + getDoctorInfo: `/api/v1/psychic/doctor-info/get`, + doctorInfopage: `/api/v1/psychic/doctor-info/page`, + focus: `/api/v1/psychic/doctor-info/focus`, + cancelFocus: `/api/v1/psychic/doctor-info/cancel-focus`, + appointTotalList: `/api/v1/psychic/appointment/remain-list`, + dateDetailList: `/api/v1/psychic/appointment/date-detail-list`, + getShowInfo: `/api/v1/psychic/doctor-info/get-show-info`, + createOrder: `/api/v1/psychic/pay/createOrder`, + refund: `/api/v1/psychic/pay/refund`, - upload: `/admin-api/infra/file/upload`, - newPageUrl: `/app-api/psychic/news/page`, - newDetailUrl: `/app-api/psychic/news/get`, - loginUrl: `/app-api/oauth/app/login`, - getBanner: `/app-api/psychic/banner/all`, - psychicSave: `/app-api/psychic/save`, - getPsychicListTest: `/app-api/psychic/study/list`, - getSingleDetail: `/app-api/psychic/study/getSingleDetail`, - userChoiceSave: `/app-api/psychic/user-choice/save`, - getResult: `/app-api/psychic/user-choice/getResult`, - myTest: `/app-api/psychic/user-choice/my-test`, - businessScopeList: `/app-api/psychic/doctor-info/business-scope-list`, - getDoctorInfo: `/app-api/psychic/doctor-info/get`, - doctorInfopage: `/app-api/psychic/doctor-info/page`, - focus: `/app-api/psychic/doctor-info/focus`, - cancelFocus: `/app-api/psychic/doctor-info/cancel-focus`, - appointTotalList: `/app-api/psychic/appointment/remain-list`, - dateDetailList: `/app-api/psychic/appointment/date-detail-list`, - getShowInfo: `/app-api/psychic/doctor-info/get-show-info`, - createOrder: `/app-api/psychic/pay/createOrder`, - refund: `/app-api/psychic/pay/refund`, + getSelfInfo: `/api/v1/psychic/platform-user/getSelfInfo`, + updateUserInfo: `/api/v1/psychic/platform-user/update`, - getSelfInfo: `/app-api/psychic/platform-user/getSelfInfo`, - updateUserInfo: `/app-api/psychic/platform-user/update`, - - notReadNum: `/app-api/psychic/platform-user/not-read-num`, - focusToRead: `/app-api/psychic/platform-user/focus-to-read`, - orderToRead: `/app-api/psychic/platform-user/order-to-read`, - userFeedback: `/app-api/psychic/platform-user/user-feedback`, - feedbackFlag: `/app-api/psychic/platform-user/feedback-flag`, + notReadNum: `/api/v1/psychic/platform-user/not-read-num`, + focusToRead: `/api/v1/psychic/platform-user/focus-to-read`, + orderToRead: `/api/v1/psychic/platform-user/order-to-read`, + userFeedback: `/api/v1/psychic/platform-user/user-feedback`, + feedbackFlag: `/api/v1/psychic/platform-user/feedback-flag`, - focusAll: `/app-api/psychic/platform-user/focus-all`, - getByCode: `/app-api/psychic/procotol/getByCode`, + focusAll: `/api/v1/psychic/platform-user/focus-all`, + getByCode: `/api/v1/psychic/procotol/getByCode`, + createZxOrder: `/api/v1/psychic/order/create`, - // 订单 - createZxOrder: `/app-api/psychic/order/create`, - - orderList: `/app-api/psychic/order/order-list`, - orderDetail: `/app-api/psychic/order/order-detail`, - getPayParam: `/app-api/psychic/order/get-pay-param`, - cancelOrder: `/app-api/psychic/order/cancel`, - deleteOrder: `/app-api/psychic/order/delete-order`, + orderList: `/api/v1/psychic/order/order-list`, + orderDetail: `/api/v1/psychic/order/order-detail`, + getPayParam: `/api/v1/psychic/order/get-pay-param`, + cancelOrder: `/api/v1/psychic/order/cancel`, + deleteOrder: `/api/v1/psychic/order/delete-order`, } export { urls -} \ No newline at end of file +} diff --git a/util/requestConfig.js b/util/requestConfig.js index f9ff268..2f1a998 100644 --- a/util/requestConfig.js +++ b/util/requestConfig.js @@ -2,13 +2,10 @@ import { sysConsts } from '../common/sysConsts.js'; import request from "./request"; -// 全局配置的请求域名 -// const baseUrl = 'http://192.168.100.19:8922'; //线上正式环境888 -// const baseUrl = 'https://api-qa.scyuelai.com/'; //qa测试环境 -// const baseUrl = 'https://api-zsh-dev.scyuelai.com/' //dev开发环境 -// const baseUrl = 'https://local.scyuelai.com/'; //本地测试环境 - const baseUrl = "https://miniapp.yuxingu.com.cn" //https://miniapp.yuxingu.com.cn -// const baseUrl = "http://8.137.99.227:8922" +import { YXG_API_BASE } from './yxgConfig.js' + +const DEVICE_KEY = 'yxg_device_key' +const baseUrl = YXG_API_BASE //可以new多个request来支持多个域名请求 @@ -69,11 +66,15 @@ $http.requestStart = function(options) { } } //请求前加入token - let myToken = uni.getStorageSync('token') + let myToken = uni.getStorageSync('token') || uni.getStorageSync('yxg_token') if (myToken) { - myToken = myToken.replace(/\"/g, ""); + myToken = String(myToken).replace(/\"/g, ""); options.header['Authorization'] = 'Bearer ' + myToken; } + const device = uni.getStorageSync(DEVICE_KEY) + if (device) { + options.header['X-Device-Key'] = device + } return options; // return false 表示请求拦截,不会继续请求 } @@ -97,19 +98,27 @@ $http.dataFactory = async function(res) { // data: res.data, // method: res.method, // }); - if (res.response.statusCode && res.response.statusCode == 200) { - let httpData = res.response.data; - - if (typeof(httpData) == "string") { - httpData = JSON.parse(httpData); + const respHeader = res.response.header || res.response.headers || {} + const deviceOut = respHeader['X-Device-Key'] || respHeader['x-device-key'] + if (deviceOut) { + uni.setStorageSync(DEVICE_KEY, deviceOut) + } + let httpData = res.response.data + if (typeof(httpData) == "string") { + try { + httpData = JSON.parse(httpData) + } catch (e) { + httpData = null } + } + const errText = (httpData && (httpData.info || httpData.msg || httpData.message)) || '' + if (res.response.statusCode && res.response.statusCode == 200) { /*********以下只是模板(及共参考),需要开发者根据各自的接口返回类型修改*********/ - //判断数据是否请求成功 - if (httpData.success || httpData.code == 0) { + if (httpData && (httpData.success || httpData.code == 0)) { // 返回正确的结果(then接受数据) return Promise.resolve(httpData.data); - } else if (httpData.code == "401" || httpData.code == "1001" || httpData.code == 1100) { + } else if (httpData.code == "401" || httpData.code == "1001" || httpData.code == 1100 || httpData.code == 40100 || httpData.code == 40112) { // let content = '此时此刻需要您登录喔~'; // if (!uni.getStorageSync('loginPageAlive')) { // await gotoLogin().then(()=>{ @@ -121,13 +130,13 @@ $http.dataFactory = async function(res) { // 返回错误的结果(catch接受数据) return Promise.reject({ statusCode: 0, - errMsg: "【request】" + (httpData.info || httpData.msg) + errMsg: "【request】" + errText }); } else { //其他错误提示 if (res.isPrompt) { setTimeout(() => { uni.showToast({ - title: httpData.info || httpData.msg, + title: errText || '请求失败', icon: "none", duration: 3000 }); @@ -136,16 +145,25 @@ $http.dataFactory = async function(res) { // 返回错误的结果(catch接受数据) return Promise.reject({ statusCode: 0, - errMsg: "【request】" + (httpData.info || httpData.msg) + errMsg: "【request】" + errText }); } } else if (res.response.statusCode && res.response.statusCode == 401) { gotoLogin(res) } else { - // 返回错误的结果(catch接受数据) + const text = errText || ("HTTP " + res.response.statusCode) + if (res.isPrompt) { + setTimeout(() => { + uni.showToast({ + title: text, + icon: "none", + duration: 3000 + }); + }, 100) + } return Promise.reject({ - statusCode: res.response.statusCode, - errMsg: "【request】数据工厂验证不通过" + statusCode: 0, + errMsg: "【request】" + text }); } }; @@ -167,38 +185,17 @@ $http.requestError = function(e) { //token过期,退出登录 function gotoLogin(res) { uni.removeStorageSync("token") + uni.removeStorageSync("yxg_token") uni.removeStorageSync("userinfo") - console.log('res', res) - // uni.navigateTo({ - // "url": "/pages/login-pop/login-pop?type=1", - // "animationType": "pop-in" - // }) - // return new Promise((resolve, rejict)=>{ - - - - - // // uni.switchTab({ - // // "url":"/pages/login-pop/login-pop", - // // success:(res)=>{ - // // // uni.showToast({ - // // // title: "登录已失效,请重新登录", - // // // icon: "none" - // // // }); - // // uni.showModal({ - // // title: '提示', - // // content: '登录已失效,是否重新登录', - // // success: function (res) { - // // if (res.confirm) { - // // uni.navigateTo({ - // // "url":"/pageLogin/user-detail/user-detail" - // // }) - // // } - // // } - // // }); - // // } - // // }) - // resolve() - // }) + const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [] + const cur = pages.length ? pages[pages.length - 1] : null + const route = (cur && (cur.route || cur.__route__)) || '' + if (String(route).indexOf('login-pop') !== -1) { + return + } + uni.navigateTo({ + url: "/pages/login-pop/login-pop?type=2", + animationType: "pop-in" + }) } export default $http; \ No newline at end of file diff --git a/util/yxgApi.js b/util/yxgApi.js new file mode 100644 index 0000000..34e6001 --- /dev/null +++ b/util/yxgApi.js @@ -0,0 +1,163 @@ +import { YXG_API_BASE, YXG_ASSET_BASE } from './yxgConfig.js' +import $http from './requestConfig.js' + +const TOKEN_KEY = 'token' +const LEGACY_TOKEN_KEY = 'yxg_token' +const DEVICE_KEY = 'yxg_device_key' + +export function getYxgToken() { + return uni.getStorageSync(TOKEN_KEY) || uni.getStorageSync(LEGACY_TOKEN_KEY) || '' +} + +export function setYxgToken(token) { + if (token) { + uni.setStorageSync(TOKEN_KEY, token) + uni.removeStorageSync(LEGACY_TOKEN_KEY) + } else { + uni.removeStorageSync(TOKEN_KEY) + uni.removeStorageSync(LEGACY_TOKEN_KEY) + uni.removeStorageSync('userinfo') + } +} + +export function getDeviceKey() { + return uni.getStorageSync(DEVICE_KEY) || '' +} + +export function setDeviceKey(key) { + if (key) uni.setStorageSync(DEVICE_KEY, key) +} + +export function assetURL(path) { + const p = String(path || '').trim() + if (!p) return '' + if (/^https?:\/\//i.test(p)) return p + return YXG_ASSET_BASE + (p.startsWith('/') ? p : `/${p}`) +} + +function joinURL(base, path) { + if (/^https?:\/\//i.test(path)) return path + return `${base.replace(/\/$/, '')}${path.startsWith('/') ? path : `/${path}`}` +} + +function request(method, path, body, extra = {}) { + return $http.request({ + method, + url: path, + data: body === undefined ? {} : body, + isPrompt: extra.isPrompt ?? false, + load: extra.load ?? false, + timeout: extra.timeout, + }).catch((e) => { + const raw = e instanceof Error ? e.message : (e && e.errMsg) || '请求失败' + throw new Error(String(raw).replace(/^【request】/, '')) + }) +} + +export const yxgApi = { + authRegister: (body) => request('POST', '/api/v1/auth/register', body), + authLogin: (body) => request('POST', '/api/v1/auth/login', body), + authLogout: () => request('POST', '/api/v1/auth/logout', {}), + authMe: () => request('GET', '/api/v1/auth/me'), + authUpdateMe: (body) => request('PATCH', '/api/v1/auth/me', body), + authUploadAvatar(filePath) { + return new Promise((resolve, reject) => { + const header = {} + const token = getYxgToken() + const device = getDeviceKey() + if (token) header.Authorization = `Bearer ${token}` + if (device) header['X-Device-Key'] = device + uni.uploadFile({ + url: joinURL(YXG_API_BASE, '/api/v1/auth/me/avatar'), + filePath, + name: 'file', + header, + success(res) { + let data = res.data + if (typeof data === 'string') { + try { + data = JSON.parse(data) + } catch { + reject(new Error('上传失败')) + return + } + } + if (!data || data.code !== 0) { + reject(new Error(data?.message || '上传失败')) + return + } + resolve(data.data) + }, + fail(err) { + reject(new Error(err.errMsg || '上传失败')) + }, + }) + }) + }, + listProfiles: () => request('GET', '/api/v1/profiles'), + createProfile: (body) => request('POST', '/api/v1/profiles', body), + updateProfile: (id, body) => request('PATCH', `/api/v1/profiles/${id}`, body), + deleteProfile: (id) => request('DELETE', `/api/v1/profiles/${id}`), + createPortrait: (profile_id) => + request('POST', '/api/v1/reports/portrait', { profile_id }), + createStar: (profile_id) => request('POST', '/api/v1/reports/star', { profile_id }), + createSynastry: (profile_id_a, profile_id_b, as_of) => + request('POST', '/api/v1/reports/synastry', { + profile_id_a, + profile_id_b, + ...(as_of ? { as_of } : {}), + }), + getLatestReport: (profile_id, type, peer_profile_id) => { + let q = `profile_id=${encodeURIComponent(profile_id)}&type=${encodeURIComponent(type)}` + if (peer_profile_id) q += `&peer_profile_id=${encodeURIComponent(peer_profile_id)}` + return request('GET', `/api/v1/reports/latest?${q}`) + }, + listSynastryNearby: (lat, lng, radius_km = 50) => + request('GET', `/api/v1/synastry/nearby?lat=${lat}&lng=${lng}&radius_km=${radius_km}`), + createSynastryInvite: (profile_id) => + request('POST', '/api/v1/synastry/invites', { profile_id }), + getSynastryInvite: (token) => request('GET', `/api/v1/synastry/invites/${token}`), + acceptSynastryInvite: (token, body) => + request('POST', `/api/v1/synastry/invites/${token}/accept`, body), + createRhythm: (profile_id) => request('POST', '/api/v1/reports/rhythm', { profile_id }), + listImageCardScenes: () => request('GET', '/api/v1/image-cards/scenes'), + getImageCardQuota: () => request('GET', '/api/v1/image-cards/quota'), + drawImageCard: (body) => request('POST', '/api/v1/image-cards/draw', body), + getSolarTermsToday: () => request('GET', '/api/v1/solar-terms/today'), + saveMood: (body) => request('POST', '/api/v1/moods', body), + getMoodToday: () => request('GET', '/api/v1/moods/today'), + getMoodsRecent: () => request('GET', '/api/v1/moods/recent'), + getExploreCatalog: () => request('GET', '/api/v1/explore/catalog'), + getExploreCategory: (key) => request('GET', `/api/v1/explore/catalog/${key}`), + listGrowthPlans: () => request('GET', '/api/v1/growth/plans'), + createGrowthPlan: (body) => request('POST', '/api/v1/growth/plans', body), + createGrowthCheckin: (planId, body) => + request('POST', `/api/v1/growth/plans/${planId}/checkin`, body || {}), + listGrowthCheckins: (planId) => request('GET', `/api/v1/growth/plans/${planId}/checkins`), + listReports: () => request('GET', '/api/v1/reports'), + getReport: (id) => request('GET', `/api/v1/reports/${id}`), + getMembership: () => request('GET', '/api/v1/membership/me'), + createOrder: (body) => request('POST', '/api/v1/orders', body), + payMock: (orderId) => request('POST', `/api/v1/orders/${orderId}/pay-mock`, {}), + createRelationInsight: (profile_a_id, profile_b_id) => + request('POST', '/api/v1/relation/insight', { profile_a_id, profile_b_id }), + listScales: () => request('GET', '/api/v1/scales'), + getScaleBankCatalog: () => request('GET', '/api/v1/scale-bank/catalog'), + getScaleBankCategory: (key) => request('GET', `/api/v1/scale-bank/categories/${key}`), + getScale: (slug) => request('GET', `/api/v1/scales/${slug}`), + getScaleResult: (slug) => request('GET', `/api/v1/scales/${slug}/result`), + submitScale: (slug, profile_id, answers) => + request('POST', `/api/v1/scales/${slug}/result`, { profile_id, answers }), + getAskQuota: () => request('GET', '/api/v1/ask/quota'), + createAskThread: (body) => request('POST', '/api/v1/ask/threads', body), + clearAskThread: (threadId) => request('DELETE', `/api/v1/ask/threads/${threadId}`), + listAskMessages: (threadId) => request('GET', `/api/v1/ask/threads/${threadId}/messages`), + sendAskMessage: (threadId, content) => + request('POST', `/api/v1/ask/threads/${threadId}/messages`, { content }, { timeout: 60000 }), + getHomeTools: () => request('GET', '/api/v1/home/tools'), + getHomeBanners: (placement = 'home') => + request('GET', `/api/v1/home/banners?placement=${encodeURIComponent(placement)}`), + getHomeFeedSlots: (placement = 'home') => + request('GET', `/api/v1/home/feed-slots?placement=${encodeURIComponent(placement)}`), + getHomeDailyTips: () => request('GET', '/api/v1/home/daily-tips'), +} diff --git a/util/yxgAuth.js b/util/yxgAuth.js new file mode 100644 index 0000000..2826995 --- /dev/null +++ b/util/yxgAuth.js @@ -0,0 +1,126 @@ +import { yxgApi, getYxgToken, setYxgToken } from './yxgApi.js' + +let cachedNick = '' + +export function setAccountNickname(v) { + cachedNick = String(v || '').trim() +} + +export function selfDisplayName(fallback = '我') { + return cachedNick || fallback +} + +export async function loadAccountNickname(force = false) { + if (cachedNick && !force) return cachedNick + try { + const me = await yxgApi.authMe() + cachedNick = (me.nickname || '').trim() + return cachedNick + } catch { + return cachedNick + } +} + +export function openWechatLogin() { + const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [] + const cur = pages.length ? pages[pages.length - 1] : null + const route = (cur && (cur.route || cur.__route__)) || '' + if (String(route).indexOf('login-pop') !== -1) return + uni.navigateTo({ url: '/pages/login-pop/login-pop?type=2' }) +} + +export async function ensureAccount(redirect) { + if (!getYxgToken()) { + openWechatLogin() + return null + } + try { + return await yxgApi.authMe() + } catch { + setYxgToken('') + openWechatLogin() + return null + } +} + +export async function findSelfProfile() { + const { items } = await yxgApi.listProfiles() + return (items || []).find((p) => p.relation === 'self') || null +} + +export async function ensureSelfProfile(input) { + await loadAccountNickname() + const nick = selfDisplayName('我') + const nameIn = (input.display_name || '').trim() + const preferred = !nameIn || nameIn === '我' ? nick : nameIn + const existing = await findSelfProfile() + if (existing) { + const keep = existing.display_name?.trim() + const display = keep && keep !== '我' ? keep : preferred + return yxgApi.updateProfile(existing.id, { + display_name: display, + birth_date: input.birth_date, + birth_time: input.birth_time, + birth_place: input.birth_place, + }) + } + try { + return await yxgApi.createProfile({ + relation: 'self', + birth_date: input.birth_date, + display_name: preferred, + birth_time: input.birth_time, + birth_place: input.birth_place, + }) + } catch (e) { + const msg = e instanceof Error ? e.message : '' + if (!/already exists|已有|409|self profile/.test(msg)) throw e + const again = await findSelfProfile() + if (!again) throw e + const keep = again.display_name?.trim() + return yxgApi.updateProfile(again.id, { + display_name: keep && keep !== '我' ? keep : preferred, + birth_date: input.birth_date, + birth_time: input.birth_time, + birth_place: input.birth_place, + }) + } +} + +export async function ensureOtherProfile(input) { + const name = (input.display_name || 'TA').trim() || 'TA' + const birth = String(input.birth_date || '').slice(0, 10) + const { items } = await yxgApi.listProfiles() + const hit = (items || []).find( + (p) => + p.relation === 'other' && + String(p.birth_date).slice(0, 10) === birth && + (p.display_name || 'TA') === name, + ) + if (hit) return hit + return yxgApi.createProfile({ + relation: 'other', + birth_date: birth, + display_name: name, + relation_type: input.relation_type, + birth_place: input.birth_place, + }) +} + +export function pickableArchiveList(items) { + const list = items || [] + const self = list.find((p) => p.relation === 'self') + const others = list.filter((p) => p.relation === 'other') + return self ? [self, ...others] : others +} + +export async function loadSelfLatest(type) { + const profile = await findSelfProfile() + if (!profile) return { profile: null, report: null } + try { + const report = await yxgApi.getLatestReport(profile.id, type) + return { profile, report } + } catch { + return { profile, report: null } + } +} diff --git a/util/yxgCatalog.js b/util/yxgCatalog.js new file mode 100644 index 0000000..8c0431b --- /dev/null +++ b/util/yxgCatalog.js @@ -0,0 +1,139 @@ +export const ICON_NAMES = [ + 'mbti', 'star', 'portrait', 'rhythm', 'synastry', 'astro', + 'companion', 'ask', 'cards', 'reports', 'growth', 'relation', + 'nine', 'eq', 'stress', 'sleep', +] + +export function resolveIcon(raw, fallback = 'mbti') { + return ICON_NAMES.includes(raw) ? raw : fallback +} + +export function toolIconFor(pathOrKey) { + const s = String(pathOrKey || '').toLowerCase() + if (s.includes('mbti') || s.includes('人格类型') || s.includes('tests')) return 'mbti' + if (s.includes('synastry') || s.includes('合盘')) return 'synastry' + if (s.includes('portrait') || s.includes('decode') || s.includes('解码')) return 'portrait' + if (s.includes('star') || s.includes('星座') || s.includes('星盘')) return 'star' + if (s.includes('rhythm') || s.includes('节律')) return 'rhythm' + if (s.includes('companion') || s.includes('节气') || s.includes('mood')) return 'companion' + if (s.includes('ask') || s.includes('问答')) return 'ask' + if (s.includes('card') || s.includes('意象')) return 'cards' + if (s.includes('report') || s.includes('报告')) return 'reports' + if (s.includes('growth') || s.includes('计划') || s.includes('会员') || s.includes('member')) return 'growth' + if (s.includes('relation') || s.includes('匹配') || s.includes('love')) return 'relation' + if (s.includes('astro') || s.includes('星象')) return 'astro' + if (s.includes('explore') || s.includes('探索') || s.includes('分类')) return 'mbti' + return 'portrait' +} + +export function typeLabel(type) { + const map = { + portrait: '愈心解码', + star: '星座排盘', + rhythm: '身心节律', + synastry: '合盘', + relation: '人格匹配', + scale: '测评', + } + return map[type] || '成长报告' +} + +export function formatDate(iso) { + if (!iso) return '' + const d = new Date(iso) + if (Number.isNaN(d.getTime())) return String(iso).slice(0, 10) + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` +} + +export function headlineOf(report) { + const s = report?.summary || {} + return s.headline || s.one_liner || s.label || s.title || typeLabel(report?.type) +} + +export const homeGridRow1 = [ + { to: '/scales/mbti-lite', icon: 'mbti', label: 'MBTI测试' }, + { to: '/star', icon: 'star', label: '星座' }, + { to: '/portrait', icon: 'portrait', label: '愈心解码', badge: '热', badgeTone: 'hot' }, + { to: '/rhythm', icon: 'rhythm', label: '身心节律' }, + { to: '/synastry', icon: 'synastry', label: '合盘', badge: '新', badgeTone: 'new' }, + { to: '/star', icon: 'astro', label: '星象性格' }, +] + +export const homeGridRow2 = [ + { to: '/companion', icon: 'companion', label: '节气陪伴' }, + { to: '/ask', icon: 'ask', label: 'AI问答' }, + { to: '/cards', icon: 'cards', label: '意象卡片' }, + { to: '/reports', icon: 'reports', label: '成长报告', badge: '新', badgeTone: 'new' }, + { to: '/growth-plan', icon: 'growth', label: '成长计划' }, + { to: '/relation', icon: 'relation', label: '人格匹配' }, +] + +export const homeFeeds = [ + { to: '/portrait', icon: 'portrait', title: '愈心解码', meta: '一个生日,读懂性格与身心节奏', stat: '核心入口', tone: 'fc-e', tag: '热' }, + { to: '/ask', icon: 'ask', title: 'AI 成长助手', meta: '结合档案聊聊卡住的事', stat: '随时可问', tone: 'fc-a', tag: 'AI' }, + { to: '/star', icon: 'star', title: '星座排盘', meta: '本命盘 · 相位 · 日周月运势', stat: '本周热门', tone: 'fc-b', tag: '新' }, + { to: '/synastry', icon: 'synastry', title: '合盘', meta: '恋爱 / 友情 / 婚姻指数', stat: '了解彼此', tone: 'fc-c' }, + { to: '/scales/mbti-lite', icon: 'mbti', title: 'MBTI测试', meta: '四维偏好探索,看见自己的能量与决策节奏', stat: '热门测评', tone: 'fc-a' }, + { to: '/membership', icon: 'growth', title: '成长会员', meta: '深度报告与全年节气陪伴', stat: '解锁更多', tone: 'fc-e' }, +] + +export const mineGroupArchive = [ + { to: '/profile', label: '个人档案', icon: 'portrait' }, + { to: '/reports', label: '我的成长报告', icon: 'reports' }, + { to: '/portrait', label: '愈心解码', icon: 'portrait' }, + { to: '/star', label: '星象性格', icon: 'star' }, + { to: '/rhythm', label: '身心节律', icon: 'rhythm' }, + { to: '/cards', label: '意象卡片', icon: 'cards' }, +] + +export const mineGroupGrowth = [ + { to: '/relation', label: '人格匹配', icon: 'relation' }, + { to: '/ask', label: 'AI 成长助手', icon: 'ask' }, + { to: '/explore', label: '探索测试', icon: 'mbti' }, + { to: '/growth-plan', label: '成长计划', icon: 'growth' }, + { to: '/membership', label: '成长会员', icon: 'growth' }, +] + +const FEED_TONES = ['fc-e', 'fc-a', 'fc-b', 'fc-c', 'fc-a', 'fc-e'] + +export function mapHomeBannersToFeeds(items) { + const sorted = [...(items || [])].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) + const out = [] + for (let i = 0; i < sorted.length; i++) { + const it = sorted[i] + const to = String(it.link_path || '').trim() + if (!to.startsWith('/') || to.includes('://')) continue + out.push({ + to, + icon: 'portrait', + title: it.title, + meta: '运营推荐', + stat: '推荐', + tone: FEED_TONES[i % FEED_TONES.length], + tag: '荐', + }) + } + return out +} + +export function mapHomeTools(items) { + const toTool = (it) => { + if (!ICON_NAMES.includes(it.icon)) return null + const t = { to: it.path, icon: it.icon, label: it.label } + if (it.badge) t.badge = it.badge + if (it.badge_tone === 'hot' || it.badge_tone === 'new') t.badgeTone = it.badge_tone + return t + } + const sorted = [...(items || [])].sort( + (a, b) => a.row_index - b.row_index || a.sort_order - b.sort_order, + ) + const row1 = [] + const row2 = [] + for (const it of sorted) { + const t = toTool(it) + if (!t) continue + if (it.row_index === 1) row1.push(t) + else if (it.row_index === 2) row2.push(t) + } + return { row1, row2 } +} diff --git a/util/yxgConfig.js b/util/yxgConfig.js index 47631ed..e1b46ca 100644 --- a/util/yxgConfig.js +++ b/util/yxgConfig.js @@ -1,16 +1,11 @@ -/** 愈心魔方 H5(digital-psychology user-h5) */ +/** 愈心魔方 + 咨询统一后台(Go)。本地请指向 Go API(:8080),不要指向 Vite。 */ import { LOCAL_H5_HOST, USE_LOCAL_H5 } from './yxgConfig.local.js' -const PROD_BASE = 'https://h5.yuxingu.com.cn/psy/' -const PROD_VERSION = '202608283' -/** 愈心魔方顶栏 logo(小程序包内 static,不请求外网) */ -export const YXG_MP_LOGO_URL = '/static/n-main/yxg-brand-logo.png' - +const PROD_API = 'https://h5.yuxingu.com.cn/psy' const isDev = import.meta.env.DEV -const useLocalH5 = isDev && USE_LOCAL_H5 +const useLocal = isDev && USE_LOCAL_H5 -export const YXG_H5_VERSION = useLocalH5 ? 'local' : PROD_VERSION -export const YXG_H5_BASE = useLocalH5 ? `http://${LOCAL_H5_HOST}/psy/` : PROD_BASE -/** H5 内嵌场景远程 logo(历史 cover 方案遗留,小程序顶栏用 YXG_MP_LOGO_URL) */ -export const YXG_H5_LOGO_URL = 'https://miniapp.yuxingu.com.cn/yxg-mp/logo.png' -export const YXG_H5_HOME_URL = `${YXG_H5_BASE}?mp=1&v=${YXG_H5_VERSION}` +export const YXG_API_BASE = useLocal ? `http://${LOCAL_H5_HOST}` : PROD_API +export const YXG_ASSET_BASE = useLocal ? `http://${LOCAL_H5_HOST}/psy` : PROD_API +export const YXG_MP_LOGO_URL = '/static/n-main/yxg-brand-logo.png' +export const YXG_DEFAULT_AVATAR = '/static/n-main/yxg-brand-logo.png' diff --git a/util/yxgConfig.local.js b/util/yxgConfig.local.js index 229e2f0..054c47f 100644 --- a/util/yxgConfig.local.js +++ b/util/yxgConfig.local.js @@ -1,7 +1,7 @@ /** - * 本地小程序联调 H5 - * USE_LOCAL_H5=false → 开发编译也访问远程 https://h5.yuxingu.com.cn/psy/(默认) - * USE_LOCAL_H5=true → 访问本机 Vite,LOCAL_H5_HOST 与 npm run dev:mp 打印一致 + * 本地小程序联调统一 Go API + * USE_LOCAL_H5=false → https://h5.yuxingu.com.cn/psy(现网还没有 /api/v1/psychic,会 404) + * USE_LOCAL_H5=true → 本机 Go,LOCAL_H5_HOST 写成 host:8080(不要写 Vite :5173) */ export const USE_LOCAL_H5 = false -export const LOCAL_H5_HOST = '192.168.100.16:5173' +export const LOCAL_H5_HOST = '127.0.0.1:8080' diff --git a/util/yxgNav.js b/util/yxgNav.js new file mode 100644 index 0000000..7c1dfc7 --- /dev/null +++ b/util/yxgNav.js @@ -0,0 +1,75 @@ +const HOME = '/pages/yxg-magic/index' + +const MAP = [ + [/^\/explore\/bank\/([^/?#]+)/, (_, c) => `/pages/yxg/explore-bank?category=${encodeURIComponent(c)}`], + [/^\/explore\/([^/?#]+)/, (_, c) => `/pages/yxg/explore-category?category=${encodeURIComponent(c)}`], + [/^\/explore\/?$/, '/pages/yxg/explore'], + [/^\/growth-plan\/?$/, '/pages/yxg/growth-plan'], + [/^\/ask\/?$/, '/pages/yxg/ask'], + [/^\/companion\/?$/, '/pages/yxg/companion'], + [/^\/mine\/?$/, '/pages/yxg/mine'], + [/^\/login\/?$/, '/pages/login-pop/login-pop?type=2'], + [/^\/profile\/?$/, '/pages/yxg/profile'], + [/^\/portrait\/?$/, '/pages/yxg/portrait'], + [/^\/star\/?$/, '/pages/yxg/star'], + [/^\/synastry\/invite\/([^/?#]+)/, (_, t) => `/pages/yxg/synastry-invite?token=${encodeURIComponent(t)}`], + [/^\/synastry\/?$/, '/pages/yxg/synastry'], + [/^\/rhythm\/?$/, '/pages/yxg/rhythm'], + [/^\/cards\/?$/, '/pages/yxg/cards'], + [/^\/relation\/?$/, '/pages/yxg/relation'], + [/^\/membership\/?$/, '/pages/yxg/membership'], + [/^\/scales\/([^/?#]+)/, (_, s) => `/pages/yxg/scale?slug=${encodeURIComponent(s)}`], + [/^\/share\/?$/, '/pages/yxg/share'], + [/^\/reports\/([^/?#]+)/, (_, id) => `/pages/yxg/report?id=${encodeURIComponent(id)}`], + [/^\/reports\/?$/, '/pages/yxg/reports'], + [/^\/decode\/?$/, '/pages/yxg/portrait'], + [/^\/?$/, HOME], +] + +function withQuery(url, query) { + if (!query) return url + const pairs = Object.keys(query) + .filter((k) => query[k] !== undefined && query[k] !== null && query[k] !== '') + .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(query[k])}`) + if (!pairs.length) return url + return url + (url.includes('?') ? '&' : '?') + pairs.join('&') +} + +export function h5ToMp(path, query) { + const raw = String(path || '/') + const pathname = raw.split('?')[0] + for (const [re, dest] of MAP) { + const m = pathname.match(re) + if (!m) continue + const url = typeof dest === 'function' ? dest(...m) : dest + return withQuery(url, query) + } + return HOME +} + +export function yxgGo(path, query) { + const url = h5ToMp(path, query) + if (url === HOME || url.startsWith(`${HOME}?`)) { + uni.switchTab({ url: HOME }) + return + } + uni.navigateTo({ url }) +} + +export function yxgReplace(path, query) { + const url = h5ToMp(path, query) + if (url === HOME || url.startsWith(`${HOME}?`)) { + uni.switchTab({ url: HOME }) + return + } + uni.redirectTo({ url }) +} + +export function yxgBack() { + const pages = getCurrentPages() + if (pages.length > 1) { + uni.navigateBack() + return + } + uni.switchTab({ url: HOME }) +} diff --git a/util/yxgTips.js b/util/yxgTips.js new file mode 100644 index 0000000..c7367cd --- /dev/null +++ b/util/yxgTips.js @@ -0,0 +1,78 @@ +export function localDayKey(d = new Date()) { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` +} + +export function localShichenIndex(d = new Date()) { + return ((d.getHours() + 1) % 24) >> 1 +} + +function nextShichenEnd(d = new Date()) { + const shi = localShichenIndex(d) + const startHour = (shi * 2 + 23) % 24 + const start = new Date(d) + start.setHours(startHour, 0, 0, 0) + if (start.getTime() > d.getTime()) start.setDate(start.getDate() - 1) + return new Date(start.getTime() + 2 * 3600 * 1000) +} + +export function localFallbackTips() { + const n = new Date() + const day = Math.floor(Date.UTC(n.getFullYear(), n.getMonth(), n.getDate()) / 86400000) + const shi = localShichenIndex(n) + const seed = day * 13 + shi * 19 + const palettes = [ + [{ name: '米白', hex: '#F5F0E8' }, { name: '雾霾蓝', hex: '#A8C4D8' }], + [{ name: '燕麦色', hex: '#E8DCC8' }, { name: '浅杏', hex: '#F0C9A8' }], + [{ name: '浅灰', hex: '#D8D6D4' }, { name: '雾粉', hex: '#E8B8C4' }], + [{ name: '天青', hex: '#9BB8D4' }, { name: '陶土', hex: '#C4A484' }], + ] + const clothing = [ + '轻薄透气更舒服,外套可备一件薄衫应付温差。', + '今日宜层搭:薄内搭+透气外衫,方便随时增减。', + '选柔软面料贴身,活动一整天也不易紧绷。', + '宽松剪裁更自在,适合慢节奏出门与散步。', + ] + const notes = ['清爽干净,不抢戏。', '今日偏清透,层次更轻。', '温柔耐看,适合日常。', '柔和提气色,不显沉。'] + const well = [ + '午后泡一杯温茶,给身心一点缓冲。', + '今日做三次深呼吸,拉长呼气更易放松。', + '今晚早点放下屏幕,让眼睛歇一会儿。', + '走路时把肩膀放松,呼吸会顺很多。', + ] + const i = seed % palettes.length + return { + clothingIndex: 55 + (seed % 41), + clothing: clothing[i], + colorNote: notes[i], + palette: palettes[i], + wellness: well[i], + source: 'fallback', + needBirth: true, + asOf: localDayKey(n), + shichen: shi, + validUntil: nextShichenEnd(n).toISOString(), + } +} + +export function tipsFromApi(t) { + return { + clothingIndex: t.clothing_index, + clothing: t.clothing, + colorNote: t.color_note, + palette: (t.palette || []).map((p) => ({ name: p.name, hex: p.hex })), + wellness: t.wellness, + needBirth: !!t.need_birth, + guaName: t.gua_name, + source: t.source, + asOf: t.as_of || localDayKey(), + shichen: t.shichen, + shichenName: t.shichen_name, + validUntil: t.valid_until, + } +} + +export function ringOffset(index) { + const c = 2 * Math.PI * 30 + const pct = Math.min(100, Math.max(0, Number(index) || 0)) / 100 + return c * (1 - pct) +}