个性化处理

1. 为所有抽屉和对话框添加 ESC 键关闭功能;
2. 为`使用记录`表格添加自动刷新开关;
3. 为后端 API 请求增加 User-Agent 头部;
4. 修改启动命令支持从.env中读取数据库和Redis配置。
This commit is contained in:
hoping
2025-12-19 17:31:15 +08:00
parent 6aa1876955
commit 8c12174521
11 changed files with 201 additions and 6 deletions

View File

@@ -1,8 +1,16 @@
# ==================== 必须配置(启动前) ==================== # ==================== 必须配置(启动前) ====================
# 以下配置项必须在项目启动前设置 # 以下配置项必须在项目启动前设置
# 数据库密码 # 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_NAME=aether
DB_PASSWORD=your_secure_password_here DB_PASSWORD=your_secure_password_here
# Redis 配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your_redis_password_here REDIS_PASSWORD=your_redis_password_here
# JWT密钥使用 python generate_keys.py 生成) # JWT密钥使用 python generate_keys.py 生成)

3
dev.sh
View File

@@ -8,7 +8,8 @@ source .env
set +a set +a
# 构建 DATABASE_URL # 构建 DATABASE_URL
export DATABASE_URL="postgresql://postgres:${DB_PASSWORD}@localhost:5432/aether" export DATABASE_URL="postgresql://${DB_USER:-postgres}:${DB_PASSWORD}@${DB_HOST:-localhost}:${DB_PORT:-5432}/${DB_NAME:-aether}"
export REDIS_URL=redis://:${REDIS_PASSWORD}@${REDIS_HOST:-localhost}:${REDIS_PORT:-6379}/0
# 启动 uvicorn热重载模式 # 启动 uvicorn热重载模式
echo "🚀 启动本地开发服务器..." echo "🚀 启动本地开发服务器..."

View File

@@ -92,6 +92,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, useSlots, type Component } from 'vue' import { computed, useSlots, type Component } from 'vue'
import { useEscapeKey } from '@/composables/useEscapeKey'
// Props 定义 // Props 定义
const props = defineProps<{ const props = defineProps<{
@@ -157,4 +158,14 @@ const maxWidthClass = computed(() => {
const containerZIndex = computed(() => props.zIndex || 60) const containerZIndex = computed(() => props.zIndex || 60)
const backdropZIndex = computed(() => props.zIndex || 60) const backdropZIndex = computed(() => props.zIndex || 60)
const contentZIndex = computed(() => (props.zIndex || 60) + 10) const contentZIndex = computed(() => (props.zIndex || 60) + 10)
// 添加 ESC 键监听
useEscapeKey(() => {
if (isOpen.value) {
handleClose()
}
}, {
disableOnInput: true,
once: false
})
</script> </script>

View File

@@ -0,0 +1,75 @@
import { onMounted, onUnmounted, ref } from 'vue'
/**
* ESC 键监听 Composable简化版本直接使用独立监听器
* 用于按 ESC 键关闭弹窗或其他可关闭的组件
*
* @param callback - 按 ESC 键时执行的回调函数
* @param options - 配置选项
*/
export function useEscapeKey(
callback: () => void,
options: {
/** 是否在输入框获得焦点时禁用 ESC 键,默认 true */
disableOnInput?: boolean
/** 是否只监听一次,默认 false */
once?: boolean
} = {}
) {
const { disableOnInput = true, once = false } = options
const isActive = ref(true)
function handleKeyDown(event: KeyboardEvent) {
// 只处理 ESC 键
if (event.key !== 'Escape') return
// 检查组件是否还活跃
if (!isActive.value) return
// 如果配置了在输入框获得焦点时禁用,则检查当前焦点元素
if (disableOnInput) {
const activeElement = document.activeElement
const isInputElement = activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.tagName === 'SELECT' ||
activeElement.contentEditable === 'true' ||
activeElement.getAttribute('role') === 'textbox' ||
activeElement.getAttribute('role') === 'combobox'
)
// 如果焦点在输入框中,不处理 ESC 键
if (isInputElement) return
}
// 执行回调
callback()
// 如果只监听一次,则移除监听器
if (once) {
removeEventListener()
}
}
function addEventListener() {
document.addEventListener('keydown', handleKeyDown)
}
function removeEventListener() {
document.removeEventListener('keydown', handleKeyDown)
}
onMounted(() => {
addEventListener()
})
onUnmounted(() => {
isActive.value = false
removeEventListener()
})
return {
addEventListener,
removeEventListener
}
}

View File

@@ -698,6 +698,7 @@ import {
Layers, Layers,
BarChart3 BarChart3
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useEscapeKey } from '@/composables/useEscapeKey'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import Card from '@/components/ui/card.vue' import Card from '@/components/ui/card.vue'
import Badge from '@/components/ui/badge.vue' import Badge from '@/components/ui/badge.vue'
@@ -833,6 +834,16 @@ watch(() => props.open, (newOpen) => {
detailTab.value = 'basic' detailTab.value = 'basic'
} }
}) })
// 添加 ESC 键监听
useEscapeKey(() => {
if (props.open) {
handleClose()
}
}, {
disableOnInput: true,
once: false
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -655,6 +655,7 @@ import {
GripVertical, GripVertical,
Copy Copy
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useEscapeKey } from '@/composables/useEscapeKey'
import Button from '@/components/ui/button.vue' import Button from '@/components/ui/button.vue'
import Badge from '@/components/ui/badge.vue' import Badge from '@/components/ui/badge.vue'
import Card from '@/components/ui/card.vue' import Card from '@/components/ui/card.vue'
@@ -1296,6 +1297,16 @@ async function loadEndpoints() {
showError(err.response?.data?.detail || '加载端点失败', '错误') showError(err.response?.data?.detail || '加载端点失败', '错误')
} }
} }
// 添加 ESC 键监听
useEscapeKey(() => {
if (props.open) {
handleClose()
}
}, {
disableOnInput: true,
once: false
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -472,6 +472,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, computed } from 'vue' import { ref, watch, computed } from 'vue'
import Button from '@/components/ui/button.vue' import Button from '@/components/ui/button.vue'
import { useEscapeKey } from '@/composables/useEscapeKey'
import Card from '@/components/ui/card.vue' import Card from '@/components/ui/card.vue'
import Badge from '@/components/ui/badge.vue' import Badge from '@/components/ui/badge.vue'
import Separator from '@/components/ui/separator.vue' import Separator from '@/components/ui/separator.vue'
@@ -897,6 +898,16 @@ const providerHeadersWithDiff = computed(() => {
return result return result
}) })
// 添加 ESC 键监听
useEscapeKey(() => {
if (props.isOpen) {
handleClose()
}
}, {
disableOnInput: true,
once: false
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -138,9 +138,21 @@
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
<RefreshButton <RefreshButton
v-if="!autoRefresh"
:loading="loading" :loading="loading"
@click="$emit('refresh')" @click="$emit('refresh')"
/> />
<!-- 自动刷新开关 -->
<div class="flex items-center gap-2">
<Switch
:model-value="autoRefresh"
@update:model-value="$emit('update:autoRefresh', $event)"
/>
<label class="text-xs text-muted-foreground cursor-pointer select-none" @click="$emit('update:autoRefresh', !autoRefresh)">
自动刷新
</label>
</div>
</template> </template>
<Table> <Table>
@@ -421,6 +433,7 @@ import {
TableCell, TableCell,
Pagination, Pagination,
RefreshButton, RefreshButton,
Switch,
} from '@/components/ui' } from '@/components/ui'
import { formatTokens, formatCurrency } from '@/utils/format' import { formatTokens, formatCurrency } from '@/utils/format'
import { formatDateTime } from '../composables' import { formatDateTime } from '../composables'
@@ -453,6 +466,8 @@ const props = defineProps<{
pageSize: number pageSize: number
totalRecords: number totalRecords: number
pageSizeOptions: number[] pageSizeOptions: number[]
// 自动刷新
autoRefresh: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -463,6 +478,7 @@ const emit = defineEmits<{
'update:filterStatus': [value: string] 'update:filterStatus': [value: string]
'update:currentPage': [value: number] 'update:currentPage': [value: number]
'update:pageSize': [value: number] 'update:pageSize': [value: number]
'update:autoRefresh': [value: boolean]
'refresh': [] 'refresh': []
'showDetail': [id: string] 'showDetail': [id: string]
}>() }>()

View File

@@ -65,6 +65,7 @@
:page-size="pageSize" :page-size="pageSize"
:total-records="totalRecords" :total-records="totalRecords"
:page-size-options="pageSizeOptions" :page-size-options="pageSizeOptions"
:auto-refresh="globalAutoRefresh"
@update:selected-period="handlePeriodChange" @update:selected-period="handlePeriodChange"
@update:filter-user="handleFilterUserChange" @update:filter-user="handleFilterUserChange"
@update:filter-model="handleFilterModelChange" @update:filter-model="handleFilterModelChange"
@@ -72,6 +73,7 @@
@update:filter-status="handleFilterStatusChange" @update:filter-status="handleFilterStatusChange"
@update:current-page="handlePageChange" @update:current-page="handlePageChange"
@update:page-size="handlePageSizeChange" @update:page-size="handlePageSizeChange"
@update:auto-refresh="handleAutoRefreshChange"
@refresh="refreshData" @refresh="refreshData"
@export="exportData" @export="exportData"
@show-detail="showRequestDetail" @show-detail="showRequestDetail"
@@ -214,7 +216,10 @@ const hasActiveRequests = computed(() => activeRequestIds.value.length > 0)
// 自动刷新定时器 // 自动刷新定时器
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
const AUTO_REFRESH_INTERVAL = 1000 // 1秒刷新一次 let globalAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
const AUTO_REFRESH_INTERVAL = 1000 // 1秒刷新一次用于活跃请求
const GLOBAL_AUTO_REFRESH_INTERVAL = 30000 // 30秒刷新一次全局自动刷新
const globalAutoRefresh = ref(false) // 全局自动刷新开关
// 轮询活跃请求状态(轻量级,只更新状态变化的记录) // 轮询活跃请求状态(轻量级,只更新状态变化的记录)
async function pollActiveRequests() { async function pollActiveRequests() {
@@ -278,9 +283,34 @@ watch(hasActiveRequests, (hasActive) => {
} }
}, { immediate: true }) }, { immediate: true })
// 启动全局自动刷新
function startGlobalAutoRefresh() {
if (globalAutoRefreshTimer) return
globalAutoRefreshTimer = setInterval(refreshData, GLOBAL_AUTO_REFRESH_INTERVAL)
}
// 停止全局自动刷新
function stopGlobalAutoRefresh() {
if (globalAutoRefreshTimer) {
clearInterval(globalAutoRefreshTimer)
globalAutoRefreshTimer = null
}
}
// 处理自动刷新开关变化
function handleAutoRefreshChange(value: boolean) {
globalAutoRefresh.value = value
if (value) {
startGlobalAutoRefresh()
} else {
stopGlobalAutoRefresh()
}
}
// 组件卸载时清理定时器 // 组件卸载时清理定时器
onUnmounted(() => { onUnmounted(() => {
stopAutoRefresh() stopAutoRefresh()
stopGlobalAutoRefresh()
}) })
// 用户页面的前端分页 // 用户页面的前端分页

View File

@@ -350,6 +350,7 @@ import {
Layers, Layers,
Image as ImageIcon Image as ImageIcon
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useEscapeKey } from '@/composables/useEscapeKey'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import Card from '@/components/ui/card.vue' import Card from '@/components/ui/card.vue'
import Badge from '@/components/ui/badge.vue' import Badge from '@/components/ui/badge.vue'
@@ -453,6 +454,16 @@ function getFirst1hCachePrice(tieredPricing: TieredPricingConfig | undefined | n
if (!tieredPricing?.tiers?.length) return '-' if (!tieredPricing?.tiers?.length) return '-'
return get1hCachePrice(tieredPricing.tiers[0]) return get1hCachePrice(tieredPricing.tiers[0])
} }
// 添加 ESC 键监听
useEscapeKey(() => {
if (props.open) {
handleClose()
}
}, {
disableOnInput: true,
once: false
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -4,6 +4,7 @@ Provider Query API 端点
""" """
import asyncio import asyncio
import os
from typing import Optional from typing import Optional
import httpx import httpx
@@ -45,7 +46,11 @@ async def _fetch_openai_models(
Returns: Returns:
tuple[list, Optional[str]]: (模型列表, 错误信息) tuple[list, Optional[str]]: (模型列表, 错误信息)
""" """
headers = {"Authorization": f"Bearer {api_key}"} useragent = os.getenv("OPENAI_USER_AGENT") or "codex_cli_rs/0.73.0 (Mac OS 14.8.4; x86_64) Apple_Terminal/453"
headers = {
"Authorization": f"Bearer {api_key}",
"User-Agent": useragent,
}
if extra_headers: if extra_headers:
# 防止 extra_headers 覆盖 Authorization # 防止 extra_headers 覆盖 Authorization
safe_headers = {k: v for k, v in extra_headers.items() if k.lower() != "authorization"} safe_headers = {k: v for k, v in extra_headers.items() if k.lower() != "authorization"}
@@ -91,10 +96,12 @@ async def _fetch_claude_models(
Returns: Returns:
tuple[list, Optional[str]]: (模型列表, 错误信息) tuple[list, Optional[str]]: (模型列表, 错误信息)
""" """
useragent = os.getenv("CLAUDE_USER_AGENT") or "claude-cli/2.0.62 (external, cli)"
headers = { headers = {
"x-api-key": api_key, "x-api-key": api_key,
"Authorization": f"Bearer {api_key}", "Authorization": f"Bearer {api_key}",
"anthropic-version": "2023-06-01", "anthropic-version": "2023-06-01",
"User-Agent": useragent,
} }
# 构建 /v1/models URL # 构建 /v1/models URL
@@ -142,9 +149,12 @@ async def _fetch_gemini_models(
models_url = f"{base_url_clean}/models?key={api_key}" models_url = f"{base_url_clean}/models?key={api_key}"
else: else:
models_url = f"{base_url_clean}/v1beta/models?key={api_key}" models_url = f"{base_url_clean}/v1beta/models?key={api_key}"
useragent = os.getenv("GEMINI_USER_AGENT") or "gemini-cli/0.1.0 (external, cli)"
headers = {
"User-Agent": useragent,
}
try: try:
response = await client.get(models_url) response = await client.get(models_url, headers=headers)
logger.debug(f"Gemini models request to {models_url}: status={response.status_code}") logger.debug(f"Gemini models request to {models_url}: status={response.status_code}")
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()