refactor(ECR-005): 再拆 Ask/Decode/Portrait 与 composable,补强 OpenAPI
压低贴线 SFC/composable;报告/档案/量表/意象卡 path 挂 Envelope $ref;CI ESS 门禁跟到 ECR-005。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import type { GrowthReport } from '@yuxingu/types'
|
||||
import type { WheelAspect, WheelPlanet } from '../components/NatalWheel.vue'
|
||||
import type { SignCard } from '../components/SignCards.vue'
|
||||
import type { PortraitSharePayload, RelationSharePayload, SharePayload } from './shareLink'
|
||||
import { asPlanets, ascLonFromChart, chartBlock, overlayEntriesFrom } from './synastryChart'
|
||||
|
||||
export const reportStarPanels = [
|
||||
{ key: 'chart', label: '星盘' },
|
||||
{ key: 'fortune', label: '运势' },
|
||||
{ key: 'planets', label: '行星' },
|
||||
]
|
||||
|
||||
export const reportSynTabs = [
|
||||
{ key: 'compare', label: '比较' },
|
||||
{ key: 'composite', label: '组合' },
|
||||
{ key: 'davison', label: '时空' },
|
||||
{ key: 'marks_me', label: '马克斯' },
|
||||
{ key: 'overlay', label: '配对' },
|
||||
{ key: 'composite_progressed', label: '组合次限' },
|
||||
]
|
||||
|
||||
export const reportFortuneKeys = ['daily', 'weekly', 'monthly', 'yearly', 'lifetime'] as const
|
||||
|
||||
export type FortunePeriod = {
|
||||
title?: string
|
||||
label?: string
|
||||
score?: number
|
||||
tip?: string
|
||||
focus?: string
|
||||
boost?: string
|
||||
caution?: string
|
||||
dims?: { love?: number; career?: number; money?: number; mood?: number }
|
||||
}
|
||||
|
||||
export type PlanetRow = { key: string; title: string; sign: string; degree: string; house: number }
|
||||
|
||||
export type TransitRow = { key: string; title: string; aspect: string; tip: string }
|
||||
|
||||
export function reportTypeLabel(type?: string) {
|
||||
const map: Record<string, string> = {
|
||||
portrait: '愈心解码',
|
||||
relation: '人格匹配',
|
||||
synastry: '合盘',
|
||||
star: '星座',
|
||||
rhythm: '身心节律',
|
||||
image_card: '意象卡片',
|
||||
}
|
||||
return map[type || ''] || '成长报告'
|
||||
}
|
||||
|
||||
export function relatedLinkForType(t?: string) {
|
||||
if (t === 'star') return { to: '/star', label: '星座' }
|
||||
if (t === 'synastry') return { to: '/synastry', label: '合盘' }
|
||||
if (t === 'portrait') return { to: '/portrait', label: '解码' }
|
||||
if (t === 'relation') return { to: '/relation', label: '匹配' }
|
||||
if (t === 'rhythm') return { to: '/rhythm', label: '节律' }
|
||||
return { to: '/reports', label: '全部' }
|
||||
}
|
||||
|
||||
export function summaryKeywords(summary: Record<string, unknown>): string[] {
|
||||
if (Array.isArray(summary.keywords)) return summary.keywords as string[]
|
||||
const a = Array.isArray(summary.me_keywords) ? (summary.me_keywords as string[]) : []
|
||||
const b = Array.isArray(summary.other_keywords) ? (summary.other_keywords as string[]) : []
|
||||
return [...a, ...b]
|
||||
}
|
||||
|
||||
export function chartObjFrom(summary: Record<string, unknown>): Record<string, unknown> {
|
||||
return summary.chart && typeof summary.chart === 'object'
|
||||
? (summary.chart as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
export function mapAspectPreview(raw: unknown): WheelAspect[] {
|
||||
if (!Array.isArray(raw)) return []
|
||||
return raw.map((a) => {
|
||||
const o = a as Record<string, unknown>
|
||||
return { a: String(o.a), b: String(o.b), type: String(o.type), orb: Number(o.orb) }
|
||||
})
|
||||
}
|
||||
|
||||
export function mapTransits(summary: Record<string, unknown>): TransitRow[] {
|
||||
const raw = summary.transits
|
||||
if (Array.isArray(raw)) return raw as TransitRow[]
|
||||
const o = summary.outlook
|
||||
if (o && typeof o === 'object' && Array.isArray((o as { transits?: unknown }).transits)) {
|
||||
return (o as { transits: TransitRow[] }).transits
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function synChartsFrom(summary: Record<string, unknown>): Record<string, unknown> {
|
||||
return summary.charts && typeof summary.charts === 'object'
|
||||
? (summary.charts as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
export function synActiveBlockFrom(
|
||||
charts: Record<string, unknown>,
|
||||
panelKey: string,
|
||||
): Record<string, unknown> | null {
|
||||
return chartBlock(charts, panelKey)
|
||||
}
|
||||
|
||||
export function synChartPair(
|
||||
summary: Record<string, unknown>,
|
||||
key: 'chart_a' | 'chart_b',
|
||||
): { planets: WheelPlanet[]; asc: number | null } {
|
||||
const c = summary[key]
|
||||
if (!c || typeof c !== 'object') return { planets: [], asc: null }
|
||||
const o = c as { planets?: unknown; asc_lon?: number }
|
||||
return { planets: asPlanets(o.planets), asc: typeof o.asc_lon === 'number' ? o.asc_lon : null }
|
||||
}
|
||||
|
||||
export function synOverlayFrom(charts: Record<string, unknown>) {
|
||||
return overlayEntriesFrom(charts).map((e) => ({
|
||||
planet: e.planet,
|
||||
house: e.house,
|
||||
house_tip: e.house_tip,
|
||||
}))
|
||||
}
|
||||
|
||||
export function formatCreatedAt(t?: string) {
|
||||
if (!t) return ''
|
||||
try {
|
||||
return new Date(t).toLocaleDateString('zh-CN')
|
||||
} catch {
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
export function signCardsFrom(summary: Record<string, unknown>): SignCard[] {
|
||||
const raw = summary.sign_cards
|
||||
return Array.isArray(raw) ? (raw as SignCard[]) : []
|
||||
}
|
||||
|
||||
export function fortuneBundleFrom(summary: Record<string, unknown>): Record<string, FortunePeriod> {
|
||||
const o = summary.outlook
|
||||
return o && typeof o === 'object' ? (o as Record<string, FortunePeriod>) : {}
|
||||
}
|
||||
|
||||
export function planetsFrom(summary: Record<string, unknown>): PlanetRow[] {
|
||||
const raw = summary.planets
|
||||
return Array.isArray(raw) ? (raw as PlanetRow[]) : []
|
||||
}
|
||||
|
||||
export function wheelPlanetsFrom(summary: Record<string, unknown>): WheelPlanet[] {
|
||||
return asPlanets(summary.planets)
|
||||
}
|
||||
|
||||
export function ascLonFromSummary(summary: Record<string, unknown>): number | null {
|
||||
return ascLonFromChart(chartObjFrom(summary))
|
||||
}
|
||||
|
||||
export function numOrNull(v: unknown): number | null {
|
||||
return typeof v === 'number' ? v : null
|
||||
}
|
||||
|
||||
export function buildReportSharePayload(
|
||||
report: GrowthReport | null,
|
||||
summary: Record<string, unknown>,
|
||||
headline: string,
|
||||
oneLiner: string,
|
||||
keywords: string[],
|
||||
): SharePayload | null {
|
||||
if (!report) return null
|
||||
if (report.type === 'relation') {
|
||||
return {
|
||||
type: 'relation',
|
||||
me: String(summary.me_style || ''),
|
||||
other: String(summary.other_style || ''),
|
||||
diff: String(summary.diff_one_liner || ''),
|
||||
keywords,
|
||||
} as RelationSharePayload
|
||||
}
|
||||
return {
|
||||
type: 'portrait',
|
||||
title: headline,
|
||||
line: oneLiner,
|
||||
keywords,
|
||||
} as PortraitSharePayload
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { WheelAspect, WheelPlanet } from '../components/NatalWheel.vue'
|
||||
import type { SignCard } from '../components/SignCards.vue'
|
||||
import type { FortunePeriod } from './reportSummary'
|
||||
import { chartObjFrom, fortuneBundleFrom, signCardsFrom } from './reportSummary'
|
||||
|
||||
export type { FortunePeriod }
|
||||
export { chartObjFrom, fortuneBundleFrom, signCardsFrom }
|
||||
|
||||
export const SETTINGS_KEY = 'yuxingu_star_wheel_settings'
|
||||
|
||||
export const starViewTabs = [
|
||||
{ key: 'overview' as const, label: '概览' },
|
||||
{ key: 'chart' as const, label: '星盘' },
|
||||
{ key: 'fortune' as const, label: '运势' },
|
||||
{ key: 'planets' as const, label: '行星' },
|
||||
]
|
||||
|
||||
export const starFortuneKeys = ['daily', 'weekly', 'monthly', 'yearly', 'lifetime'] as const
|
||||
|
||||
export type PlanetRow = WheelPlanet & { element?: string; modality?: string }
|
||||
export type AspectRow = WheelAspect & { a_title?: string; b_title?: string; label?: string }
|
||||
export type HouseRow = { num: number; sign: string }
|
||||
|
||||
export type GridCell = { key: string; nick: string; role: string; center?: boolean }
|
||||
|
||||
export function fortuneLabel(k: string) {
|
||||
return (
|
||||
({ daily: '今日', weekly: '本周', monthly: '本月', yearly: '今年', lifetime: '一生' } as Record<
|
||||
string,
|
||||
string
|
||||
>)[k] || k
|
||||
)
|
||||
}
|
||||
|
||||
export function housesFromChart(chart: Record<string, unknown>): HouseRow[] {
|
||||
return Array.isArray(chart.houses) ? (chart.houses as HouseRow[]) : []
|
||||
}
|
||||
|
||||
export function planetsAsRows(summary: Record<string, unknown>): PlanetRow[] {
|
||||
const raw = summary.planets
|
||||
return Array.isArray(raw) ? (raw as PlanetRow[]) : []
|
||||
}
|
||||
|
||||
export function toWheelPlanets(planets: PlanetRow[]): WheelPlanet[] {
|
||||
return planets.map((p) => ({
|
||||
key: p.key,
|
||||
title: p.title,
|
||||
sign: p.sign,
|
||||
degree: p.degree,
|
||||
house: p.house,
|
||||
lon: Number(p.lon),
|
||||
element: p.element,
|
||||
modality: p.modality,
|
||||
}))
|
||||
}
|
||||
|
||||
export function aspectsFrom(raw: unknown): AspectRow[] {
|
||||
return Array.isArray(raw) ? (raw as AspectRow[]) : []
|
||||
}
|
||||
|
||||
export function aspectLinesFrom(
|
||||
hasDeep: boolean | undefined,
|
||||
full: AspectRow[],
|
||||
preview: AspectRow[],
|
||||
): WheelAspect[] {
|
||||
const src = hasDeep ? full : preview
|
||||
return src.map((a) => ({ a: a.a, b: a.b, type: a.type, orb: a.orb, label: a.label }))
|
||||
}
|
||||
|
||||
export function buildGridCells(cards: SignCard[], planets: PlanetRow[]): GridCell[] {
|
||||
const slots: GridCell[] = []
|
||||
const order = ['sun', 'moon', 'rise', 'venus', 'mars', 'mercury', 'north_node', 'juno', 'jupiter']
|
||||
const byKey = new Map(cards.map((c) => [c.key, c]))
|
||||
const planetByKey = new Map(planets.map((p) => [p.key, p]))
|
||||
for (let i = 0; i < 9; i++) {
|
||||
if (i === 4) {
|
||||
slots.push({ key: '_center', nick: '', role: '', center: true })
|
||||
continue
|
||||
}
|
||||
const key = order[i < 4 ? i : i - 1] || cards[i]?.key || `p${i}`
|
||||
const c = byKey.get(key)
|
||||
const p = planetByKey.get(key)
|
||||
if (c) {
|
||||
slots.push({
|
||||
key: c.key,
|
||||
nick: (c.teaser || c.label || c.title).slice(0, 6),
|
||||
role: `${c.title}${c.label}`,
|
||||
})
|
||||
} else if (p) {
|
||||
slots.push({ key: p.key, nick: p.sign, role: `${p.title}${p.sign}` })
|
||||
} else if (cards[i < 4 ? i : i - 1]) {
|
||||
const x = cards[i < 4 ? i : i - 1]
|
||||
slots.push({ key: x.key, nick: (x.teaser || x.label).slice(0, 6), role: `${x.title}${x.label}` })
|
||||
} else {
|
||||
slots.push({ key: `empty${i}`, nick: '—', role: '待生成' })
|
||||
}
|
||||
}
|
||||
return slots
|
||||
}
|
||||
|
||||
export function loadWheelSettings(): { showOuter: boolean; tightOrb: boolean } {
|
||||
try {
|
||||
const raw = localStorage.getItem(SETTINGS_KEY)
|
||||
if (raw) {
|
||||
const o = JSON.parse(raw) as { showOuter?: boolean; tightOrb?: boolean }
|
||||
return {
|
||||
showOuter: typeof o.showOuter === 'boolean' ? o.showOuter : true,
|
||||
tightOrb: typeof o.tightOrb === 'boolean' ? o.tightOrb : false,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return { showOuter: true, tightOrb: false }
|
||||
}
|
||||
|
||||
export function saveWheelSettings(showOuter: boolean, tightOrb: boolean) {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ showOuter, tightOrb }))
|
||||
}
|
||||
Reference in New Issue
Block a user