mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat: 扩展 Rust gateway 全功能模块,新增 billing/crypto/wallet crate 及完整数据层
- 新增 aether-billing、aether-crypto、aether-wallet 独立 crate - aether-data 扩展 repository 层:announcements、auth_modules、billing、 candidate_selection、gemini_file_mappings、global_models、management_tokens、 oauth_providers、proxy_nodes、quota、users、wallet 等模块 - aether-gateway 新增 api/auth/billing/control/middleware/scheduler/usage/ video_tasks/hooks/maintenance/model_fetch/provider_transport 等功能模块 - 重构 executor decision 和 gateway state 为模块目录结构 - 新增 gateway router、frontdoor 路由层及对应测试 - Python 侧 API 路由重构,新增 compat/support 模块 - 前端 Logo 组件更新及 Provider 管理页面调整
This commit is contained in:
@@ -207,9 +207,6 @@ export interface PoolBatchAction {
|
||||
| 'enable'
|
||||
| 'disable'
|
||||
| 'delete'
|
||||
| 'clear_cooldown'
|
||||
| 'reset_cost'
|
||||
| 'regenerate_fingerprint'
|
||||
| 'clear_proxy'
|
||||
| 'set_proxy'
|
||||
payload?: Record<string, unknown> | null
|
||||
|
||||
@@ -41,10 +41,8 @@ export async function getProvidersSummary(
|
||||
* 获取单个 Provider 的详细信息
|
||||
*/
|
||||
export async function getProvider(providerId: string): Promise<ProviderWithEndpointsSummary> {
|
||||
return dedupedRequest(`providers:detail:${providerId}`, async () => {
|
||||
const response = await client.get<ProviderWithEndpointsSummary>(`/api/admin/providers/${providerId}/summary`)
|
||||
return response.data
|
||||
})
|
||||
const response = await client.get<ProviderWithEndpointsSummary>(`/api/admin/providers/${providerId}/summary`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
<!-- Layer 0: Ghost Tracks (Always visible, faint) -->
|
||||
<g class="ghost-layer">
|
||||
<path
|
||||
v-for="(path, index) in linePaths"
|
||||
:key="`ghost-${index}`"
|
||||
:d="path"
|
||||
v-for="line in orderedLines"
|
||||
:key="`ghost-${line.order}`"
|
||||
:d="line.path"
|
||||
class="ghost-path"
|
||||
fill="none"
|
||||
:stroke="currentColors.primary"
|
||||
@@ -67,15 +67,15 @@
|
||||
<!-- Layer 2: Animated lines -->
|
||||
<g class="lines-layer">
|
||||
<path
|
||||
v-for="(path, index) in linePaths"
|
||||
:key="`line-${index}`"
|
||||
:ref="(el) => setPathRef(el as SVGPathElement, index)"
|
||||
:d="path"
|
||||
v-for="line in orderedLines"
|
||||
:key="`line-${line.order}`"
|
||||
:ref="(el) => setPathRef(el as SVGPathElement, line.order)"
|
||||
:d="line.path"
|
||||
class="line-path"
|
||||
fill="none"
|
||||
:stroke="currentColors.primary"
|
||||
:stroke-width="strokeWidth"
|
||||
:style="getLineStyle(index)"
|
||||
:style="getLineStyle(line.order)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
@@ -86,7 +86,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
||||
import { AETHER_SVG_VIEWBOX, AETHER_LINE_PATHS, AETHER_FULL_PATH } from '@/constants/logoPaths'
|
||||
import {
|
||||
AETHER_SVG_VIEWBOX,
|
||||
AETHER_ORDERED_LINES,
|
||||
AETHER_FULL_PATH,
|
||||
getAetherLineDelayMultiplier
|
||||
} from '@/constants/logoPaths'
|
||||
|
||||
// Animation phases
|
||||
type AnimationPhase = 'idle' | 'drawOutline' | 'fillFadeIn' | 'hold' | 'fillFadeOut' | 'eraseOutline'
|
||||
@@ -133,7 +138,7 @@ const emit = defineEmits<{
|
||||
(e: 'colorChange', colors: ColorScheme): void
|
||||
}>()
|
||||
// Constants
|
||||
const LINE_COUNT = AETHER_LINE_PATHS.length
|
||||
const LINE_COUNT = AETHER_ORDERED_LINES.length
|
||||
const DEFAULT_PATH_LENGTH = 3000
|
||||
|
||||
// Light mode color schemes
|
||||
@@ -172,7 +177,7 @@ const DARK_MODE_SCHEMES: ColorScheme[] = [
|
||||
const gradientId = `aether-gradient-${Math.random().toString(36).slice(2, 9)}`
|
||||
|
||||
const viewBox = AETHER_SVG_VIEWBOX
|
||||
const linePaths = AETHER_LINE_PATHS
|
||||
const orderedLines = AETHER_ORDERED_LINES
|
||||
const fullPath = AETHER_FULL_PATH
|
||||
|
||||
// Path refs and lengths
|
||||
@@ -310,9 +315,11 @@ const startAnimation = async () => {
|
||||
currentPhase.value = 'drawOutline'
|
||||
emit('phaseChange', 'drawOutline')
|
||||
|
||||
for (let i = 0; i < LINE_COUNT; i++) {
|
||||
lineDrawn.value[i] = true
|
||||
if (i < LINE_COUNT - 1) await wait(props.lineDelay)
|
||||
for (const line of orderedLines) {
|
||||
lineDrawn.value[line.order] = true
|
||||
if (line.order < LINE_COUNT - 1) {
|
||||
await wait(Math.round(props.lineDelay * getAetherLineDelayMultiplier(line.order, LINE_COUNT)))
|
||||
}
|
||||
}
|
||||
await wait(props.strokeDuration)
|
||||
|
||||
@@ -336,9 +343,11 @@ const startAnimation = async () => {
|
||||
currentPhase.value = 'eraseOutline'
|
||||
emit('phaseChange', 'eraseOutline')
|
||||
|
||||
for (let i = 0; i < LINE_COUNT; i++) {
|
||||
lineDrawn.value[i] = false
|
||||
if (i < LINE_COUNT - 1) await wait(props.lineDelay)
|
||||
for (const line of [...orderedLines].reverse()) {
|
||||
lineDrawn.value[line.order] = false
|
||||
if (line.order > 0) {
|
||||
await wait(Math.round(props.lineDelay * getAetherLineDelayMultiplier(line.order, LINE_COUNT)))
|
||||
}
|
||||
}
|
||||
await wait(props.strokeDuration)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -146,33 +146,41 @@
|
||||
</div>
|
||||
<!-- 端点 API 格式 -->
|
||||
<div class="flex items-center gap-1.5 flex-wrap mt-3">
|
||||
<template
|
||||
v-for="endpoint in endpoints"
|
||||
:key="endpoint.id"
|
||||
>
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-md border border-border bg-background hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors font-medium"
|
||||
:class="{ 'opacity-40': !endpoint.is_active }"
|
||||
:title="`编辑 ${formatApiFormat(endpoint.api_format)} 端点`"
|
||||
@click="handleEditEndpoint(endpoint)"
|
||||
>{{ formatApiFormat(endpoint.api_format) }}</span>
|
||||
<template v-if="loadingProviderEndpoints && endpoints.length === 0">
|
||||
<span class="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Loader2 class="w-3.5 h-3.5 animate-spin" />
|
||||
加载端点中
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template
|
||||
v-for="endpoint in endpoints"
|
||||
:key="endpoint.id"
|
||||
>
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-md border border-border bg-background hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors font-medium"
|
||||
:class="{ 'opacity-40': !endpoint.is_active }"
|
||||
:title="`编辑 ${formatApiFormat(endpoint.api_format)} 端点`"
|
||||
@click="handleEditEndpoint(endpoint)"
|
||||
>{{ formatApiFormat(endpoint.api_format) }}</span>
|
||||
</template>
|
||||
<span
|
||||
v-if="endpoints.length > 0"
|
||||
class="text-xs px-2 py-0.5 rounded-md border border-dashed border-border hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors text-muted-foreground"
|
||||
title="编辑端点"
|
||||
@click="showAddEndpointDialog"
|
||||
>编辑</span>
|
||||
<Button
|
||||
v-else
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-7 text-xs"
|
||||
@click="showAddEndpointDialog"
|
||||
>
|
||||
<Plus class="w-3 h-3 mr-1" />
|
||||
添加 API 端点
|
||||
</Button>
|
||||
</template>
|
||||
<span
|
||||
v-if="endpoints.length > 0"
|
||||
class="text-xs px-2 py-0.5 rounded-md border border-dashed border-border hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors text-muted-foreground"
|
||||
title="编辑端点"
|
||||
@click="showAddEndpointDialog"
|
||||
>编辑</span>
|
||||
<Button
|
||||
v-else
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-7 text-xs"
|
||||
@click="showAddEndpointDialog"
|
||||
>
|
||||
<Plus class="w-3 h-3 mr-1" />
|
||||
添加 API 端点
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -243,7 +251,15 @@
|
||||
|
||||
<!-- 密钥列表 -->
|
||||
<div
|
||||
v-if="allKeys.length > 0"
|
||||
v-if="loadingProviderKeys && allKeys.length === 0"
|
||||
class="flex items-center justify-center gap-2 py-12 text-sm text-muted-foreground"
|
||||
>
|
||||
<Loader2 class="w-4 h-4 animate-spin" />
|
||||
正在加载{{ isKeyManagedProviderType(provider.provider_type) ? '密钥' : '账号' }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="allKeys.length > 0"
|
||||
ref="keysListRef"
|
||||
class="divide-y divide-border/40"
|
||||
:class="shouldPaginateKeys && 'flex flex-col'"
|
||||
@@ -926,6 +942,7 @@
|
||||
:provider="provider"
|
||||
:models="providerModels"
|
||||
:endpoints="endpoints"
|
||||
:loading="loadingProviderModels"
|
||||
@edit-model="handleEditModel"
|
||||
@batch-assign="handleBatchAssign"
|
||||
@refresh="loadEndpoints"
|
||||
@@ -941,6 +958,7 @@
|
||||
:provider-keys="providerKeys"
|
||||
:models="providerModels"
|
||||
:mapping-preview="providerMappingPreview"
|
||||
:loading="loadingProviderEndpoints || loadingProviderKeys || loadingProviderModels || loadingProviderMappingPreview"
|
||||
@refresh="handleModelMappingChanged"
|
||||
/>
|
||||
</div>
|
||||
@@ -1065,6 +1083,7 @@ import { useSmartPagination } from '@/composables/useSmartPagination'
|
||||
import {
|
||||
Plus,
|
||||
Key,
|
||||
Loader2,
|
||||
Edit,
|
||||
Trash2,
|
||||
RefreshCw,
|
||||
@@ -1155,6 +1174,7 @@ interface ProviderEndpointWithKeys extends ProviderEndpoint {
|
||||
interface Props {
|
||||
providerId: string | null
|
||||
open: boolean
|
||||
initialProvider?: ProviderWithEndpointsSummary | null
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -1176,6 +1196,10 @@ const endpoints = ref<ProviderEndpointWithKeys[]>([])
|
||||
const providerKeys = ref<EndpointAPIKey[]>([]) // Provider 级别的 keys
|
||||
const providerModels = ref<Model[]>([]) // Provider 级别的 models
|
||||
const providerMappingPreview = ref<ProviderMappingPreviewResponse | null>(null) // 映射预览
|
||||
const loadingProviderEndpoints = ref(false)
|
||||
const loadingProviderKeys = ref(false)
|
||||
const loadingProviderModels = ref(false)
|
||||
const loadingProviderMappingPreview = ref(false)
|
||||
let providerLoadRequestId = 0
|
||||
let endpointsLoadRequestId = 0
|
||||
let mappingPreviewLoadRequestId = 0
|
||||
@@ -1377,6 +1401,10 @@ watch(
|
||||
[() => props.providerId, () => props.open],
|
||||
async ([newId, newOpen], [_oldId, oldOpen]) => {
|
||||
if (newOpen && newId) {
|
||||
if (props.initialProvider?.id === newId) {
|
||||
provider.value = props.initialProvider
|
||||
loading.value = false
|
||||
}
|
||||
// mapping-preview 较慢,不阻塞首屏渲染
|
||||
void loadMappingPreview()
|
||||
await Promise.all([
|
||||
@@ -1403,6 +1431,10 @@ watch(
|
||||
providerKeys.value = [] // 清空 Provider 级别的 keys
|
||||
providerModels.value = []
|
||||
providerMappingPreview.value = null
|
||||
loadingProviderEndpoints.value = false
|
||||
loadingProviderKeys.value = false
|
||||
loadingProviderModels.value = false
|
||||
loadingProviderMappingPreview.value = false
|
||||
|
||||
// 重置分页状态
|
||||
resetKeysPagination()
|
||||
@@ -2775,14 +2807,15 @@ async function loadSystemFormatConversionConfig() {
|
||||
async function loadProvider() {
|
||||
if (!props.providerId) return
|
||||
const requestId = ++providerLoadRequestId
|
||||
const shouldShowSpinner = !provider.value || provider.value.id !== props.providerId
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
// 并行加载 Provider 信息和系统级格式转换配置
|
||||
const [providerData] = await Promise.all([
|
||||
getProvider(props.providerId),
|
||||
loadSystemFormatConversionConfig(),
|
||||
])
|
||||
if (shouldShowSpinner) {
|
||||
loading.value = true
|
||||
}
|
||||
// 系统级格式转换配置只影响一个图标状态,不应阻塞详情抽屉首屏。
|
||||
void loadSystemFormatConversionConfig()
|
||||
const providerData = await getProvider(props.providerId)
|
||||
if (requestId !== providerLoadRequestId) return
|
||||
provider.value = providerData
|
||||
|
||||
@@ -2793,7 +2826,7 @@ async function loadProvider() {
|
||||
if (requestId !== providerLoadRequestId) return
|
||||
showError(parseApiError(err, '加载失败'), '错误')
|
||||
} finally {
|
||||
if (requestId === providerLoadRequestId) {
|
||||
if (requestId === providerLoadRequestId && shouldShowSpinner) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
@@ -2802,22 +2835,14 @@ async function loadProvider() {
|
||||
// 加载端点列表
|
||||
async function loadEndpoints() {
|
||||
if (!props.providerId) return
|
||||
const providerId = props.providerId
|
||||
const requestId = ++endpointsLoadRequestId
|
||||
loadingProviderEndpoints.value = true
|
||||
loadingProviderKeys.value = true
|
||||
loadingProviderModels.value = true
|
||||
|
||||
try {
|
||||
// 并行加载端点列表、Provider 级别的 keys 和 models
|
||||
// mapping-preview 较慢,拆到 loadMappingPreview 独立加载,不阻塞首屏
|
||||
const [endpointsList, providerKeysResult, modelsResult] = await Promise.all([
|
||||
getProviderEndpoints(props.providerId),
|
||||
getProviderKeys(props.providerId).catch(() => []),
|
||||
getProviderModels(props.providerId).catch(() => []),
|
||||
])
|
||||
if (requestId !== endpointsLoadRequestId) return
|
||||
|
||||
providerKeys.value = providerKeysResult
|
||||
providerModels.value = modelsResult
|
||||
// 按 API 格式排序
|
||||
const sortedEndpoints = endpointsList.sort((a, b) => {
|
||||
const sortEndpoints = (items: ProviderEndpoint[]): ProviderEndpointWithKeys[] => {
|
||||
return [...items].sort((a, b) => {
|
||||
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)
|
||||
const bIdx = API_FORMAT_ORDER.indexOf(b.api_format)
|
||||
if (aIdx === -1 && bIdx === -1) return 0
|
||||
@@ -2825,18 +2850,60 @@ async function loadEndpoints() {
|
||||
if (bIdx === -1) return -1
|
||||
return aIdx - bIdx
|
||||
})
|
||||
endpoints.value = sortedEndpoints
|
||||
syncCurrentSelections(sortedEndpoints, providerKeysResult)
|
||||
} catch (err: unknown) {
|
||||
if (requestId !== endpointsLoadRequestId) return
|
||||
showError(parseApiError(err, '加载端点失败'), '错误')
|
||||
}
|
||||
|
||||
const endpointsPromise = getProviderEndpoints(providerId)
|
||||
.then((endpointsList) => {
|
||||
if (requestId !== endpointsLoadRequestId) return
|
||||
const sortedEndpoints = sortEndpoints(endpointsList)
|
||||
endpoints.value = sortedEndpoints
|
||||
syncCurrentSelections(sortedEndpoints, providerKeys.value)
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (requestId !== endpointsLoadRequestId) return
|
||||
endpoints.value = []
|
||||
syncCurrentSelections([], providerKeys.value)
|
||||
showError(parseApiError(err, '加载端点失败'), '错误')
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestId === endpointsLoadRequestId) {
|
||||
loadingProviderEndpoints.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const providerKeysPromise = getProviderKeys(providerId)
|
||||
.catch(() => [])
|
||||
.then((providerKeysResult) => {
|
||||
if (requestId !== endpointsLoadRequestId) return
|
||||
providerKeys.value = providerKeysResult
|
||||
syncCurrentSelections(endpoints.value, providerKeysResult)
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestId === endpointsLoadRequestId) {
|
||||
loadingProviderKeys.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const modelsPromise = getProviderModels(providerId)
|
||||
.catch(() => [])
|
||||
.then((modelsResult) => {
|
||||
if (requestId !== endpointsLoadRequestId) return
|
||||
providerModels.value = modelsResult
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestId === endpointsLoadRequestId) {
|
||||
loadingProviderModels.value = false
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.allSettled([endpointsPromise, providerKeysPromise, modelsPromise])
|
||||
}
|
||||
|
||||
// 加载映射预览(独立于 loadEndpoints,不阻塞首屏渲染)
|
||||
async function loadMappingPreview() {
|
||||
if (!props.providerId) return
|
||||
const requestId = ++mappingPreviewLoadRequestId
|
||||
loadingProviderMappingPreview.value = true
|
||||
try {
|
||||
const preview = await getProviderMappingPreview(props.providerId)
|
||||
if (requestId !== mappingPreviewLoadRequestId) return
|
||||
@@ -2844,6 +2911,10 @@ async function loadMappingPreview() {
|
||||
} catch {
|
||||
if (requestId !== mappingPreviewLoadRequestId) return
|
||||
providerMappingPreview.value = null
|
||||
} finally {
|
||||
if (requestId === mappingPreviewLoadRequestId) {
|
||||
loadingProviderMappingPreview.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
v-if="isLoading"
|
||||
class="flex items-center justify-center py-12"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
@@ -396,6 +396,7 @@ const props = defineProps<{
|
||||
providerKeys?: EndpointAPIKey[]
|
||||
models?: Model[]
|
||||
mappingPreview?: ProviderMappingPreviewResponse | null
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -428,6 +429,7 @@ const parsedTestRequestHeaders = computed(() => parseModelTestRequestHeadersDraf
|
||||
const testRequestHeadersError = computed(() => parsedTestRequestHeaders.value.error)
|
||||
const parsedTestRequestBody = computed(() => parseModelTestRequestBodyDraft(testRequestBodyDraft.value))
|
||||
const testRequestBodyError = computed(() => parsedTestRequestBody.value.error)
|
||||
const isLoading = computed(() => Boolean(props.loading) || loading.value)
|
||||
|
||||
// 使用 props 传入的数据
|
||||
const models = computed(() => props.models ?? [])
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
v-if="isLoading"
|
||||
class="flex items-center justify-center py-12"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
@@ -269,6 +269,7 @@ const props = defineProps<{
|
||||
provider: ProviderWithEndpointsSummary
|
||||
models?: Model[]
|
||||
endpoints?: ProviderEndpoint[]
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -300,6 +301,7 @@ const testRequestHeadersError = computed(() => parsedTestRequestHeaders.value.er
|
||||
const parsedTestRequestBody = computed(() => parseModelTestRequestBodyDraft(testRequestBodyDraft.value))
|
||||
const testRequestBodyError = computed(() => parsedTestRequestBody.value.error)
|
||||
const models = computed(() => props.models ?? localModels.value)
|
||||
const isLoading = computed(() => Boolean(props.loading) || loading.value)
|
||||
// 按名称排序的模型列表
|
||||
const sortedModels = computed(() => {
|
||||
return [...models.value].sort((a, b) => {
|
||||
|
||||
@@ -241,6 +241,7 @@
|
||||
<ProviderDetailDrawer
|
||||
:open="providerDrawerOpen"
|
||||
:provider-id="selectedProviderId"
|
||||
:initial-provider="selectedProvider"
|
||||
@update:open="providerDrawerOpen = $event"
|
||||
@edit="openEditProviderDialog"
|
||||
@toggle-status="toggleProviderStatus"
|
||||
@@ -313,6 +314,10 @@ const priorityDialogOpen = ref(false)
|
||||
const priorityMode = ref<'provider' | 'global_key'>('provider')
|
||||
const providerDrawerOpen = ref(false)
|
||||
const selectedProviderId = ref<string | null>(null)
|
||||
const selectedProvider = computed<ProviderWithEndpointsSummary | null>(() => {
|
||||
if (!selectedProviderId.value) return null
|
||||
return providers.value.find(provider => provider.id === selectedProviderId.value) ?? null
|
||||
})
|
||||
const providerDeleteProgress = ref<ProviderDeleteProgressState | null>(null)
|
||||
let deletePollAbort: AbortController | null = null
|
||||
|
||||
|
||||
@@ -160,7 +160,9 @@ const isAdminPage = computed(() => route.path.startsWith('/admin'))
|
||||
const statsExpanded = useLocalStorage('usage-stats-expanded', true)
|
||||
|
||||
// 时间范围选择
|
||||
const timeRange = ref<DateRangeParams>(getDateRangeFromPeriod('today'))
|
||||
const timeRange = ref<DateRangeParams>(
|
||||
getDateRangeFromPeriod(isAdminPage.value ? 'last7days' : 'today')
|
||||
)
|
||||
|
||||
// 分页状态
|
||||
const currentPage = ref(1)
|
||||
|
||||
Reference in New Issue
Block a user