feat: 小程序统一走 Go 后台,咨询与登录切到 /api/v1
去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+38
-42
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+52
-55
@@ -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;
|
||||
+163
@@ -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'),
|
||||
}
|
||||
+126
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
+7
-12
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user