mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-09 16:06:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d26bc36db5 | ||
|
|
96d5ab674a | ||
|
|
84c0db2613 | ||
|
|
d819315d8b |
@@ -129,16 +129,6 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master
|
||||
with:
|
||||
toolchain: ${{ steps.toolchains.outputs.rust }}
|
||||
- name: Check dependency pins
|
||||
run: |
|
||||
ui_revision="$(git -C modules/ui rev-parse HEAD)"
|
||||
for lockfile in frontend/pnpm-lock.yaml modules/plugins/examples/workbench/frontend/pnpm-lock.yaml; do
|
||||
locked_revision="$(sed -nE 's@.*codeload.github.com/zyycn/codex-proxy-ui/tar.gz/([a-f0-9]{40}).*@\1@p' "$lockfile" | sort -u)"
|
||||
if [ "$locked_revision" != "$ui_revision" ]; then
|
||||
echo "::error file=$lockfile::UI dependency must match modules/ui ($ui_revision)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
- name: Install development dependencies
|
||||
run: |
|
||||
pnpm --dir modules/ui install --frozen-lockfile
|
||||
|
||||
+2
-2
@@ -114,7 +114,7 @@ pnpm --dir modules/plugins/examples/workbench/frontend build:source
|
||||
```
|
||||
|
||||
`build:source` 用各项目的 `tsconfig.source.json` 检查源码类型,管理端构建到 `frontend/node_modules/.vite/source-dist`,示例构建到其前端项目的 `.vite/source-dist`,不改写正式依赖、锁文件或 `dist`。
|
||||
源码联调 CI 显式检出子模块、核对 UI 提交锁定并验证这两个命令
|
||||
源码联调 CI 显式检出子模块,分别按锁文件安装依赖,验证这两个命令并确认联调未改写正式依赖和锁文件
|
||||
|
||||
### 本地 SDK 验证
|
||||
|
||||
@@ -145,7 +145,7 @@ pnpm --dir modules/plugins/examples/workbench/frontend build:source
|
||||
1. 在要修改的子模块内先创建分支,例如 `git -C modules/ui switch -c feat/select`。初次初始化的子模块通常处于 detached HEAD
|
||||
2. 在子模块运行自身检查,并在宿主根目录运行所需联调检查
|
||||
3. 分别提交、推送并合并组件库或插件 PR。宿主只记录子模块提交号,不能替代子仓库的提交与推送
|
||||
4. UI 更新后,将管理端和插件页面的 UI 依赖同步到同一固定标签或提交,重新生成各自锁文件,再更新宿主的子模块指针;CI 会核对三处是否一致
|
||||
4. 管理端与插件页面按需升级 UI 依赖,各自固定标签或提交并维护锁文件,无需与 `modules/ui` 同步版本
|
||||
5. 提交宿主改动及 `modules/ui`、`modules/plugins` 指针。指针必须指向远程可取得的提交,不能只存在于开发机
|
||||
|
||||
更新子模块指针不会自动升级消费方依赖。正式依赖不能写成开发机路径或浮动分支;SDK 仍由插件仓库固定到包含所需合同的宿主提交
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@boxicons/vue": "1.0.1",
|
||||
"@codex-proxy/ui": "github:zyycn/codex-proxy-ui#v0.6.1",
|
||||
"@codex-proxy/ui": "github:zyycn/codex-proxy-ui#v0.7.0",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"@lucide/vue": "^1.59.0",
|
||||
|
||||
Generated
+15
-6
@@ -195,8 +195,8 @@ importers:
|
||||
specifier: 1.0.1
|
||||
version: 1.0.1(vue@3.5.44(typescript@6.0.3))
|
||||
'@codex-proxy/ui':
|
||||
specifier: github:zyycn/codex-proxy-ui#v0.6.1
|
||||
version: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/c480b40b947761968412543d74d29a6e6e100f3f(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))
|
||||
specifier: github:zyycn/codex-proxy-ui#v0.7.0
|
||||
version: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))
|
||||
'@fontsource-variable/inter':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
@@ -443,9 +443,9 @@ packages:
|
||||
resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==}
|
||||
engines: {node: '>= 20.12.0'}
|
||||
|
||||
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/c480b40b947761968412543d74d29a6e6e100f3f':
|
||||
resolution: {gitHosted: true, integrity: sha512-mmawMrkJokL/x/H/gWYOYSL+XXC4EFQ8cRyYVF3Pqj+EjU3RlhEP4zWHxWWkTjE9+8sWShXXHG1akdC37D/Y7A==, tarball: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/c480b40b947761968412543d74d29a6e6e100f3f}
|
||||
version: 0.6.1
|
||||
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225':
|
||||
resolution: {gitHosted: true, integrity: sha512-JJesyJ1Y/QcdXhBtmKX6k0GyUgOSxiV8sJrUymvLyKjC8NhHLmtpXWt/evZcjYBDyJEIRll85bmSmtMF9/Wpqw==, tarball: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225}
|
||||
version: 0.7.0
|
||||
engines: {node: '>=24'}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -972,6 +972,10 @@ packages:
|
||||
resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@rc-component/async-validator@6.0.0':
|
||||
resolution: {integrity: sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==}
|
||||
engines: {node: '>=14.x'}
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.14':
|
||||
resolution: {integrity: sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -3422,11 +3426,12 @@ snapshots:
|
||||
fast-wrap-ansi: 0.2.2
|
||||
sisteransi: 1.0.5
|
||||
|
||||
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/c480b40b947761968412543d74d29a6e6e100f3f(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))':
|
||||
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.4(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@ant-design/colors': 8.0.1
|
||||
'@ant-design/fast-color': 3.0.1
|
||||
'@lucide/vue': 1.59.0(vue@3.5.44(typescript@6.0.3))
|
||||
'@rc-component/async-validator': 6.0.0
|
||||
'@vueuse/core': 15.0.0(vue@3.5.44(typescript@6.0.3))
|
||||
'@vueuse/integrations': 15.0.0(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(sortablejs@1.15.7)(vue@3.5.44(typescript@6.0.3))
|
||||
dayjs: 1.11.23
|
||||
@@ -3858,6 +3863,10 @@ snapshots:
|
||||
|
||||
'@pkgr/core@0.3.6': {}
|
||||
|
||||
'@rc-component/async-validator@6.0.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.10
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.14':
|
||||
optional: true
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { escape } from 'es-toolkit'
|
||||
import { isRecord } from '@/utils/data'
|
||||
|
||||
interface ChartTooltipTheme {
|
||||
@@ -45,6 +46,11 @@ export function chartTooltipStyle(
|
||||
}
|
||||
}
|
||||
|
||||
export function chartTooltipContent(label: string, lines: string[]) {
|
||||
const title = label ? `<span style="font-size:13px;line-height:20px">${escape(label)}</span>` : ''
|
||||
return [title, ...lines].filter(Boolean).join('<br/>')
|
||||
}
|
||||
|
||||
export function tooltipRows(params: unknown): Record<string, unknown>[] {
|
||||
const values = Array.isArray(params) ? params : [params]
|
||||
return values.filter(isRecord)
|
||||
|
||||
@@ -7,7 +7,7 @@ import { formatProviderLabel } from '@/utils/providers'
|
||||
|
||||
export const usageDataColumns = computed(() => ([
|
||||
{ key: 'model', label: t('usage.fields.model'), kind: 'custom', size: 'lg' },
|
||||
{ key: 'reasoningEffort', label: t('usage.fields.reasoningEffort'), kind: 'status', size: 'md', align: 'left' },
|
||||
{ key: 'reasoningEffort', label: t('usage.fields.reasoningEffort'), kind: 'status', size: 'md' },
|
||||
{ key: 'route', label: t('usage.fields.endpoint'), kind: 'mono' },
|
||||
{ key: 'upstreamTransport', label: t('usage.fields.upstream'), kind: 'status', size: 'md' },
|
||||
{ key: 'clientTransport', label: t('usage.fields.access'), kind: 'status', size: 'md' },
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"activeAccounts": {
|
||||
"todayTokens": "Tokens",
|
||||
"requestCount": "Requests",
|
||||
"title": "Active account usage",
|
||||
"sortByRecent": "Sorted by last used",
|
||||
"empty": "No recent account requests",
|
||||
|
||||
@@ -640,6 +640,12 @@
|
||||
"description": "Manage scheduling, upstreams, pricing, security, history retention, and backups",
|
||||
"sectionsLabel": "Settings sections",
|
||||
"undoAllBasicSettingsChanges": "Undo all basic settings changes",
|
||||
"unsavedChanges": {
|
||||
"title": "Unsaved changes",
|
||||
"description": "Your unsaved settings changes will be lost if you leave",
|
||||
"leave": "Discard and leave",
|
||||
"stay": "Keep editing"
|
||||
},
|
||||
"saveChanges": "Save",
|
||||
"settingsLoadingFailed": "Settings loading failed",
|
||||
"reload": "Reload",
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"unavailableCount": "不可用 {value}"
|
||||
},
|
||||
"activeAccounts": {
|
||||
"todayTokens": "今日 Token",
|
||||
"todayTokens": "Token",
|
||||
"requestCount": "次数",
|
||||
"title": "活跃账号用量",
|
||||
"sortByRecent": "最近使用排序",
|
||||
"empty": "暂无账号请求记录",
|
||||
|
||||
@@ -640,6 +640,12 @@
|
||||
"description": "配置网关调度、上游与定价,管理访问安全、历史保留与备份",
|
||||
"sectionsLabel": "设置分区",
|
||||
"undoAllBasicSettingsChanges": "撤销全部基础设置更改",
|
||||
"unsavedChanges": {
|
||||
"title": "有未保存的更改",
|
||||
"description": "离开后,未保存的设置更改将丢失",
|
||||
"leave": "放弃并离开",
|
||||
"stay": "继续编辑"
|
||||
},
|
||||
"saveChanges": "保存基础设置",
|
||||
"settingsLoadingFailed": "设置加载失败",
|
||||
"reload": "重新加载",
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"unavailableCount": "不可用 {value}"
|
||||
},
|
||||
"activeAccounts": {
|
||||
"todayTokens": "今日 Token",
|
||||
"todayTokens": "Token",
|
||||
"requestCount": "次數",
|
||||
"title": "活躍賬號用量",
|
||||
"sortByRecent": "最近使用排序",
|
||||
"empty": "暫無賬號請求記錄",
|
||||
|
||||
@@ -640,6 +640,12 @@
|
||||
"description": "配置網關調度、上游與定價,管理訪問安全、歷史保留與備份",
|
||||
"sectionsLabel": "設置分區",
|
||||
"undoAllBasicSettingsChanges": "撤銷全部基礎設置更改",
|
||||
"unsavedChanges": {
|
||||
"title": "有未儲存的更改",
|
||||
"description": "離開後,未儲存的設定更改將遺失",
|
||||
"leave": "放棄並離開",
|
||||
"stay": "繼續編輯"
|
||||
},
|
||||
"saveChanges": "保存基礎設置",
|
||||
"settingsLoadingFailed": "設置加載失敗",
|
||||
"reload": "重新加載",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ZScrollbar } from '@codex-proxy/ui'
|
||||
import { useModalFocus, ZScrollbar } from '@codex-proxy/ui'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { RouterView, useRoute } from 'vue-router'
|
||||
@@ -22,16 +23,30 @@ const { toggleSidebar } = uiStore
|
||||
const route = useRoute()
|
||||
const pageScrollbarRef = ref<InstanceType<typeof ZScrollbar> | null>(null)
|
||||
const mobileSidebarOpen = shallowRef(false)
|
||||
const mobileSidebarPanel = useTemplateRef<HTMLElement>('mobileSidebarPanel')
|
||||
const desktop = useMediaQuery('(min-width: 961px)')
|
||||
const { rememberFocus, focusInitial, handleKeydown, restoreFocus } = useModalFocus(mobileSidebarPanel, {
|
||||
isOpen: () => mobileSidebarOpen.value,
|
||||
onEscape: closeMobileSidebar,
|
||||
})
|
||||
const systemUpdateOpen = shallowRef(false)
|
||||
|
||||
function openMobileSidebar() {
|
||||
async function openMobileSidebar() {
|
||||
rememberFocus()
|
||||
mobileSidebarOpen.value = true
|
||||
await nextTick()
|
||||
focusInitial()
|
||||
}
|
||||
|
||||
function closeMobileSidebar() {
|
||||
mobileSidebarOpen.value = false
|
||||
}
|
||||
|
||||
watch(desktop, (isDesktop) => {
|
||||
if (isDesktop)
|
||||
closeMobileSidebar()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void systemUpdateStore.loadVersion().catch(() => undefined)
|
||||
})
|
||||
@@ -52,14 +67,14 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex h-dvh overflow-hidden bg-cp-bg-layout">
|
||||
<div class="relative flex h-dvh overflow-hidden bg-cp-bg-layout" :inert="mobileSidebarOpen">
|
||||
<AppSidebar
|
||||
:collapsed="sidebarCollapsed"
|
||||
@toggle="toggleSidebar"
|
||||
|
||||
@open-system-update="systemUpdateOpen = true"
|
||||
/>
|
||||
<FloatingSidebarToggle v-if="!mobileSidebarOpen" @open="openMobileSidebar" />
|
||||
<FloatingSidebarToggle v-show="!mobileSidebarOpen" @open="openMobileSidebar" />
|
||||
<main class="relative isolate h-dvh min-w-0 flex-1 overflow-hidden">
|
||||
<ZScrollbar ref="pageScrollbarRef">
|
||||
<div class="flex min-h-full min-w-0 flex-col p-4 min-[961px]:p-6">
|
||||
@@ -71,21 +86,26 @@ watch(
|
||||
</main>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="mobile-sidebar">
|
||||
<div v-if="mobileSidebarOpen" class="fixed inset-0 z-50 min-[961px]:hidden">
|
||||
<Transition name="mobile-sidebar" @after-leave="restoreFocus">
|
||||
<div v-if="mobileSidebarOpen" :inert="!mobileSidebarOpen" role="presentation" class="fixed inset-0 z-50 min-[961px]:hidden" @keydown="handleKeydown">
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="absolute inset-0 border-0 bg-black/32 backdrop-blur-[1px] cursor-default"
|
||||
:aria-label="t('navigation.sidebar.close')"
|
||||
@click="closeMobileSidebar"
|
||||
/>
|
||||
<div class="mobile-sidebar-panel absolute inset-y-0 left-0 flex">
|
||||
<div
|
||||
ref="mobileSidebarPanel"
|
||||
class="mobile-sidebar-panel absolute inset-y-0 left-0 flex"
|
||||
role="dialog" aria-modal="true" :aria-label="t('navigation.mainLabel')" tabindex="-1"
|
||||
>
|
||||
<AppSidebar
|
||||
mobile
|
||||
@close="closeMobileSidebar"
|
||||
@navigate="closeMobileSidebar"
|
||||
|
||||
@open-system-update="systemUpdateOpen = true"
|
||||
@open-system-update="closeMobileSidebar(); systemUpdateOpen = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,7 @@ const groupOptions = computed(() => [
|
||||
>
|
||||
<ZInput
|
||||
v-model="search"
|
||||
:aria-label="t('accounts.filters.searchLabel')"
|
||||
:placeholder="t('accounts.filters.searchLabel')"
|
||||
class="col-span-2 min-w-0 sm:col-span-3 xl:w-80 xl:flex-none"
|
||||
>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const accountColumns = computed(() => (defineTableColumns<Account>([
|
||||
align: 'center',
|
||||
formatter: value => formatProviderLabel(typeof value === 'string' ? value : null),
|
||||
},
|
||||
{ key: 'status', label: t('common.fields.status'), kind: 'status', align: 'left', sortable: true },
|
||||
{ key: 'status', label: t('common.fields.status'), kind: 'status', align: 'center', sortable: true },
|
||||
{ key: 'planType', label: t('accounts.profile.subscription.label'), kind: 'status', size: 'sm', sortable: true },
|
||||
{ key: 'usage', label: t('usage.usage'), kind: 'custom', size: '2xl', sortable: true },
|
||||
{ key: 'groups', label: t('accounts.fields.accountGrouping'), kind: 'status', size: 'sm' },
|
||||
|
||||
@@ -252,7 +252,7 @@ const statusBars = computed(() => {
|
||||
>
|
||||
<span class="grid min-w-0 gap-1">
|
||||
<span class="text-cp-xs leading-none font-bold text-cp-text-quaternary">
|
||||
{{ account.metric === 'requests' ? t('dashboard.metrics.requestCount') : t('dashboard.activeAccounts.todayTokens') }}
|
||||
{{ account.metric === 'requests' ? t('dashboard.activeAccounts.requestCount') : t('dashboard.activeAccounts.todayTokens') }}
|
||||
</span>
|
||||
<strong
|
||||
class="font-mono text-sm leading-[1.15] font-heavy tabular-nums text-cp-text"
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { DashboardTrendMetric } from '@/api'
|
||||
import { usePreferredReducedMotion } from '@vueuse/core'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
import { sampleGapBridgeSeries } from '@/components/charts/timeSeriesGap'
|
||||
import { chartTooltipStyle, tooltipIndex } from '@/components/charts/tooltip'
|
||||
import { chartTooltipContent, chartTooltipStyle, tooltipIndex } from '@/components/charts/tooltip'
|
||||
|
||||
import { useChartPalette } from '@/composables/useChartPalette'
|
||||
import { t } from '@/i18n'
|
||||
@@ -189,8 +189,7 @@ export function useRequestTrendChart(options: {
|
||||
const title = point?.label ?? ''
|
||||
|
||||
if (activeKind.value === 'usage') {
|
||||
return [
|
||||
title,
|
||||
return chartTooltipContent(title, [
|
||||
tooltipItem(t('dashboard.metrics.inputTokens'), point?.inputTokens, trendColor('inputTokens')),
|
||||
tooltipItem(t('dashboard.metrics.outputTokens'), point?.outputTokens, trendColor('outputTokens')),
|
||||
tooltipItem(
|
||||
@@ -204,9 +203,7 @@ export function useRequestTrendChart(options: {
|
||||
trendColor('cachedInputTokens'),
|
||||
),
|
||||
tooltipItem(t('dashboard.metrics.requests'), point?.requests, palette.value.textSecondary),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('<br/>')
|
||||
])
|
||||
}
|
||||
|
||||
const lines = rows.map((row) => {
|
||||
@@ -216,7 +213,7 @@ export function useRequestTrendChart(options: {
|
||||
const marker = tooltipValue(row, 'marker')
|
||||
return `${marker}${name}: ${tooltipDisplayValue(point, metric, value)}`
|
||||
})
|
||||
return [title, ...lines].filter(Boolean).join('<br/>')
|
||||
return chartTooltipContent(title, lines)
|
||||
}
|
||||
|
||||
function tooltipDisplayValue(point: TrendPoint | undefined, metric: string, value: string) {
|
||||
|
||||
@@ -5,10 +5,11 @@ import type { AccountGroupRef } from '@/api'
|
||||
import { ZButton, ZDialog, ZForm, ZFormItem, ZInput } from '@codex-proxy/ui'
|
||||
import { DollarSign, KeyRound } from '@lucide/vue'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { computed, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AccountGroupCheckboxGrid from '@/components/AccountGroupCheckboxGrid.vue'
|
||||
import ProviderRequestProfilesEditor from '@/components/client-profile/ProviderRequestProfilesEditor.vue'
|
||||
import { useApiKeyFormRules } from '../composables/useApiKeyFormRules'
|
||||
|
||||
const props = defineProps<{
|
||||
groups: AccountGroupRef[]
|
||||
@@ -27,6 +28,16 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const form = defineModel<ApiKeyFormValue>('form', { required: true })
|
||||
const title = computed(() => props.editing ? t('keys.actions.editKey') : t('keys.filters.createApiKey'))
|
||||
const formRef = useTemplateRef<InstanceType<typeof ZForm>>('formElement')
|
||||
const rules = useApiKeyFormRules()
|
||||
|
||||
watch(open, () => formRef.value?.clearValidate())
|
||||
|
||||
async function requestSave() {
|
||||
if (props.saving || !await formRef.value?.validate().catch(() => false))
|
||||
return
|
||||
emit('save')
|
||||
}
|
||||
|
||||
function updateForm<Key extends keyof ApiKeyFormValue>(key: Key, value: ApiKeyFormValue[Key]) {
|
||||
form.value = { ...form.value, [key]: value }
|
||||
@@ -47,8 +58,8 @@ function updateForm<Key extends keyof ApiKeyFormValue>(key: Key, value: ApiKeyFo
|
||||
<KeyRound class="text-cp-text" :size="20" aria-hidden="true" />
|
||||
</template>
|
||||
|
||||
<ZForm class="grid gap-6">
|
||||
<ZFormItem :label="t('common.fields.name')" required>
|
||||
<ZForm ref="formElement" :model="form" :rules="rules" scroll-to-error focus-on-error novalidate class="grid gap-6" @submit="requestSave">
|
||||
<ZFormItem prop="name" :label="t('common.fields.name')">
|
||||
<ZInput
|
||||
:model-value="form.name" :aria-label="t('common.fields.name')"
|
||||
:placeholder="t('keys.editor.namePlaceholder')"
|
||||
@@ -69,6 +80,7 @@ function updateForm<Key extends keyof ApiKeyFormValue>(key: Key, value: ApiKeyFo
|
||||
|
||||
<ZFormItem
|
||||
v-if="!editing"
|
||||
prop="customKey"
|
||||
:label="t('keys.editor.customKeyOptional')"
|
||||
>
|
||||
<ZInput
|
||||
@@ -102,7 +114,7 @@ function updateForm<Key extends keyof ApiKeyFormValue>(key: Key, value: ApiKeyFo
|
||||
</ZFormItem>
|
||||
|
||||
<div class="grid gap-6 sm:grid-cols-2">
|
||||
<ZFormItem :label="t('keys.editor.dailyLimit')">
|
||||
<ZFormItem prop="dailyLimitUsd" :label="t('keys.editor.dailyLimit')">
|
||||
<ZInput
|
||||
:model-value="form.dailyLimitUsd" type="number"
|
||||
min="0"
|
||||
@@ -117,7 +129,7 @@ function updateForm<Key extends keyof ApiKeyFormValue>(key: Key, value: ApiKeyFo
|
||||
</template>
|
||||
</ZInput>
|
||||
</ZFormItem>
|
||||
<ZFormItem :label="t('keys.editor.weeklyLimit')">
|
||||
<ZFormItem prop="weeklyLimitUsd" :label="t('keys.editor.weeklyLimit')">
|
||||
<ZInput
|
||||
:model-value="form.weeklyLimitUsd" type="number"
|
||||
min="0"
|
||||
@@ -135,7 +147,7 @@ function updateForm<Key extends keyof ApiKeyFormValue>(key: Key, value: ApiKeyFo
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 sm:grid-cols-2">
|
||||
<ZFormItem :label="t('keys.editor.maximumConcurrency')">
|
||||
<ZFormItem prop="maxConcurrency" :label="t('keys.editor.maximumConcurrency')">
|
||||
<ZInput
|
||||
:model-value="form.maxConcurrency" type="number"
|
||||
:aria-label="t('keys.editor.maximumConcurrency')"
|
||||
@@ -146,7 +158,7 @@ function updateForm<Key extends keyof ApiKeyFormValue>(key: Key, value: ApiKeyFo
|
||||
@update:model-value="updateForm('maxConcurrency', $event)"
|
||||
/>
|
||||
</ZFormItem>
|
||||
<ZFormItem :label="t('keys.editor.requestsPerMinuteRpm')">
|
||||
<ZFormItem prop="requestsPerMinute" :label="t('keys.editor.requestsPerMinuteRpm')">
|
||||
<ZInput
|
||||
:model-value="form.requestsPerMinute" type="number"
|
||||
:aria-label="t('keys.editor.requestsPerMinute')"
|
||||
@@ -167,8 +179,7 @@ function updateForm<Key extends keyof ApiKeyFormValue>(key: Key, value: ApiKeyFo
|
||||
<ZButton
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
:disabled="!form.name.trim()"
|
||||
@click="emit('save')"
|
||||
@click="requestSave"
|
||||
>
|
||||
{{ editing ? t('common.actions.save') : t('common.actions.create') }}
|
||||
</ZButton>
|
||||
|
||||
@@ -53,7 +53,7 @@ export function useApiKeyEditor(options: {
|
||||
}
|
||||
|
||||
function requestSave() {
|
||||
if (!validateForm() || savingKey.value)
|
||||
if (savingKey.value)
|
||||
return
|
||||
if (form.value.groupIds.length === 0) {
|
||||
showAllAccountsConfirm.value = true
|
||||
@@ -68,7 +68,7 @@ export function useApiKeyEditor(options: {
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!validateForm() || savingKey.value)
|
||||
if (savingKey.value)
|
||||
return
|
||||
|
||||
await savingKeyAction.run(
|
||||
@@ -117,34 +117,6 @@ export function useApiKeyEditor(options: {
|
||||
)
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
for (const [label, value] of [[t('keys.editor.dailyLimit'), form.value.dailyLimitUsd], [t('keys.editor.weeklyLimit'), form.value.weeklyLimitUsd]]) {
|
||||
if (value.trim() && !/^\d{1,10}(?:\.\d{1,10})?$/.test(value.trim())) {
|
||||
ZNotification.warning({ message: t('keys.editor.validation.amount', { label }) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (!form.value.name.trim()) {
|
||||
ZNotification.warning({ message: t('keys.editor.enterApiKeyName') })
|
||||
return false
|
||||
}
|
||||
if (!editingKey.value && form.value.customKey && !/^[\x21-\x7E]+$/.test(form.value.customKey)) {
|
||||
ZNotification.warning({ message: t('keys.editor.invalidKey') })
|
||||
return false
|
||||
}
|
||||
for (const [label, value] of [
|
||||
[t('keys.editor.maximumConcurrency'), form.value.maxConcurrency],
|
||||
[t('keys.editor.requestsPerMinute'), form.value.requestsPerMinute],
|
||||
] as const) {
|
||||
const parsed = Number(value)
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
||||
ZNotification.warning({ message: t('keys.editor.validation.integer', { label }) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function clearCustomKey() {
|
||||
form.value.customKey = ''
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { FormRule, FormRules } from '@codex-proxy/ui'
|
||||
import type { ApiKeyFormValue } from './useApiKeyEditor'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export function useApiKeyFormRules() {
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
function amountRule(label: string): FormRule {
|
||||
return {
|
||||
trigger: ['blur', 'change'],
|
||||
transform: value => String(value ?? '').trim(),
|
||||
pattern: /^\d{1,10}(?:\.\d{1,10})?$/,
|
||||
message: t('keys.editor.validation.amount', { label }),
|
||||
}
|
||||
}
|
||||
|
||||
function integerRule(label: string): FormRule {
|
||||
return {
|
||||
trigger: ['blur', 'change'],
|
||||
type: 'integer',
|
||||
transform: Number,
|
||||
min: 0,
|
||||
max: Number.MAX_SAFE_INTEGER,
|
||||
message: t('keys.editor.validation.integer', { label }),
|
||||
}
|
||||
}
|
||||
|
||||
return computed<FormRules<ApiKeyFormValue>>(() => ({
|
||||
name: {
|
||||
required: true,
|
||||
whitespace: true,
|
||||
message: t('keys.editor.enterApiKeyName'),
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
customKey: {
|
||||
trigger: ['blur', 'change'],
|
||||
pattern: /^[\x21-\x7E]+$/,
|
||||
message: t('keys.editor.invalidKey'),
|
||||
},
|
||||
dailyLimitUsd: amountRule(t('keys.editor.dailyLimit')),
|
||||
weeklyLimitUsd: amountRule(t('keys.editor.weeklyLimit')),
|
||||
maxConcurrency: integerRule(t('keys.editor.maximumConcurrency')),
|
||||
requestsPerMinute: integerRule(t('keys.editor.requestsPerMinute')),
|
||||
}))
|
||||
}
|
||||
@@ -16,6 +16,7 @@ const probeEnabled = defineModel<boolean>('probeEnabled', { required: true })
|
||||
const probeModel = defineModel<string>('probeModel', { required: true })
|
||||
const adaptiveConcurrency = defineModel<boolean>('adaptiveConcurrency', { required: true })
|
||||
const adaptiveConcurrencyHintId = useId()
|
||||
const probeModelId = useId()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -98,6 +99,7 @@ const adaptiveConcurrencyHintId = useId()
|
||||
<ZFormItem
|
||||
:label="t('settings.autoDisable.recoveryProbeModel')"
|
||||
:description="t('settings.autoDisable.probeModelHelp')"
|
||||
:for="probeModelId"
|
||||
>
|
||||
<template #extra>
|
||||
<ZCheckbox
|
||||
@@ -107,6 +109,7 @@ const adaptiveConcurrencyHintId = useId()
|
||||
/>
|
||||
</template>
|
||||
<ZInput
|
||||
:id="probeModelId"
|
||||
v-model="probeModel"
|
||||
:disabled="!enabled || !probeEnabled"
|
||||
:aria-label="t('settings.autoDisable.recoveryProbeModel')"
|
||||
|
||||
@@ -19,6 +19,7 @@ const {
|
||||
testing,
|
||||
savingSchedule,
|
||||
loaded,
|
||||
hasChanges,
|
||||
verified,
|
||||
storage,
|
||||
schedule,
|
||||
@@ -28,6 +29,8 @@ const {
|
||||
saveSchedule,
|
||||
} = useBackupSettings()
|
||||
|
||||
defineExpose({ hasChanges })
|
||||
|
||||
const {
|
||||
records,
|
||||
page,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BackupSettingsView, UpdateBackupStoragePayload } from '@/api'
|
||||
import { isEqual } from 'es-toolkit'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
|
||||
import {
|
||||
@@ -39,6 +40,9 @@ export function useBackupSettings() {
|
||||
retentionDays: '7',
|
||||
retentionCount: '7',
|
||||
})
|
||||
const savedStorage = shallowRef({ ...storage.value })
|
||||
const savedSchedule = shallowRef({ ...schedule.value })
|
||||
const hasChanges = computed(() => loaded.value && (!isEqual(storage.value, savedStorage.value) || !isEqual(schedule.value, savedSchedule.value)))
|
||||
|
||||
async function load(): Promise<void> {
|
||||
if (loaded.value || busy.value)
|
||||
@@ -68,6 +72,7 @@ export function useBackupSettings() {
|
||||
prefix: data.prefix ?? 'backups/',
|
||||
forcePathStyle: data.forcePathStyle,
|
||||
}
|
||||
savedStorage.value = { ...storage.value }
|
||||
}
|
||||
|
||||
function applySchedule(data: BackupSettingsView): void {
|
||||
@@ -77,6 +82,7 @@ export function useBackupSettings() {
|
||||
retentionDays: String(data.retentionDays),
|
||||
retentionCount: String(data.retentionCount),
|
||||
}
|
||||
savedSchedule.value = { ...schedule.value }
|
||||
}
|
||||
|
||||
function storagePayload(): UpdateBackupStoragePayload {
|
||||
@@ -101,8 +107,10 @@ export function useBackupSettings() {
|
||||
const schedulePaused = storageChanged && schedule.value.scheduleEnabled && !data.scheduleEnabled
|
||||
applyStorage(data)
|
||||
// 仅存储版本变化时同步服务端暂停状态,普通保存不覆盖计划草稿。
|
||||
if (storageChanged)
|
||||
if (storageChanged) {
|
||||
schedule.value.scheduleEnabled = data.scheduleEnabled
|
||||
savedSchedule.value = { ...savedSchedule.value, scheduleEnabled: data.scheduleEnabled }
|
||||
}
|
||||
ZNotification.success({ message: schedulePaused ? t('settings.backup.storageSaved') : t('settings.backup.storageConfigurationSaved') })
|
||||
return true
|
||||
}
|
||||
@@ -167,6 +175,7 @@ export function useBackupSettings() {
|
||||
testing,
|
||||
savingSchedule,
|
||||
loaded,
|
||||
hasChanges,
|
||||
verified,
|
||||
storage,
|
||||
schedule,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { useConfirmDialog, useEventListener } from '@vueuse/core'
|
||||
import { onScopeDispose, shallowRef, watch } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
|
||||
export function useSettingsLeaveGuard(hasChanges: Readonly<Ref<boolean>>) {
|
||||
const showLeaveConfirm = shallowRef(false)
|
||||
const { reveal, confirm: confirmLeave, cancel: cancelLeave } = useConfirmDialog(showLeaveConfirm)
|
||||
let pending: Promise<boolean> | undefined
|
||||
|
||||
onBeforeRouteLeave((to) => {
|
||||
if (to.matched.some(record => record.path === '/settings') || !hasChanges.value)
|
||||
return true
|
||||
pending ??= reveal().then(result => !result.isCanceled).finally(() => {
|
||||
pending = undefined
|
||||
})
|
||||
return pending
|
||||
})
|
||||
|
||||
// 关闭按钮、遮罩与 Esc 都按继续编辑处理,不能留下未完成的路由导航
|
||||
watch(showLeaveConfirm, (open) => {
|
||||
if (!open)
|
||||
cancelLeave()
|
||||
})
|
||||
useEventListener(() => hasChanges.value ? window : undefined, 'beforeunload', (event: BeforeUnloadEvent) => {
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
})
|
||||
onScopeDispose(cancelLeave)
|
||||
|
||||
return { showLeaveConfirm, confirmLeave }
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ZButton, ZEmpty, ZIconButton, ZPageHeader, ZSegmented, ZSelect } from '@codex-proxy/ui'
|
||||
import { ZButton, ZConfirmDialog, ZEmpty, ZIconButton, ZPageHeader, ZSegmented, ZSelect } from '@codex-proxy/ui'
|
||||
|
||||
import { Save, Undo2 } from '@lucide/vue'
|
||||
|
||||
import { computed, nextTick, reactive, shallowRef, watch } from 'vue'
|
||||
import { computed, nextTick, reactive, shallowRef, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -22,6 +22,7 @@ import RotationStrategyCard from './components/RotationStrategyCard.vue'
|
||||
import SettingsAccessSection from './components/SettingsAccessSection.vue'
|
||||
import TokenRefreshCard from './components/TokenRefreshCard.vue'
|
||||
import { useSettingsForm } from './composables/useSettingsForm'
|
||||
import { useSettingsLeaveGuard } from './composables/useSettingsLeaveGuard'
|
||||
import { rotationOptions } from './constants'
|
||||
import PricingSection from './pricing/index.vue'
|
||||
|
||||
@@ -89,6 +90,9 @@ const {
|
||||
saveSettings,
|
||||
loadSettings,
|
||||
} = useSettingsForm()
|
||||
const backupSection = useTemplateRef<InstanceType<typeof SettingsBackupSection>>('backupSection')
|
||||
const hasUnsavedChanges = computed(() => hasChanges.value || !!backupSection.value?.hasChanges)
|
||||
const { showLeaveConfirm, confirmLeave } = useSettingsLeaveGuard(hasUnsavedChanges)
|
||||
|
||||
watch(() => !loading.value && route.hash === '#reset-credits', async (ready) => {
|
||||
if (ready) {
|
||||
@@ -231,7 +235,15 @@ watch(section, (value) => {
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<SettingsBackupSection v-if="visited.has('backup')" v-show="section === 'backup'" class="mt-5" :active="section === 'backup'" />
|
||||
<SettingsBackupSection v-if="visited.has('backup')" v-show="section === 'backup'" ref="backupSection" class="mt-5" :active="section === 'backup'" />
|
||||
<PricingSection v-if="visited.has('pricing')" v-show="section === 'pricing'" class="mt-5 min-h-0 flex-1" />
|
||||
<ZConfirmDialog
|
||||
v-model="showLeaveConfirm"
|
||||
:title="t('settings.page.unsavedChanges.title')"
|
||||
:description="t('settings.page.unsavedChanges.description')"
|
||||
:confirm-text="t('settings.page.unsavedChanges.leave')"
|
||||
:cancel-text="t('settings.page.unsavedChanges.stay')"
|
||||
@confirm="confirmLeave()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ZButton, ZInput, ZPageHeader, ZScrollbar, ZSegmented } from '@codex-proxy/ui'
|
||||
import { ZButton, ZInput, ZPageHeader, ZScrollbar, ZSegmented, ZSkeleton } from '@codex-proxy/ui'
|
||||
import { Check, Eye, Pencil, RotateCcw, Save, Search, Undo2 } from '@lucide/vue'
|
||||
|
||||
import { computed, shallowRef } from 'vue'
|
||||
import { computed, defineAsyncComponent, shallowRef } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -10,9 +10,15 @@ import { notification as ZNotification } from '@/utils/notification'
|
||||
|
||||
import { useThemeEditor } from '../composables/useThemeEditor'
|
||||
import ThemeComponentTokenPanel from './ThemeComponentTokenPanel.vue'
|
||||
import ThemeEditorPreview from './ThemeEditorPreview.vue'
|
||||
import ThemeGlobalTokenPanel from './ThemeGlobalTokenPanel.vue'
|
||||
|
||||
const ThemeEditorPreview = defineAsyncComponent({
|
||||
loader: () => import('./ThemeEditorPreview.vue'),
|
||||
loadingComponent: ZSkeleton,
|
||||
delay: 200,
|
||||
timeout: 30000,
|
||||
})
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
const mobilePreviewOpen = shallowRef(false)
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import type { ResolvedTheme, ThemeColorPreset, ThemeColorPresetId, ThemeMode, Th
|
||||
import type { ThemeEditorDraft, ThemeEditorGlobalCategory } from '../composables/useThemeEditor'
|
||||
import { ZSegmented } from '@codex-proxy/ui'
|
||||
|
||||
import { resolveTheme, THEME_COLOR_PRESETS } from '@codex-proxy/ui/theme'
|
||||
import { resolveThemeSeedTokens, THEME_COLOR_PRESETS } from '@codex-proxy/ui/theme'
|
||||
import { Check, ChevronDown, Laptop, Moon, Sun } from '@lucide/vue'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { uiLocale } from '@/i18n/ui'
|
||||
|
||||
@@ -41,6 +41,7 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
const category = defineModel<ThemeEditorGlobalCategory>('category', { required: true })
|
||||
const derivedOpen = shallowRef(false)
|
||||
|
||||
const localizedPresets = computed(() => THEME_COLOR_PRESETS.map(preset => ({ ...preset, ...uiLocale.value.theme[preset.id] })))
|
||||
const colorPresets = THEME_COLOR_PRESETS.map(preset => preset.seed)
|
||||
@@ -58,9 +59,9 @@ const presetSwatchesById = new Map(
|
||||
THEME_COLOR_PRESETS.map(preset => [
|
||||
preset.id,
|
||||
[
|
||||
resolveTheme('light', preset.id, preset.seed).seedTokens.colorBgBase,
|
||||
resolveThemeSeedTokens('light', preset.seed, undefined, preset.appearance?.light).colorBgBase,
|
||||
preset.seed,
|
||||
resolveTheme('dark', preset.id, preset.seed).seedTokens.colorBgBase,
|
||||
resolveThemeSeedTokens('dark', preset.seed, undefined, preset.appearance?.dark).colorBgBase,
|
||||
],
|
||||
]),
|
||||
)
|
||||
@@ -226,14 +227,19 @@ function presetSwatches(preset: ThemeColorPreset) {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<details v-if="derivedTokens.length > 0" class="group rounded-cp-lg bg-cp-bg-container shadow-cp-tertiary">
|
||||
<details
|
||||
v-if="derivedTokens.length > 0"
|
||||
:open="derivedOpen"
|
||||
class="group rounded-cp-lg bg-cp-bg-container shadow-cp-tertiary"
|
||||
@toggle="derivedOpen = ($event.target as HTMLDetailsElement).open"
|
||||
>
|
||||
<summary
|
||||
class="flex cursor-pointer list-none items-center justify-between gap-3 px-3 py-3 text-cp-sm font-bold text-cp-text"
|
||||
>
|
||||
{{ t('appearance.globalTokens.derivedVariablesMapAliasToken') }}
|
||||
<ChevronDown class="size-3.5 transition-transform duration-150 group-open:rotate-180" />
|
||||
</summary>
|
||||
<div class="grid gap-1 px-2 pb-2">
|
||||
<div v-if="derivedOpen" class="grid gap-1 px-2 pb-2">
|
||||
<div
|
||||
v-for="[name, value] in derivedTokens"
|
||||
:key="name"
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
requestActivityByBucket,
|
||||
zeroInactiveValues,
|
||||
} from '@/components/charts/timeSeriesGap'
|
||||
import { tooltipIndex, tooltipRows } from '@/components/charts/tooltip'
|
||||
import { chartTooltipContent, tooltipIndex, tooltipRows } from '@/components/charts/tooltip'
|
||||
import { useChartPalette } from '@/composables/useChartPalette'
|
||||
import { decimalDisplayNumber, formatLocalizedCompactNumber as formatCompactNumber, formatPercent, formatUsd } from '@/utils/format'
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
usageGapAwareLineSeries,
|
||||
usageLegend,
|
||||
usageTooltip,
|
||||
usageTooltipContent,
|
||||
usageTooltipItem,
|
||||
usageValueAxis,
|
||||
} from '../utils/chart'
|
||||
@@ -189,7 +188,7 @@ function formatTooltip(params: unknown) {
|
||||
const theme = palette.value
|
||||
|
||||
if (activeView.value === 'savings') {
|
||||
return usageTooltipContent(theme, point.label, [
|
||||
return chartTooltipContent(point.label, [
|
||||
usageTooltipItem(
|
||||
t('usage.cost.cacheSavings'),
|
||||
formatUsd(costAmount(point.cacheSavings, pointIndex)),
|
||||
@@ -199,7 +198,7 @@ function formatTooltip(params: unknown) {
|
||||
}
|
||||
|
||||
if (activeView.value === 'cache') {
|
||||
return usageTooltipContent(theme, point.label, [
|
||||
return chartTooltipContent(point.label, [
|
||||
usageTooltipItem(t('usage.cost.cacheTokenProportion'), formatPercent(point.cachedTokenRate), theme.normal),
|
||||
usageTooltipItem(t('usage.cost.hitRequestRate'), formatPercent(point.cacheHitRequestRate), theme.success),
|
||||
])
|
||||
@@ -219,7 +218,7 @@ function formatTooltip(params: unknown) {
|
||||
theme.textMuted,
|
||||
))
|
||||
}
|
||||
return usageTooltipContent(theme, point.label, lines)
|
||||
return chartTooltipContent(point.label, lines)
|
||||
}
|
||||
|
||||
function costAmount(value: string | null, pointIndex: number) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { use } from 'echarts/core'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BaseChart from '@/components/charts/BaseChart.vue'
|
||||
import { tooltipIndex, tooltipRows } from '@/components/charts/tooltip'
|
||||
import { chartTooltipContent, tooltipIndex, tooltipRows } from '@/components/charts/tooltip'
|
||||
import { healthEligibleRequestCount } from '@/components/usage/shared/health'
|
||||
import { useChartPalette } from '@/composables/useChartPalette'
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
usageGapAwareLineSeries,
|
||||
usageLegend,
|
||||
usageTooltip,
|
||||
usageTooltipContent,
|
||||
usageTooltipItem,
|
||||
usageValueAxis,
|
||||
} from '../utils/chart'
|
||||
@@ -149,7 +148,7 @@ function formatTooltip(params: unknown) {
|
||||
const successRate = serviceSuccessRate(point)
|
||||
const theme = palette.value
|
||||
|
||||
return usageTooltipContent(theme, point.label, [
|
||||
return chartTooltipContent(point.label, [
|
||||
usageTooltipItem(t('usage.health.requestVolume'), formatCompactNumber(requestCount(point)), theme.textSecondary),
|
||||
usageTooltipItem(t('common.status.success'), formatCompactNumber(point.successRequests), theme.success),
|
||||
usageTooltipItem(t('usage.health.point.serviceFailed'), formatCompactNumber(point.failedRequests), theme.danger),
|
||||
|
||||
@@ -8,7 +8,7 @@ import { computed, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BaseChart from '@/components/charts/BaseChart.vue'
|
||||
import { requestActivityByBucket, zeroInactiveValues } from '@/components/charts/timeSeriesGap'
|
||||
import { tooltipIndex, tooltipRows } from '@/components/charts/tooltip'
|
||||
import { chartTooltipContent, tooltipIndex, tooltipRows } from '@/components/charts/tooltip'
|
||||
import { useChartPalette } from '@/composables/useChartPalette'
|
||||
import { formatLocalizedCompactNumber as formatCompactNumber, formatDuration, formatPercent } from '@/utils/format'
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
usageGapAwareLineSeries,
|
||||
usageLegend,
|
||||
usageTooltip,
|
||||
usageTooltipContent,
|
||||
usageTooltipItem,
|
||||
usageValueAxis,
|
||||
} from '../utils/chart'
|
||||
@@ -206,7 +205,7 @@ function formatTooltip(params: unknown) {
|
||||
return ''
|
||||
|
||||
const theme = palette.value
|
||||
return usageTooltipContent(theme, selected.point.label, [
|
||||
return chartTooltipContent(selected.point.label, [
|
||||
usageTooltipItem(
|
||||
seriesLabels.value[0],
|
||||
formatSeriesValue(selected.first, 'first'),
|
||||
|
||||
@@ -93,20 +93,11 @@ export function usageTooltip(
|
||||
) {
|
||||
return {
|
||||
trigger: 'axis' as const,
|
||||
...chartTooltipStyle(theme, { axisPointer: true }),
|
||||
...chartTooltipStyle(theme, { axisPointer: true, fontWeight: 600 }),
|
||||
formatter,
|
||||
}
|
||||
}
|
||||
|
||||
export function usageTooltipContent(
|
||||
theme: UsageChartPalette,
|
||||
label: string,
|
||||
lines: string[],
|
||||
) {
|
||||
const title = escapeTooltip(label)
|
||||
return `<div style="margin:0 0 7px;padding:0 0 7px;color:${theme.textPrimary};font-family:var(--font-mono);font-size:11px;font-weight:750;line-height:1.2">${title}</div><div style="line-height:1.55">${lines.join('<br/>')}</div>`
|
||||
}
|
||||
|
||||
export function usageTooltipItem(label: string, value: string, color: string) {
|
||||
return `<span style="display:inline-block;width:7px;height:7px;margin-right:6px;border-radius:999px;background:${escapeTooltip(color)}"></span>${escapeTooltip(label)}: ${escapeTooltip(value)}`
|
||||
}
|
||||
|
||||
+1
-1
Submodule modules/plugins updated: af65e68333...df671028e9
+1
-1
Submodule modules/ui updated: c480b40b94...0692ca034b
+14
-13
@@ -1,19 +1,20 @@
|
||||
# v3.31.1
|
||||
# v3.31.2
|
||||
|
||||
## 插件与请求记录
|
||||
## 表单与设置
|
||||
|
||||
- 移除 `provider_parameters` 和 `installation_id` 专用构造入口,身份生成、持久化及关联字段改写统一由插件通过现有 `request` / `attempt` / `outbound` 链路完成;原生默认身份处理保留
|
||||
- 移除上游返回模型的采集、存储写入、接口字段和界面展示,保留请求模型、映射后模型以及原始上游响应透传
|
||||
- 本次不新增数据库迁移,旧请求记录沿用现有保留策略清理
|
||||
- API Key 名称、额度和并发等字段接入表单校验,错误持续显示在对应字段下方,提交失败时自动滚动并聚焦首个错误字段
|
||||
- 错误提示显示时保持表单高度稳定,修复恢复探测模型标签误触发旁边复选框的问题
|
||||
- 设置及备份配置存在未保存修改时,离开页面前提供确认,减少误操作造成的草稿丢失
|
||||
|
||||
## 界面优化
|
||||
## 交互与显示
|
||||
|
||||
- 更新 UI 组件库至 v0.6.1,增强排序箭头的方向区分,保持原有尺寸和表头文字颜色
|
||||
- 分页按钮随页码长度扩宽,按容器实际宽度调整可见页码;单行空间不足时隐藏每页条数选择框,保留翻页按钮与当前页码
|
||||
- 折叠侧栏的悬停提示延迟至约 800 毫秒显示,语言切换的加载状态移至右侧勾选位置,保留国旗
|
||||
- 统一使用统计、请求记录及英文 Analytics、Request log 文案,精简模型筛选输入并收窄模型列
|
||||
- 修复请求健康卡片加载时显示临时统计粒度的问题,统一账号额度时间窗口标签
|
||||
- 补齐移动侧栏的初始焦点、Tab 循环、Esc 关闭及焦点恢复,账号搜索补充可访问名称
|
||||
- 收紧没有正文的确认弹窗留白,未保存设置的离开按钮简化为“放弃并离开”
|
||||
- 账号状态与请求记录推理强度居中显示,收窄表格默认单元格左右内边距,精简活跃账号用量文案
|
||||
- 统一首页与用量图表提示中的时间字体和下方间距,日期选择浮层与输入框保留更清楚的间隔
|
||||
- 修复窗口重新激活时意外打开悬停浮层,以及紧凑行高下单行省略文本的纵向裁切
|
||||
|
||||
## 构建与部署
|
||||
## 主题配置
|
||||
|
||||
- 更新 Node.js 构建镜像以及 PostgreSQL、Redis 镜像固定版本,调整 CI 运行环境并修复 Ubuntu 26.04 下的备份测试探针
|
||||
- 减少进入主题配置时的同步计算,折叠的变量列表在展开后渲染,完整预览异步加载
|
||||
- 管理端升级至 UI 组件库 v0.7.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
version: "3.31.1"
|
||||
version: "3.31.2"
|
||||
|
||||
Reference in New Issue
Block a user