修复全量审查问题;接入vitest;精简IdP图标逻辑
CI / test (push) Successful in 46s

This commit is contained in:
2026-07-22 16:51:45 +08:00
parent efcb84eaa8
commit 119f60516c
39 changed files with 1164 additions and 797 deletions
+11
View File
@@ -50,6 +50,14 @@ const currencySymbol = computed(() => {
const currency = ov.value?.cost.currency
return currency === 'EUR' ? '€' : currency === 'GBP' ? '£' : '$'
})
/** 主币种之外的其余币种合计;跨币种金额不可相加,只并列展示 */
const otherCurrencyTotals = computed(() =>
(ov.value?.cost.series ?? [])
.slice(1)
.map((s) => `${s.currency} ${s.total.toFixed(2)}`)
.join('、'),
)
</script>
<template>
@@ -126,6 +134,9 @@ const currencySymbol = computed(() => {
<div class="text-[12.5px] text-ink-3">
{{ costDaily.labels[0] }} {{ costDaily.labels.at(-1) }} 合计
</div>
<div v-if="otherCurrencyTotals" class="text-[12.5px] text-ink-3">
另有 {{ otherCurrencyTotals }}跨币种不相加
</div>
</div>
<div class="px-3 pb-2">
<CostChart :labels="costDaily.labels" :values="costDaily.values" />
+24 -12
View File
@@ -42,12 +42,15 @@ const auth = useAuthStore()
// 设置页以 tab 组织:通知(管理 + 方式)/ 任务(抢机熔断阈值)/ 安全(WAF/网络/账号)
const tab = ref('notify')
// OAuth 绑定回跳:成功/失败提示后清理 query,并停在安全 tab;
// 绑定使令牌版本递增,fragment 携带的新 token 须先落地,否则后续请求 401
// OAuth 绑定回跳:绑定使令牌版本递增,fragment 携带的新 token 必须在本组件
// 任何请求发起前(setup 顶部)落地——setup 阶段的 useAsync 会立即用旧 token
// 发请求,迟到的 401 会误清刚写入的新会话
consumeBindToken()
// 成功/失败提示后清理 query,并停在安全 tab
onMounted(() => {
const { oauth, oauthError } = route.query
if (oauth !== 'bound' && !oauthError) return
consumeBindToken()
tab.value = 'security'
if (oauth === 'bound') message.success('外部身份绑定成功')
if (typeof oauthError === 'string' && oauthError) message.error(oauthError)
@@ -244,7 +247,7 @@ function pickHeader(v: string) {
customHeaderMode.value = false
if (securityForm.value.realIpHeader === v) return
securityForm.value.realIpHeader = v
void saveSecurity()
void saveSecurity({ realIpHeader: v })
}
function pickCustomHeader() {
@@ -265,18 +268,27 @@ watch(
},
)
/** 控件变更即全量 PUT(与 AI 设置一致);失败重拉服务端值回滚 UI */
async function saveSecurity() {
/** 控件变更即发字段补丁,服务端只落库出现字段(并发编辑互不回滚);
* 失败重拉服务端值回滚 UI。在途时把补丁并入尾随一轮,完成后合并补发。 */
let pendingSecurityPatch: Partial<SecuritySetting> | null = null
async function saveSecurity(patch: Partial<SecuritySetting>) {
if (savingSecurity.value) {
pendingSecurityPatch = { ...pendingSecurityPatch, ...patch }
return
}
savingSecurity.value = true
try {
await updateSecuritySetting({ ...securityForm.value })
await updateSecuritySetting(patch)
message.success('已保存,立即生效')
void security.run({ silent: true })
if (!pendingSecurityPatch) void security.run({ silent: true })
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败')
void security.run({ silent: true })
if (!pendingSecurityPatch) void security.run({ silent: true })
} finally {
savingSecurity.value = false
const next = pendingSecurityPatch
pendingSecurityPatch = null
if (next) void saveSecurity(next)
}
}
@@ -287,7 +299,7 @@ function commitSecurityNum(
) {
if (v == null || v === securityForm.value[key]) return
securityForm.value[key] = v
void saveSecurity()
void saveSecurity({ [key]: v })
}
async function saveTaskSetting() {
@@ -676,7 +688,7 @@ async function sendTest() {
v-model:value="securityForm.realIpHeader"
class="mono mt-2 !w-70"
placeholder="自定义头名,如 X-Client-IP"
@change="() => void saveSecurity()"
@change="() => void saveSecurity({ realIpHeader: securityForm.realIpHeader })"
/>
<div v-if="customHeaderMode" class="mt-1.5 text-xs text-ink-3">
头名仅限字母数字与连字符
@@ -689,7 +701,7 @@ async function sendTest() {
<NInput
v-model:value="securityForm.appUrl"
placeholder="https://demo.example.com"
@change="() => void saveSecurity()"
@change="() => void saveSecurity({ appUrl: securityForm.appUrl })"
/>
</FormField>
</div>
+13 -3
View File
@@ -20,9 +20,19 @@ const tasks = useAsync(listTasks)
const showCreate = ref(false)
const editingTask = ref<Task | null>(null)
// 每 5 秒静默刷新不闪 loading
const refreshTimer = setInterval(() => void tasks.run({ silent: true }), 5000)
onUnmounted(() => clearInterval(refreshTimer))
// 每 5 秒静默刷新(不闪 loading);上一轮完成后再排下一轮,
// 接口变慢时不堆叠请求,也避免响应总被下一轮标记过期而饿死
let refreshTimer = 0
let refreshStopped = false
async function refreshLoop() {
await tasks.run({ silent: true })
if (!refreshStopped) refreshTimer = window.setTimeout(() => void refreshLoop(), 5000)
}
refreshTimer = window.setTimeout(() => void refreshLoop(), 5000)
onUnmounted(() => {
refreshStopped = true
clearTimeout(refreshTimer)
})
const cfgAliasById = computed(
() => new Map((scope.configs.data ?? []).map((c) => [c.id, c.alias])),