mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(public): keep original GitHub links visible
(cherry picked from commit 7c93552697c9c35b77df35e117900ff8e9b62994)
This commit is contained in:
@@ -685,6 +685,7 @@ async fn gateway_handles_public_catalog_site_info_without_proxying_upstream() {
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["site_name"], "Aether Local");
|
||||
assert_eq!(payload["site_subtitle"], "Rust Only");
|
||||
assert_eq!(payload.as_object().map(|object| object.len()), Some(2));
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
|
||||
32
frontend/src/composables/__tests__/useSiteInfo.spec.ts
Normal file
32
frontend/src/composables/__tests__/useSiteInfo.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const apiClientMocks = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/client', () => ({
|
||||
default: apiClientMocks,
|
||||
}))
|
||||
|
||||
describe('useSiteInfo', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
apiClientMocks.get.mockReset()
|
||||
})
|
||||
|
||||
it('loads public site info', async () => {
|
||||
apiClientMocks.get.mockResolvedValue({
|
||||
data: {
|
||||
site_name: 'Custom Aether',
|
||||
site_subtitle: 'Gateway',
|
||||
},
|
||||
})
|
||||
|
||||
const { useSiteInfo } = await import('../useSiteInfo')
|
||||
const { siteName, siteSubtitle, refreshSiteInfo } = useSiteInfo()
|
||||
await refreshSiteInfo()
|
||||
|
||||
expect(siteName.value).toBe('Custom Aether')
|
||||
expect(siteSubtitle.value).toBe('Gateway')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h, nextTick, type App } from 'vue'
|
||||
|
||||
import SiteInfoSection from '../SiteInfoSection.vue'
|
||||
|
||||
vi.mock('@/components/layout', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
return {
|
||||
CardSection: defineComponent({
|
||||
name: 'CardSectionStub',
|
||||
props: {
|
||||
title: String,
|
||||
description: String,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => h('section', [
|
||||
h('h2', props.title),
|
||||
h('p', props.description),
|
||||
slots.actions?.(),
|
||||
slots.default?.(),
|
||||
])
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/components/ui/button.vue', () => ({
|
||||
default: defineComponent({
|
||||
name: 'ButtonStub',
|
||||
setup(_, { slots }) {
|
||||
return () => h('button', slots.default?.())
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
|
||||
|
||||
function mountSection() {
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(SiteInfoSection, {
|
||||
siteName: 'Aether',
|
||||
siteSubtitle: 'AI Gateway',
|
||||
loading: false,
|
||||
hasChanges: true,
|
||||
onSave: vi.fn(),
|
||||
'onUpdate:siteName': vi.fn(),
|
||||
'onUpdate:siteSubtitle': vi.fn(),
|
||||
})
|
||||
app.mount(root)
|
||||
mountedApps.push({ app, root })
|
||||
return { root }
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const { app, root } of mountedApps.splice(0)) {
|
||||
app.unmount()
|
||||
root.remove()
|
||||
}
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
describe('SiteInfoSection', () => {
|
||||
it('renders site name and subtitle fields', async () => {
|
||||
const { root } = mountSection()
|
||||
await nextTick()
|
||||
|
||||
expect(root.textContent).toContain('站点名称')
|
||||
expect(root.textContent).toContain('站点副标题')
|
||||
})
|
||||
})
|
||||
@@ -789,7 +789,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, onBeforeUnmount, nextTick, watch } from 'vue'
|
||||
import { ref, onMounted, computed, onBeforeUnmount, nextTick, watch, markRaw } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { dashboardApi, type DashboardStat, type DailyStat, type ProviderSummary } from '@/api/dashboard'
|
||||
import { getDateRangeFromPeriod } from '@/features/usage/composables'
|
||||
@@ -838,6 +839,10 @@ import type { ChartData, ChartOptions, ChartDataset, TooltipItem } from 'chart.j
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
type DashboardStatCard = Omit<DashboardStat, 'icon'> & {
|
||||
icon: Component
|
||||
}
|
||||
|
||||
const statsPanelRef = ref<HTMLElement | null>(null)
|
||||
const announcementsHeight = ref<number | null>(null)
|
||||
const announcementsTimelineRef = ref<HTMLElement | null>(null)
|
||||
@@ -941,7 +946,7 @@ const getStatIconColor = (_index: number): string => {
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const stats = ref<DashboardStat[]>([])
|
||||
const stats = ref<DashboardStatCard[]>([])
|
||||
const todayStats = ref<{
|
||||
requests: number
|
||||
tokens: number
|
||||
@@ -1006,7 +1011,7 @@ const loadingAnnouncements = ref(false)
|
||||
const selectedAnnouncement = ref<Announcement | null>(null)
|
||||
const detailDialogOpen = ref(false)
|
||||
|
||||
const iconMap: Record<string, unknown> = {
|
||||
const iconMap: Record<string, Component> = {
|
||||
Users, Activity, TrendingUp, DollarSign, Key, Hash, Zap, Database
|
||||
}
|
||||
|
||||
@@ -1328,7 +1333,7 @@ async function loadDashboardData() {
|
||||
})
|
||||
stats.value = statsData.stats.map(stat => ({
|
||||
...stat,
|
||||
icon: iconMap[stat.icon] || Activity
|
||||
icon: markRaw(iconMap[stat.icon] || Activity)
|
||||
}))
|
||||
if (statsData.today) todayStats.value = statsData.today
|
||||
if (isAdmin.value) {
|
||||
|
||||
Reference in New Issue
Block a user