feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -91,21 +91,88 @@ export function useAskPage() {
|
||||
sending.value = true
|
||||
sendError.value = ''
|
||||
quotaExhausted.value = false
|
||||
draft.value = ''
|
||||
const streamId = `stream-${Date.now()}`
|
||||
let gotMeta = false
|
||||
try {
|
||||
const tid = await ensureThread()
|
||||
const out = await api.sendAskMessage(tid, content)
|
||||
messages.value = [...messages.value, out.user_message, out.assistant_message]
|
||||
quota.value = out.quota
|
||||
draft.value = ''
|
||||
await api.sendAskMessageStream(tid, content, {
|
||||
onMeta: (userMsg) => {
|
||||
gotMeta = true
|
||||
messages.value = [
|
||||
...messages.value,
|
||||
userMsg,
|
||||
{
|
||||
id: streamId,
|
||||
thread_id: tid,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
},
|
||||
onDelta: (text) => {
|
||||
const list = messages.value
|
||||
const last = list[list.length - 1]
|
||||
if (!last || last.id !== streamId) return
|
||||
messages.value = [...list.slice(0, -1), { ...last, content: last.content + text }]
|
||||
},
|
||||
onDone: (payload) => {
|
||||
const base = messages.value.filter(
|
||||
(m) => m.id !== streamId && m.id !== payload.assistant_message.id,
|
||||
)
|
||||
messages.value = [...base, payload.assistant_message]
|
||||
quota.value = payload.quota
|
||||
},
|
||||
onError: (msg) => {
|
||||
sendError.value = msg
|
||||
quotaExhausted.value =
|
||||
msg.includes('次数已用完') || msg.includes('购买额度') || msg.includes('成长会员')
|
||||
messages.value = messages.value.filter((m) => m.id !== streamId)
|
||||
if (!gotMeta) draft.value = content
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : '发送失败'
|
||||
sendError.value = msg
|
||||
quotaExhausted.value = msg.includes('次数已用完') || msg.includes('成长会员')
|
||||
if (!sendError.value) sendError.value = msg
|
||||
quotaExhausted.value =
|
||||
msg.includes('次数已用完') || msg.includes('购买额度') || msg.includes('成长会员')
|
||||
messages.value = messages.value.filter((m) => m.id !== streamId)
|
||||
if (!gotMeta && !draft.value) draft.value = content
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshQuota() {
|
||||
try {
|
||||
quota.value = await api.getAskQuota()
|
||||
if (quota.value.remaining > 0) {
|
||||
quotaExhausted.value = false
|
||||
sendError.value = ''
|
||||
}
|
||||
} catch {
|
||||
/* ignore; user can retry send */
|
||||
}
|
||||
}
|
||||
|
||||
async function clearChat() {
|
||||
if (sending.value) return
|
||||
if (!messages.value.length && !threadId.value) return
|
||||
sendError.value = ''
|
||||
const tid = threadId.value
|
||||
messages.value = []
|
||||
threadId.value = ''
|
||||
draft.value = ''
|
||||
if (tid) {
|
||||
try {
|
||||
await api.clearAskThread(tid)
|
||||
} catch {
|
||||
/* local clear still applies */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(boot)
|
||||
|
||||
return {
|
||||
@@ -126,5 +193,7 @@ export function useAskPage() {
|
||||
askAdvisor,
|
||||
send,
|
||||
boot,
|
||||
refreshQuota,
|
||||
clearChat,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,82 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref, toRefs } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api } from '../api/client'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { homeFeeds, homeGridRow1, homeGridRow2, homeSearchHints } from '../lib/homeCatalog'
|
||||
import {
|
||||
homeFeeds,
|
||||
homeGridRow1,
|
||||
homeGridRow2,
|
||||
homeSearchHints,
|
||||
type HomeTool,
|
||||
} from '../lib/homeCatalog'
|
||||
import type { HomeToolIconName } from '../components/HomeToolIcon.vue'
|
||||
import { useHomeMood } from './useHomeMood'
|
||||
|
||||
const ICONS = new Set([
|
||||
'mbti', 'star', 'portrait', 'rhythm', 'synastry', 'astro',
|
||||
'companion', 'ask', 'cards', 'reports', 'growth', 'relation',
|
||||
])
|
||||
|
||||
function mapTools(
|
||||
items: Array<{
|
||||
row_index: number
|
||||
sort_order: number
|
||||
path: string
|
||||
icon: string
|
||||
label: string
|
||||
badge?: string | null
|
||||
badge_tone?: string | null
|
||||
}>,
|
||||
): { row1: HomeTool[]; row2: HomeTool[] } {
|
||||
const toTool = (it: (typeof items)[0]): HomeTool | null => {
|
||||
if (!ICONS.has(it.icon)) return null
|
||||
const t: HomeTool = {
|
||||
to: it.path,
|
||||
icon: it.icon as HomeToolIconName,
|
||||
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: HomeTool[] = []
|
||||
const row2: HomeTool[] = []
|
||||
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 }
|
||||
}
|
||||
|
||||
export function useHomePage() {
|
||||
const router = useRouter()
|
||||
const profileLabel = ref('自己')
|
||||
const plusOpen = ref(false)
|
||||
const mood = useHomeMood()
|
||||
const grid = reactive({
|
||||
gridRow1: [...homeGridRow1] as HomeTool[],
|
||||
gridRow2: [...homeGridRow2] as HomeTool[],
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void api
|
||||
.getHomeTools()
|
||||
.then((res) => {
|
||||
const mapped = mapTools(res.items || [])
|
||||
if (mapped.row1.length || mapped.row2.length) {
|
||||
grid.gridRow1 = mapped.row1
|
||||
grid.gridRow2 = mapped.row2
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
/* keep static fallback */
|
||||
})
|
||||
})
|
||||
|
||||
const searchHint = computed(() => {
|
||||
const i = new Date().getHours() % homeSearchHints.length
|
||||
@@ -39,8 +107,7 @@ export function useHomePage() {
|
||||
plusOpen,
|
||||
mood,
|
||||
searchHint,
|
||||
gridRow1: homeGridRow1,
|
||||
gridRow2: homeGridRow2,
|
||||
...toRefs(grid),
|
||||
feeds: homeFeeds,
|
||||
goPlus,
|
||||
trackGrid,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { GrowthReport } from '@yuxingu/types'
|
||||
import { validateBirth } from '@yuxingu/utils'
|
||||
import { api } from '../api/client'
|
||||
import type { WuxingBar } from '../components/WuxingBars.vue'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { ensureAccount, loadSelfLatest } from '../lib/authSession'
|
||||
|
||||
export const resultTabs = [
|
||||
{ key: 'overview' as const, label: '概览' },
|
||||
@@ -15,6 +16,7 @@ export const resultTabs = [
|
||||
|
||||
export function useLifeRhythmPage() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const needBirth = ref(true)
|
||||
const error = ref('')
|
||||
@@ -68,7 +70,7 @@ export function useLifeRhythmPage() {
|
||||
try {
|
||||
const birth = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||
const profile = await api.createProfile({ relation: 'self', birth_date: birth, display_name: '我' })
|
||||
report.value = await api.createRhythm(profile.id)
|
||||
report.value = await api.getLatestReport(profile.id, 'rhythm')
|
||||
tab.value = 'overview'
|
||||
track(AnalyticsEvent.PortraitCompleted, { source: 'rhythm' })
|
||||
} catch (e) {
|
||||
@@ -92,6 +94,7 @@ export function useLifeRhythmPage() {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!(await ensureAccount(router, route.fullPath))) return
|
||||
const reportId = String(route.query.report_id || '')
|
||||
if (reportId) {
|
||||
loading.value = true
|
||||
@@ -107,14 +110,35 @@ export function useLifeRhythmPage() {
|
||||
}
|
||||
return
|
||||
}
|
||||
const y = Number(route.query.y)
|
||||
const m = Number(route.query.m)
|
||||
const d = Number(route.query.d)
|
||||
if (y && m && d && !validateBirth(y, m, d)) {
|
||||
year.value = String(y)
|
||||
month.value = String(m)
|
||||
day.value = String(d)
|
||||
await generate(y, m, d)
|
||||
loading.value = true
|
||||
try {
|
||||
const cached = await loadSelfLatest('rhythm')
|
||||
if (cached.report) {
|
||||
report.value = cached.report
|
||||
needBirth.value = false
|
||||
tab.value = 'overview'
|
||||
return
|
||||
}
|
||||
if (cached.profile) {
|
||||
report.value = await api.createRhythm(cached.profile.id)
|
||||
needBirth.value = false
|
||||
tab.value = 'overview'
|
||||
return
|
||||
}
|
||||
const y = Number(route.query.y)
|
||||
const m = Number(route.query.m)
|
||||
const d = Number(route.query.d)
|
||||
if (y && m && d && !validateBirth(y, m, d)) {
|
||||
year.value = String(y)
|
||||
month.value = String(m)
|
||||
day.value = String(d)
|
||||
await generate(y, m, d)
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
needBirth.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { GrowthReport } from '@yuxingu/types'
|
||||
import { validateBirth } from '@yuxingu/utils'
|
||||
import { api } from '../api/client'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { ensureAccount, loadSelfLatest } from '../lib/authSession'
|
||||
import type { PortraitSharePayload } from '../lib/shareLink'
|
||||
|
||||
export function usePortraitPage() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const needBirth = ref(true)
|
||||
const error = ref('')
|
||||
@@ -44,7 +46,7 @@ export function usePortraitPage() {
|
||||
try {
|
||||
const birth = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||
const profile = await api.createProfile({ relation: 'self', birth_date: birth, display_name: '我' })
|
||||
report.value = await api.createPortrait(profile.id)
|
||||
report.value = await api.getLatestReport(profile.id, 'portrait')
|
||||
track(AnalyticsEvent.PortraitCompleted, { source: 'form' })
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
@@ -67,6 +69,7 @@ export function usePortraitPage() {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!(await ensureAccount(router, route.fullPath))) return
|
||||
const reportId = String(route.query.report_id || '')
|
||||
if (reportId) {
|
||||
loading.value = true
|
||||
@@ -83,18 +86,38 @@ export function usePortraitPage() {
|
||||
}
|
||||
return
|
||||
}
|
||||
const y = Number(route.query.y)
|
||||
const m = Number(route.query.m)
|
||||
const d = Number(route.query.d)
|
||||
if (y && m && d && !validateBirth(y, m, d)) {
|
||||
year.value = String(y)
|
||||
month.value = String(m)
|
||||
day.value = String(d)
|
||||
await generate(y, m, d)
|
||||
return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const cached = await loadSelfLatest('portrait')
|
||||
if (cached.report) {
|
||||
report.value = cached.report
|
||||
needBirth.value = false
|
||||
track(AnalyticsEvent.PortraitCompleted, { source: 'cache' })
|
||||
return
|
||||
}
|
||||
if (cached.profile) {
|
||||
report.value = await api.createPortrait(cached.profile.id)
|
||||
needBirth.value = false
|
||||
return
|
||||
}
|
||||
const y = Number(route.query.y)
|
||||
const m = Number(route.query.m)
|
||||
const d = Number(route.query.d)
|
||||
if (y && m && d && !validateBirth(y, m, d)) {
|
||||
year.value = String(y)
|
||||
month.value = String(m)
|
||||
day.value = String(d)
|
||||
await generate(y, m, d)
|
||||
return
|
||||
}
|
||||
needBirth.value = true
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
needBirth.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
needBirth.value = true
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function retry() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { Profile } from '@yuxingu/types'
|
||||
import { ensureAccount } from '../lib/authSession'
|
||||
import {
|
||||
avatarInitial,
|
||||
createProfilePageActions,
|
||||
@@ -102,6 +103,7 @@ export function useProfilePage() {
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (!(await ensureAccount(router, route.fullPath))) return
|
||||
await load()
|
||||
applyQueryFlags()
|
||||
})
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { GrowthReport } from '@yuxingu/types'
|
||||
import { validateBirth } from '@yuxingu/utils'
|
||||
import { api } from '../api/client'
|
||||
import type { WheelAspect, WheelPlanet } from '../components/NatalWheel.vue'
|
||||
import { AnalyticsEvent, track } from '../lib/analytics'
|
||||
import { ensureAccount, loadSelfLatest } from '../lib/authSession'
|
||||
import {
|
||||
ascLonFromSummary,
|
||||
fortuneBundleFrom,
|
||||
@@ -30,6 +31,7 @@ export { starViewTabs, starFortuneKeys, fortuneLabel }
|
||||
|
||||
export function useStarProfilePage() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const needBirth = ref(true)
|
||||
const error = ref('')
|
||||
@@ -116,7 +118,7 @@ export function useStarProfilePage() {
|
||||
birth_time: bt,
|
||||
birth_place: birthPlace.value || undefined,
|
||||
})
|
||||
report.value = await api.createStar(profile.id)
|
||||
report.value = await api.getLatestReport(profile.id, 'star')
|
||||
view.value = 'overview'
|
||||
track(AnalyticsEvent.PortraitCompleted, { source: 'star' })
|
||||
} catch (e) {
|
||||
@@ -140,6 +142,7 @@ export function useStarProfilePage() {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!(await ensureAccount(router, route.fullPath))) return
|
||||
const reportId = String(route.query.report_id || '')
|
||||
if (reportId) {
|
||||
loading.value = true
|
||||
@@ -155,14 +158,35 @@ export function useStarProfilePage() {
|
||||
}
|
||||
return
|
||||
}
|
||||
const y = Number(route.query.y)
|
||||
const m = Number(route.query.m)
|
||||
const d = Number(route.query.d)
|
||||
if (y && m && d && !validateBirth(y, m, d)) {
|
||||
year.value = String(y)
|
||||
month.value = String(m)
|
||||
day.value = String(d)
|
||||
await generate(y, m, d)
|
||||
loading.value = true
|
||||
try {
|
||||
const cached = await loadSelfLatest('star')
|
||||
if (cached.report) {
|
||||
report.value = cached.report
|
||||
needBirth.value = false
|
||||
view.value = 'overview'
|
||||
return
|
||||
}
|
||||
if (cached.profile) {
|
||||
report.value = await api.createStar(cached.profile.id)
|
||||
needBirth.value = false
|
||||
view.value = 'overview'
|
||||
return
|
||||
}
|
||||
const y = Number(route.query.y)
|
||||
const m = Number(route.query.m)
|
||||
const d = Number(route.query.d)
|
||||
if (y && m && d && !validateBirth(y, m, d)) {
|
||||
year.value = String(y)
|
||||
month.value = String(m)
|
||||
day.value = String(d)
|
||||
await generate(y, m, d)
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
needBirth.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user