feat: 小程序统一走 Go 后台,咨询与登录切到 /api/v1

去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-09-15 00:26:28 +08:00
co-authored by Cursor
parent 6e97f01378
commit b09d82df8d
40 changed files with 3852 additions and 285 deletions
+38 -42
View File
@@ -1,52 +1,48 @@
//统一设置API接口地址
// 统一打 Go /api/v1ECR-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
View File
@@ -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
View File
@@ -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
View File
@@ -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 }
}
}
+139
View File
@@ -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
View File
@@ -1,16 +1,11 @@
/** 愈心魔方 H5digital-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'
+4 -4
View File
@@ -1,7 +1,7 @@
/**
* 本地小程序联调 H5
* USE_LOCAL_H5=false → 开发编译也访问远程 https://h5.yuxingu.com.cn/psy/(默认
* USE_LOCAL_H5=true → 访问本机 ViteLOCAL_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 → 本机 GoLOCAL_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'
+75
View File
@@ -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 })
}
+78
View File
@@ -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)
}