@@ -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
@@ -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>
|
||||
|
||||
@@ -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])),
|
||||
|
||||
Reference in New Issue
Block a user