mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf23658aaf | ||
|
|
bc4728332e | ||
|
|
2cb5cda077 | ||
|
|
9d90dedb81 | ||
|
|
6074cff68c | ||
|
|
6ab8e7d0a6 | ||
|
|
17f9ef8915 | ||
|
|
15e0a28bd0 |
@@ -631,25 +631,74 @@ impl ProviderQuota {
|
||||
/// 这里只解释跨 Provider 共享的窗口语义,绝不读取 Provider 私有 JSON。
|
||||
#[must_use]
|
||||
pub fn representative_used_percent(&self) -> Option<f64> {
|
||||
if self.limit_reached {
|
||||
return Some(100.0);
|
||||
}
|
||||
self.representative_used_window()
|
||||
.map(|(_, used_percent)| used_percent)
|
||||
}
|
||||
|
||||
/// 将已确认的账号级额度耗尽事实投影到展示窗口。
|
||||
///
|
||||
/// Provider 已指出具体触顶窗口时只归一化这些窗口;否则归一化 Dashboard
|
||||
/// 同样会选择的代表窗口,避免把多个独立额度窗口全部伪造成已用尽。
|
||||
pub fn apply_limit_reached_display(&mut self) {
|
||||
if !self.limit_reached {
|
||||
return;
|
||||
}
|
||||
let reached_windows = self
|
||||
.windows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, window)| window.limit_reached.then_some(index))
|
||||
.collect::<Vec<_>>();
|
||||
if !reached_windows.is_empty() {
|
||||
for index in reached_windows {
|
||||
self.windows[index].used_percent = Some(100.0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let representative = self
|
||||
.representative_used_window()
|
||||
.map(|(index, _)| index)
|
||||
.or_else(|| {
|
||||
self.windows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(index, window)| (quota_usage_priority(window), *index))
|
||||
.map(|(index, _)| index)
|
||||
});
|
||||
if let Some(index) = representative {
|
||||
self.windows[index].used_percent = Some(100.0);
|
||||
self.windows[index].limit_reached = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn representative_used_window(&self) -> Option<(usize, f64)> {
|
||||
self.windows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|window| {
|
||||
let (index, window) = window;
|
||||
let used_percent = window
|
||||
.used_percent
|
||||
.filter(|value| value.is_finite())
|
||||
.map(|value| value.clamp(0.0, 100.0))?;
|
||||
Some((quota_usage_priority(window), used_percent))
|
||||
Some((index, quota_usage_priority(window), used_percent))
|
||||
})
|
||||
.fold(None::<(u8, f64)>, |selected, candidate| match selected {
|
||||
Some(current)
|
||||
if current.0 < candidate.0
|
||||
|| (current.0 == candidate.0 && current.1 >= candidate.1) =>
|
||||
{
|
||||
.fold(
|
||||
None::<(usize, u8, f64)>,
|
||||
|selected, candidate| match selected {
|
||||
Some(current)
|
||||
}
|
||||
_ => Some(candidate),
|
||||
})
|
||||
.map(|(_, used_percent)| used_percent)
|
||||
if current.1 < candidate.1
|
||||
|| (current.1 == candidate.1 && current.2 >= candidate.2) =>
|
||||
{
|
||||
Some(current)
|
||||
}
|
||||
_ => Some(candidate),
|
||||
},
|
||||
)
|
||||
.map(|(index, _, used_percent)| (index, used_percent))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,50 @@ fn representative_quota_should_prefer_short_window_and_highest_usage() {
|
||||
assert_eq!(quota.representative_used_percent(), Some(45.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exhausted_quota_should_project_full_usage_to_only_the_representative_window() {
|
||||
let mut quota = ProviderQuota {
|
||||
observed_at: None,
|
||||
refresh_token_expires_at: None,
|
||||
windows: vec![
|
||||
quota_window("monthly", Some(2_592_000), Some(99.0)),
|
||||
quota_window("shortTerm", Some(604_800), Some(80.0)),
|
||||
quota_window("shortTerm", Some(18_000), Some(30.0)),
|
||||
quota_window("shortTerm", Some(14_400), Some(95.0)),
|
||||
],
|
||||
limit_reached: true,
|
||||
provider_data: None,
|
||||
};
|
||||
|
||||
assert_eq!(quota.representative_used_percent(), Some(100.0));
|
||||
quota.apply_limit_reached_display();
|
||||
|
||||
assert_eq!(quota.windows[0].used_percent, Some(99.0));
|
||||
assert_eq!(quota.windows[1].used_percent, Some(80.0));
|
||||
assert_eq!(quota.windows[2].used_percent, Some(30.0));
|
||||
assert_eq!(quota.windows[3].used_percent, Some(100.0));
|
||||
assert!(quota.windows[3].limit_reached);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exhausted_quota_should_preserve_the_provider_identified_reached_window() {
|
||||
let mut reached = quota_window("monthly", Some(2_592_000), Some(98.0));
|
||||
reached.limit_reached = true;
|
||||
let mut quota = ProviderQuota {
|
||||
observed_at: None,
|
||||
refresh_token_expires_at: None,
|
||||
windows: vec![reached, quota_window("shortTerm", Some(18_000), Some(95.0))],
|
||||
limit_reached: true,
|
||||
provider_data: None,
|
||||
};
|
||||
|
||||
quota.apply_limit_reached_display();
|
||||
|
||||
assert_eq!(quota.windows[0].used_percent, Some(100.0));
|
||||
assert_eq!(quota.windows[1].used_percent, Some(95.0));
|
||||
assert!(!quota.windows[1].limit_reached);
|
||||
}
|
||||
|
||||
fn quota_window(
|
||||
group: &str,
|
||||
window_seconds: Option<u64>,
|
||||
|
||||
@@ -1574,10 +1574,11 @@ fn account_view(item: AccountDirectoryItem, now: DateTime<Utc>) -> AccountView {
|
||||
}
|
||||
|
||||
fn account_quota_view(
|
||||
quota: ProviderQuota,
|
||||
mut quota: ProviderQuota,
|
||||
rate_limited_until: Option<DateTime<Utc>>,
|
||||
now: DateTime<Utc>,
|
||||
) -> (AccountQuotaView, Option<String>) {
|
||||
quota.apply_limit_reached_display();
|
||||
let refresh_token_expires_at = quota
|
||||
.refresh_token_expires_at
|
||||
.map(|value| china_rfc3339(&value));
|
||||
|
||||
@@ -52,9 +52,12 @@ pub(crate) async fn provider_account_usage(
|
||||
}
|
||||
statement.push(
|
||||
" group by pa.id, pa.provider_kind, pa.authentication_kind, pa.name, pa.email,
|
||||
pa.plan_type
|
||||
order by max(mr.started_at) desc nulls last, pa.name, pa.id limit ",
|
||||
pa.plan_type",
|
||||
);
|
||||
if query.account_ids.is_none() {
|
||||
statement.push(" having count(mr.id) > 0");
|
||||
}
|
||||
statement.push(" order by max(mr.started_at) desc nulls last, pa.name, pa.id limit ");
|
||||
statement.push_bind(i64::from(query.limit));
|
||||
let rows = statement
|
||||
.build()
|
||||
|
||||
@@ -730,8 +730,18 @@ async fn dashboard_summary_totals_include_history_outside_selected_range() {
|
||||
.await
|
||||
.expect("dashboard summary");
|
||||
assert_eq!(dashboard.requests.request_count, 2);
|
||||
assert_eq!(dashboard.account_usage[0].request_count, 0);
|
||||
assert_eq!(dashboard.account_usage[0].request_buckets.len(), 2);
|
||||
assert!(dashboard.account_usage.is_empty());
|
||||
|
||||
let explicit_account_usage = repository
|
||||
.provider_account_usage(
|
||||
ProviderAccountUsageQuery::for_accounts(range, vec!["acct_observe".to_owned()])
|
||||
.expect("account usage query"),
|
||||
)
|
||||
.await
|
||||
.expect("provider account usage");
|
||||
assert_eq!(explicit_account_usage.len(), 1);
|
||||
assert_eq!(explicit_account_usage[0].request_count, 0);
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
dashboard.totals.request_count,
|
||||
|
||||
@@ -10,6 +10,7 @@ const props = withDefaults(
|
||||
variant?: ButtonVariant
|
||||
size?: ButtonSize
|
||||
loading?: boolean
|
||||
spinIconOnLoading?: boolean
|
||||
disabled?: boolean
|
||||
active?: boolean
|
||||
iconOnly?: boolean
|
||||
@@ -21,6 +22,7 @@ const props = withDefaults(
|
||||
variant: 'primary',
|
||||
size: 'default',
|
||||
loading: false,
|
||||
spinIconOnLoading: false,
|
||||
disabled: false,
|
||||
active: false,
|
||||
iconOnly: false,
|
||||
@@ -80,6 +82,7 @@ const loadingIndicatorStyle = computed(() => {
|
||||
const classes = computed(() => [
|
||||
'inline-flex items-center justify-center border-0 font-bold leading-[1.15] transition-[background-color,box-shadow,color,opacity,transform] duration-150 cursor-pointer outline-none motion-safe:active:translate-y-px motion-safe:active:scale-[0.985]',
|
||||
props.iconOnly ? iconOnlySizeClasses[props.size] : sizeClasses[props.size],
|
||||
props.iconOnly ? 'shrink-0' : undefined,
|
||||
props.iconOnly ? '' : 'rounded-(--cp-button-radius-base)',
|
||||
'focus-visible:ring-2 focus-visible:ring-(--cp-info-border) focus-visible:ring-offset-2 focus-visible:ring-offset-(--cp-bg-surface)',
|
||||
'disabled:cursor-not-allowed disabled:transform-none disabled:bg-(--cp-disabled-bg) disabled:text-(--cp-disabled-text) disabled:shadow-none',
|
||||
@@ -124,7 +127,7 @@ const loadingIconClasses = computed(() => [
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<span
|
||||
v-if="loading"
|
||||
v-if="loading && !spinIconOnLoading"
|
||||
:class="loadingIconClasses"
|
||||
:style="loadingIndicatorStyle"
|
||||
aria-hidden="true"
|
||||
@@ -140,7 +143,7 @@ const loadingIconClasses = computed(() => [
|
||||
<slot />
|
||||
</span>
|
||||
<span
|
||||
v-if="iconOnly && !$slots.icon && !loading"
|
||||
v-if="iconOnly && !$slots.icon && (!loading || spinIconOnLoading)"
|
||||
:class="loadingIconClasses"
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
||||
@@ -27,10 +27,12 @@ export function usePagedQuery<Result extends PageResult>(options: {
|
||||
const error = shallowRef('')
|
||||
let requestSequence = 0
|
||||
|
||||
async function execute() {
|
||||
async function execute(execution: { silent?: boolean } = {}) {
|
||||
const requestId = ++requestSequence
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
if (!execution.silent) {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await options.load({
|
||||
@@ -42,7 +44,7 @@ export function usePagedQuery<Result extends PageResult>(options: {
|
||||
|
||||
if (result.items.length === 0 && result.page.total > 0 && result.page.page > 1) {
|
||||
page.value = clamp(result.page.totalPages, 1, Number.POSITIVE_INFINITY)
|
||||
return execute()
|
||||
return execute(execution)
|
||||
}
|
||||
|
||||
items.value = result.items
|
||||
@@ -55,8 +57,10 @@ export function usePagedQuery<Result extends PageResult>(options: {
|
||||
catch (cause: unknown) {
|
||||
if (requestId !== requestSequence)
|
||||
return false
|
||||
error.value = errorMessage(cause, '加载失败')
|
||||
options.onError?.(cause)
|
||||
if (!execution.silent) {
|
||||
error.value = errorMessage(cause, '加载失败')
|
||||
options.onError?.(cause)
|
||||
}
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -91,8 +91,16 @@ const statusView = computed(() => {
|
||||
iconClass: 'text-(--cp-success)',
|
||||
}
|
||||
}
|
||||
if (updateInfo.value) {
|
||||
return {
|
||||
label: '已是最新',
|
||||
icon: CheckCircle2,
|
||||
badge: 'bg-(--cp-success-bg) text-(--cp-success-text)',
|
||||
iconClass: 'text-(--cp-success)',
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: updateInfo.value ? '已是最新' : '未检查',
|
||||
label: '未检查',
|
||||
icon: CheckCircle2,
|
||||
badge: 'bg-(--cp-bg-muted) text-(--cp-text-secondary)',
|
||||
iconClass: 'text-(--cp-text-muted)',
|
||||
@@ -318,6 +326,7 @@ watch(
|
||||
<BaseModal
|
||||
v-model="open"
|
||||
title="系统更新"
|
||||
description="检查版本、查看发布说明并执行在线更新"
|
||||
variant="success"
|
||||
width="820px"
|
||||
body-max-height="72dvh"
|
||||
|
||||
@@ -66,8 +66,8 @@ export const accountGroupColumns = [
|
||||
{
|
||||
key: 'actions',
|
||||
label: '操作',
|
||||
width: '100px',
|
||||
minWidth: '100px',
|
||||
width: '136px',
|
||||
minWidth: '136px',
|
||||
cellClass,
|
||||
},
|
||||
] satisfies BaseTableColumn<AccountGroup>[]
|
||||
|
||||
@@ -45,6 +45,7 @@ const quotaWindows = computed(() => orderedPanelQuotaWindows(props.account.quota
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="刷新额度"
|
||||
spin-icon-on-loading
|
||||
:loading="refreshing"
|
||||
:disabled="refreshing"
|
||||
@click="emit('refreshQuota', account.id)"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccountRow } from '../constants'
|
||||
import { modelSuccessRateTextClass } from '../constants'
|
||||
|
||||
defineProps<{
|
||||
account: AccountRow
|
||||
@@ -62,7 +63,7 @@ defineProps<{
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-[1.5fr_0.7fr_0.8fr_1fr_1fr_1fr_1fr_1fr_1.4fr] gap-3 pb-2 text-[11px] font-heavy text-(--cp-text-secondary) shadow-[inset_0_-1px_0_var(--cp-divider-subtle)]"
|
||||
class="grid grid-cols-[1.5fr_0.7fr_0.8fr_1fr_1fr_1fr_1fr_1fr_1.2fr] gap-3 pb-2 text-[11px] font-heavy text-(--cp-text-secondary) shadow-[inset_0_-1px_0_var(--cp-divider-subtle)]"
|
||||
>
|
||||
<span>模型</span>
|
||||
<span>调用</span>
|
||||
@@ -84,11 +85,13 @@ defineProps<{
|
||||
<div
|
||||
v-for="model in account.usage.models"
|
||||
:key="model.model"
|
||||
class="grid grid-cols-[1.5fr_0.7fr_0.8fr_1fr_1fr_1fr_1fr_1fr_1.4fr] gap-3 pt-3 text-[12px] font-emphasis text-(--cp-text-primary)"
|
||||
class="grid grid-cols-[1.5fr_0.7fr_0.8fr_1fr_1fr_1fr_1fr_1fr_1.2fr] gap-3 pt-3 text-[12px] font-emphasis text-(--cp-text-primary)"
|
||||
>
|
||||
<span class="truncate">{{ model.model }}</span>
|
||||
<span>{{ model.requestCountDisplay }}</span>
|
||||
<span class="text-(--cp-warning-text)">{{ model.successRateDisplay }}</span>
|
||||
<span :class="modelSuccessRateTextClass(model.successRate)">
|
||||
{{ model.successRateDisplay }}
|
||||
</span>
|
||||
<span>{{ model.inputTokensDisplay }}</span>
|
||||
<span>{{ model.outputTokensDisplay }}</span>
|
||||
<span>{{ model.cachedTokensDisplay }}</span>
|
||||
|
||||
@@ -141,6 +141,7 @@ export function useAccountsQuery() {
|
||||
loading: query.loading,
|
||||
accounts: query.items,
|
||||
loadAccounts: query.execute,
|
||||
refreshAccountsSilently: () => query.execute({ silent: true }),
|
||||
searchQuery,
|
||||
providerQuery,
|
||||
statusQuery,
|
||||
|
||||
@@ -121,8 +121,8 @@ export const accountColumns = [
|
||||
{
|
||||
key: 'actions',
|
||||
label: '操作',
|
||||
width: '120px',
|
||||
minWidth: '120px',
|
||||
width: '136px',
|
||||
minWidth: '136px',
|
||||
},
|
||||
] satisfies BaseTableColumn<AccountRow>[]
|
||||
|
||||
@@ -211,6 +211,18 @@ export function quotaWindowPercentTextClass(window: AccountQuotaWindow) {
|
||||
return 'text-(--cp-success-text)'
|
||||
}
|
||||
|
||||
export function modelSuccessRateTextClass(successRate: number | null) {
|
||||
if (successRate === null)
|
||||
return 'text-(--cp-text-muted)'
|
||||
if (successRate >= 99.5)
|
||||
return 'text-(--cp-success-text)'
|
||||
if (successRate >= 98)
|
||||
return 'text-(--cp-normal-text)'
|
||||
if (successRate >= 95)
|
||||
return 'text-(--cp-warning-text)'
|
||||
return 'text-(--cp-danger-text)'
|
||||
}
|
||||
|
||||
function compareQuotaWindows(left: AccountQuotaWindow, right: AccountQuotaWindow) {
|
||||
const groupDifference = groupOrder(left) - groupOrder(right)
|
||||
if (groupDifference !== 0)
|
||||
|
||||
@@ -37,6 +37,7 @@ const {
|
||||
loading,
|
||||
accounts,
|
||||
loadAccounts,
|
||||
refreshAccountsSilently,
|
||||
searchQuery,
|
||||
providerQuery,
|
||||
statusQuery,
|
||||
@@ -108,7 +109,7 @@ const {
|
||||
openConnectionTest,
|
||||
handleRefreshConnectionTestModels,
|
||||
handleTestConnection,
|
||||
} = useAccountConnectionTest({ reload: loadAccounts })
|
||||
} = useAccountConnectionTest({ reload: refreshAccountsSilently })
|
||||
|
||||
const {
|
||||
expandedAccountIds,
|
||||
@@ -262,9 +263,9 @@ const {
|
||||
</template>
|
||||
|
||||
<template #groups="{ row }">
|
||||
<AccountGroupMarks
|
||||
:groups="row.groups"
|
||||
/>
|
||||
<div class="flex w-full justify-center">
|
||||
<AccountGroupMarks :groups="row.groups" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #lastUsedAt="{ row }">
|
||||
|
||||
@@ -9,8 +9,8 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-1.5">
|
||||
<div v-if="apiKey.routingScope === 'all'" class="flex items-center gap-2">
|
||||
<div class="grid w-full justify-items-center gap-1.5">
|
||||
<div v-if="apiKey.routingScope === 'all'" class="flex items-center justify-center gap-2">
|
||||
<span class="inline-flex h-6 items-center rounded-lg bg-(--cp-warning-bg) px-2 text-[11px] font-bold text-(--cp-warning-text)">
|
||||
全部账号权限
|
||||
</span>
|
||||
|
||||
@@ -43,8 +43,8 @@ export const apiKeyColumns = [
|
||||
{
|
||||
key: 'actions',
|
||||
label: '操作',
|
||||
width: '172px',
|
||||
minWidth: '172px',
|
||||
width: '208px',
|
||||
minWidth: '208px',
|
||||
headerClass: relaxedActionCellClass,
|
||||
cellClass: relaxedActionCellClass,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { dashboardSnapshotView, MetricTone } from '../composables/presenter'
|
||||
import { CircleCheck, CircleOff, RefreshCw, ShieldAlert, TriangleAlert } from '@lucide/vue'
|
||||
import { CircleCheck, RefreshCw, ShieldAlert, TriangleAlert } from '@lucide/vue'
|
||||
import { clamp } from 'es-toolkit'
|
||||
|
||||
import { computed } from 'vue'
|
||||
@@ -106,8 +106,7 @@ const statusRows = computed(() => {
|
||||
{ label: '正常', description: '暂无账号池观测', value: '—', tone: 'success', icon: CircleCheck },
|
||||
{ label: '配额耗尽', description: '暂无账号池观测', value: '—', tone: 'warning', icon: TriangleAlert },
|
||||
{ label: '限流中', description: '暂无账号池观测', value: '—', tone: 'normal', icon: RefreshCw },
|
||||
{ label: '已停用', description: '暂无账号池观测', value: '—', tone: 'normal', icon: CircleOff },
|
||||
{ label: '错误', description: '暂无账号池观测', value: '—', tone: 'danger', icon: ShieldAlert },
|
||||
{ label: '待处理', description: '停用 — · 错误 —', value: '—', tone: 'danger', icon: ShieldAlert },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -134,16 +133,9 @@ const statusRows = computed(() => {
|
||||
icon: RefreshCw,
|
||||
},
|
||||
{
|
||||
label: '已停用',
|
||||
description: '账号已停用',
|
||||
value: String(counts.disabled),
|
||||
tone: 'normal',
|
||||
icon: CircleOff,
|
||||
},
|
||||
{
|
||||
label: '错误',
|
||||
description: '账号状态异常',
|
||||
value: String(counts.error),
|
||||
label: '待处理',
|
||||
description: `停用 ${counts.disabled} · 错误 ${counts.error}`,
|
||||
value: String(counts.disabled + counts.error),
|
||||
tone: 'danger',
|
||||
icon: ShieldAlert,
|
||||
},
|
||||
@@ -165,8 +157,7 @@ const statusBars = computed(() => {
|
||||
{ pct: (counts.normal / counts.total) * 100, cls: 'bg-(--cp-success)' },
|
||||
{ pct: (counts.quotaExhausted / counts.total) * 100, cls: 'bg-(--cp-warning)' },
|
||||
{ pct: (counts.rateLimited / counts.total) * 100, cls: 'bg-(--cp-normal)' },
|
||||
{ pct: (counts.disabled / counts.total) * 100, cls: 'bg-(--cp-text-secondary)' },
|
||||
{ pct: (counts.error / counts.total) * 100, cls: 'bg-(--cp-danger)' },
|
||||
{ pct: ((counts.disabled + counts.error) / counts.total) * 100, cls: 'bg-(--cp-danger)' },
|
||||
].filter(b => b.pct > 0)
|
||||
})
|
||||
</script>
|
||||
@@ -333,11 +324,11 @@ const statusBars = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid h-67 w-full grid-rows-5 gap-2">
|
||||
<div class="mt-6.5 grid h-65.5 w-full gap-2.5">
|
||||
<div
|
||||
v-for="row in statusRows"
|
||||
:key="row.label"
|
||||
class="grid min-h-0 grid-cols-[28px_14px_minmax(0,1fr)_76px] items-center rounded-[14px] bg-(--cp-bg-subtle) px-3.5"
|
||||
class="grid h-14.5 grid-cols-[28px_14px_minmax(0,1fr)_76px] items-center rounded-[14px] bg-(--cp-bg-subtle) px-3.5"
|
||||
>
|
||||
<span
|
||||
class="inline-flex size-7 items-center justify-center rounded-[9px]"
|
||||
|
||||
@@ -46,6 +46,7 @@ const {
|
||||
class="text-(--cp-normal)"
|
||||
size="md"
|
||||
label="刷新概览"
|
||||
spin-icon-on-loading
|
||||
:loading="loading || refreshing"
|
||||
:disabled="loading || refreshing"
|
||||
@click="refresh"
|
||||
|
||||
@@ -45,7 +45,7 @@ const columns: BaseTableColumn<BackupRecord>[] = [
|
||||
{ key: 'expiresAt', label: '过期时间', width: 170 },
|
||||
{ key: 'trigger', label: '触发方式', width: 100 },
|
||||
{ key: 'startedAt', label: '开始时间', width: 170 },
|
||||
{ key: 'actions', label: '操作', width: 96, fixed: 'right' },
|
||||
{ key: 'actions', label: '操作', width: 112, minWidth: 112, fixed: 'right' },
|
||||
]
|
||||
|
||||
function triggerLabel(value: string): string {
|
||||
|
||||
@@ -69,6 +69,7 @@ function showDetail(record: Awaited<ReturnType<typeof getOpsErrors>>['items'][nu
|
||||
variant="ghost"
|
||||
size="md"
|
||||
label="刷新错误明细"
|
||||
spin-icon-on-loading
|
||||
:loading="refreshing"
|
||||
:disabled="loading || refreshing"
|
||||
@click="refresh"
|
||||
|
||||
@@ -32,6 +32,7 @@ const search = defineModel<string>('search', { required: true })
|
||||
variant="ghost"
|
||||
size="md"
|
||||
label="刷新使用记录"
|
||||
spin-icon-on-loading
|
||||
:loading="refreshing"
|
||||
:disabled="loading || refreshing"
|
||||
@click="emit('refresh')"
|
||||
|
||||
@@ -1 +1 @@
|
||||
version: "3.2.1"
|
||||
version: "3.2.3"
|
||||
|
||||
Reference in New Issue
Block a user