merge: 合入本地 Ops 扩展与 origin/main(ECR-009–016)

保留远程用户侧 ECR-009–016 与本地 Ops 目录/RBAC/CMS/危机等能力;文档标注分叉期间 ECR 编号冲突。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-13 01:45:53 +08:00
co-authored by Cursor
593 changed files with 21918 additions and 328 deletions
+29 -10
View File
@@ -18,7 +18,7 @@
<template v-else>
<div class="hero-block">
<HomeToolIcon :name="payload.type === 'relation' ? 'relation' : 'portrait'" :size="64" />
<HomeToolIcon :name="heroIcon" :size="64" />
<h1 class="hero-title">{{ shareTitle }}</h1>
<p class="hero-sub">{{ shareSub }}</p>
</div>
@@ -49,24 +49,35 @@ const payload = computed(() => parseShareQuery(route.query as Record<string, unk
const shareTitle = computed(() => {
const p = payload.value
if (!p) return '分享'
return p.type === 'relation' ? '关系理解分享' : '个人画像分享'
if (p.type === 'relation') return '关系理解分享'
if (p.type === 'star') return '星座分享'
return '个人画像分享'
})
const shareSub = computed(() => {
const p = payload.value
if (!p) return ''
return p.type === 'relation' ? '朋友分享了一段关系理解' : '朋友分享了一份个人画像'
if (p.type === 'relation') return '朋友分享了一段关系理解'
if (p.type === 'star') return '朋友分享了一份星座探索'
return '朋友分享了一份个人画像'
})
const heroIcon = computed(() => {
const t = payload.value?.type
if (t === 'relation') return 'relation'
if (t === 'star') return 'star'
return 'portrait'
})
const cardProps = computed(() => {
const p = payload.value
if (!p) return {}
if (p.type === 'portrait') {
if (p.type === 'portrait' || p.type === 'star') {
return {
type: 'portrait' as const,
type: p.type,
title: p.title,
line: p.line,
keywords: p.keywords,
ctaLabel: '查看完整成长报告',
ctaLabel: p.type === 'star' ? '查看完整星座报告' : '查看完整成长报告',
}
}
return {
@@ -78,10 +89,18 @@ const cardProps = computed(() => {
}
})
const ctaTo = computed(() => (payload.value?.type === 'relation' ? '/relation' : '/'))
const ctaText = computed(() =>
payload.value?.type === 'relation' ? '开始关系理解' : '生成我的个人画像',
)
const ctaTo = computed(() => {
const t = payload.value?.type
if (t === 'relation') return '/relation'
if (t === 'star') return '/star'
return '/'
})
const ctaText = computed(() => {
const t = payload.value?.type
if (t === 'relation') return '开始关系理解'
if (t === 'star') return '生成我的星座'
return '生成我的个人画像'
})
</script>
<style scoped>
+15 -10
View File
@@ -6,9 +6,9 @@ const { api, routeQuery } = vi.hoisted(() => ({
api: {
authMe: vi.fn(),
listProfiles: vi.fn(),
getLatestReport: vi.fn(),
createProfile: vi.fn(),
createStar: vi.fn(),
getLatestReport: vi.fn(),
getReport: vi.fn(),
createOrder: vi.fn(),
payMock: vi.fn(),
@@ -18,8 +18,8 @@ const { api, routeQuery } = vi.hoisted(() => ({
vi.mock('../api/client', () => ({ api }))
vi.mock('vue-router', () => ({
useRoute: () => ({ query: routeQuery, fullPath: '/star' }),
useRouter: () => ({ back: vi.fn(), replace: vi.fn() }),
useRoute: () => ({ query: routeQuery, fullPath: '/star?y=1990&m=5&d=12' }),
useRouter: () => ({ back: vi.fn(), replace: vi.fn(), push: vi.fn() }),
}))
const summary = {
@@ -42,24 +42,29 @@ const summary = {
keywords: ['金牛'],
}
const starReport = {
id: 'r1',
type: 'star',
has_deep_access: false,
summary,
detail: null,
}
describe('StarProfilePage', () => {
beforeEach(() => {
vi.clearAllMocks()
api.authMe.mockResolvedValue({ id: 'u1' })
api.listProfiles.mockResolvedValue({ items: [] })
api.createProfile.mockResolvedValue({ id: 'p1' })
api.createStar.mockResolvedValue({
id: 'r1',
has_deep_access: false,
summary,
detail: null,
})
api.getLatestReport.mockResolvedValue(starReport)
api.createStar.mockResolvedValue(starReport)
})
it('shows natal wheel and free chart content', async () => {
const w = mount(StarProfilePage, { global: { stubs: { RouterLink: true } } })
await flushPromises()
expect(api.createStar).toHaveBeenCalled()
expect(api.createProfile).toHaveBeenCalled()
expect(api.getLatestReport).toHaveBeenCalledWith('p1', 'star')
await w.findAll('button').find((b) => b.text() === '星盘')!.trigger('click')
await flushPromises()
expect(w.find('svg.wheel').exists()).toBe(true)