feat(subscription): 优化凭据导入与账号卡片 (#457)
* feat(subscription): 优化凭据导入与账号卡片 * feat(subscription): 调整额度色彩与加载骨架 * feat(subscription): 优化额度色彩与 JSON 入口 * feat(subscription): 优化额度条样式为淡底 + 左强调 + 底部进度线 将额度条从「整行 34px 铺色」改为「淡色背景 + 3px 语义左竖条 + 3px 行底进度线」, 左竖条改用 inset 阴影避免与进度线的接缝断口。强调色采用在 sRGB 色域内的高饱和值, 同一值同时适配明暗主题。 * fix(subscription): 防止窄卡片操作裁切 * fix(subscription): 优化额度窗口排序 * fix(subscription): 对齐刷新骨架窗口数量 * fix(subscription): 移除重复重置卡到期信息
This commit is contained in:
1 parent
7f64cbbd9e
commit
dd7e7204e2
12 files changed
+623
-370
No files matched your search
@@ -1,8 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
summary: string
|
||||
open?: boolean
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
summary: string
|
||||
open?: boolean
|
||||
indicatorPosition?: 'end' | 'inline'
|
||||
}>(),
|
||||
{ indicatorPosition: 'end' },
|
||||
)
|
||||
const emit = defineEmits<{ 'update:open': [open: boolean] }>()
|
||||
|
||||
function handleToggle(event: Event): void {
|
||||
@@ -12,7 +16,21 @@ function handleToggle(event: Event): void {
|
||||
|
||||
<template>
|
||||
<details class="disclosure-panel" :open="open" @toggle="handleToggle">
|
||||
<summary>{{ summary }}</summary>
|
||||
<summary>
|
||||
<span class="disclosure-panel__summary-content">
|
||||
<slot name="summary">{{ summary }}</slot>
|
||||
<span
|
||||
v-if="props.indicatorPosition === 'inline'"
|
||||
class="disclosure-panel__summary-indicator"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</span>
|
||||
<span
|
||||
v-if="props.indicatorPosition === 'end'"
|
||||
class="disclosure-panel__summary-indicator"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</summary>
|
||||
<div class="disclosure-panel__body">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -27,30 +45,81 @@ function handleToggle(event: Event): void {
|
||||
|
||||
.disclosure-panel summary {
|
||||
display: flex;
|
||||
min-height: var(--control-sm);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
border-radius: var(--radius-control);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
list-style: none;
|
||||
padding: 5px 8px;
|
||||
transition:
|
||||
background-color var(--duration-fast) var(--easing-standard),
|
||||
color var(--duration-fast) var(--easing-standard);
|
||||
}
|
||||
|
||||
.disclosure-panel summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.disclosure-panel summary::after {
|
||||
color: var(--color-text-faint);
|
||||
content: '+';
|
||||
.disclosure-panel summary:hover {
|
||||
background: var(--color-surface-sunken);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.disclosure-panel[open] summary::after {
|
||||
content: '−';
|
||||
.disclosure-panel summary:focus-visible {
|
||||
outline: 2px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.disclosure-panel__summary-content {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.disclosure-panel__summary-indicator {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
color: var(--color-text-faint);
|
||||
}
|
||||
|
||||
.disclosure-panel__summary-indicator::before,
|
||||
.disclosure-panel__summary-indicator::after {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 1.5px;
|
||||
border-radius: 2px;
|
||||
background: currentColor;
|
||||
content: '';
|
||||
transition: transform var(--duration-fast) var(--easing-standard);
|
||||
}
|
||||
|
||||
.disclosure-panel__summary-indicator::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.disclosure-panel[open] .disclosure-panel__summary-indicator::after {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.disclosure-panel__body {
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.disclosure-panel summary,
|
||||
.disclosure-panel__summary-indicator::before,
|
||||
.disclosure-panel__summary-indicator::after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -132,10 +132,6 @@ const connectOperationKey = ref<string>()
|
||||
const connectFeedback = ref('')
|
||||
const copyControllers = useAbortControllerPool()
|
||||
const searchDebounce = useDebouncedAction(250)
|
||||
const connectionWorkspaceDescription = computed(() =>
|
||||
t('group.credentials.subscription.connectDescription'),
|
||||
)
|
||||
|
||||
const collection = computed(() => credentialsQuery.data.value)
|
||||
const {
|
||||
initial: initialLoading,
|
||||
@@ -1206,8 +1202,7 @@ async function runBatch(
|
||||
appearance="ledger"
|
||||
:open="connectionWorkspaceOpen"
|
||||
:title="t('group.credentials.subscription.connect')"
|
||||
:description="connectionWorkspaceDescription"
|
||||
show-description
|
||||
:description="t('group.credentials.subscription.connectDescription')"
|
||||
:close-label="t('common.close')"
|
||||
:dismissible="!connectBusy"
|
||||
@update:open="setConnectionWorkspace"
|
||||
@@ -1231,12 +1226,14 @@ async function runBatch(
|
||||
<template #footer>
|
||||
<AppButton
|
||||
variant="secondary"
|
||||
size="compact"
|
||||
:disabled="connectBusy"
|
||||
@click="setConnectionWorkspace(false)"
|
||||
>
|
||||
{{ t('group.credentials.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
size="compact"
|
||||
:busy="connectBusy"
|
||||
:disabled="readyConnectionStages.length === 0"
|
||||
@click="saveConnectedAccounts"
|
||||
|
||||
@@ -27,7 +27,6 @@ import AppRelativeTime from '@/components/ui/AppRelativeTime.vue'
|
||||
import AppTooltip from '@/components/ui/AppTooltip.vue'
|
||||
import IconButton from '@/components/ui/IconButton.vue'
|
||||
import OverflowTooltip from '@/components/ui/OverflowTooltip.vue'
|
||||
import QuotaProgressBar from '@/components/ui/QuotaProgressBar.vue'
|
||||
import SkeletonBlock from '@/components/ui/SkeletonBlock.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import { formatEstimatedCost, formatLocalInstant, formatTokens } from '@/lib/format'
|
||||
@@ -97,14 +96,61 @@ const supportsResetCredit = computed(() =>
|
||||
props.capabilities.credential_actions.includes('reset_credit'),
|
||||
)
|
||||
const snapshot = computed(() => observation.value?.snapshot)
|
||||
// 最快恢复的额度窗口排在前面;缺少时长的上游窗口保持在最后。
|
||||
const quotaWindows = computed(() =>
|
||||
[...(snapshot.value?.quota_windows ?? [])].sort((left, right) => {
|
||||
const leftSeconds = left.window_seconds ?? Number.MAX_SAFE_INTEGER
|
||||
const rightSeconds = right.window_seconds ?? Number.MAX_SAFE_INTEGER
|
||||
return leftSeconds - rightSeconds
|
||||
}),
|
||||
)
|
||||
function isAccountWideQuotaWindow(window: CredentialQuotaWindowDto): boolean {
|
||||
return window.scope === 'account'
|
||||
}
|
||||
|
||||
function quotaWindowDuration(window: CredentialQuotaWindowDto): number {
|
||||
const seconds = window.window_seconds
|
||||
return seconds !== undefined && Number.isFinite(seconds) && seconds > 0
|
||||
? seconds
|
||||
: Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
|
||||
function quotaWindowGroupKey(window: CredentialQuotaWindowDto): string {
|
||||
const modelIDs = (window.model_ids ?? [])
|
||||
.map((model) => model.trim())
|
||||
.filter((model) => model !== '')
|
||||
.sort()
|
||||
if (modelIDs.length > 0) return `models:${modelIDs.join('\u0000')}`
|
||||
|
||||
const period = quotaWindowPeriodLabel(window.window_seconds)
|
||||
const labelParts = window.label
|
||||
.split('·')
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part !== '')
|
||||
const finalPart = labelParts.at(-1)
|
||||
if (
|
||||
period &&
|
||||
labelParts.length > 1 &&
|
||||
finalPart !== undefined &&
|
||||
normalizedQuotaLabelPart(finalPart) === normalizedQuotaLabelPart(period)
|
||||
) {
|
||||
const subject = normalizedQuotaLabelPart(labelParts.slice(0, -1).join(' '))
|
||||
if (subject) return `subject:${subject}`
|
||||
}
|
||||
return `scope:${normalizedQuotaLabelPart(window.scope) || window.id}`
|
||||
}
|
||||
|
||||
// 呈现层统一排序:账号全局窗口优先;其余按同一模型/专属窗口成组,组内按时长升序。
|
||||
const quotaWindows = computed(() => {
|
||||
const windows = snapshot.value?.quota_windows ?? []
|
||||
const groupOrder = new Map<string, number>()
|
||||
for (const [index, window] of windows.entries()) {
|
||||
const key = quotaWindowGroupKey(window)
|
||||
if (!groupOrder.has(key)) groupOrder.set(key, index)
|
||||
}
|
||||
return [...windows].sort((left, right) => {
|
||||
const scopeDifference =
|
||||
Number(!isAccountWideQuotaWindow(left)) - Number(!isAccountWideQuotaWindow(right))
|
||||
if (scopeDifference !== 0) return scopeDifference
|
||||
const groupDifference =
|
||||
(groupOrder.get(quotaWindowGroupKey(left)) ?? 0) -
|
||||
(groupOrder.get(quotaWindowGroupKey(right)) ?? 0)
|
||||
if (groupDifference !== 0) return groupDifference
|
||||
return quotaWindowDuration(left) - quotaWindowDuration(right)
|
||||
})
|
||||
})
|
||||
const accountQuotaWindows = computed(() =>
|
||||
quotaWindows.value.filter((window) => window.scope === 'account'),
|
||||
)
|
||||
@@ -113,7 +159,7 @@ const usageQuotaWindows = computed(() =>
|
||||
)
|
||||
const hasUsageQuotaWindows = computed(() => usageQuotaWindows.value.length > 0)
|
||||
const windowSkeletonHeight = computed(() => `${24 + usageQuotaWindows.value.length * 32}px`)
|
||||
const refreshSkeletonRows = computed(() => Math.max(2, Math.min(4, quotaWindows.value.length || 2)))
|
||||
const refreshSkeletonRows = computed(() => Math.min(4, quotaWindows.value.length))
|
||||
const constrainedModels = computed(() =>
|
||||
Array.from(new Set(quotaWindows.value.flatMap((window) => window.model_ids ?? []))),
|
||||
)
|
||||
@@ -152,6 +198,8 @@ const syncExactTimeTooltip = computed(() => {
|
||||
if (observedAtMS === undefined || observedAtMS === null) return undefined
|
||||
return formatLocalInstant(observedAtMS, locale.value)
|
||||
})
|
||||
const quotaResetPrefix = computed(() => t('group.credentials.subscription.quotaResetPrefix'))
|
||||
const quotaResetSuffix = computed(() => t('group.credentials.subscription.quotaResetSuffix'))
|
||||
const resetCreditsAvailable = computed(() => snapshot.value?.reset_credits_available ?? 0)
|
||||
const resetCredits = computed(() => snapshot.value?.reset_credits ?? [])
|
||||
const availableResetCreditDetails = computed(() =>
|
||||
@@ -464,6 +512,11 @@ function quotaTone(window: CredentialQuotaWindowDto): 'success' | 'warning' | 'd
|
||||
return quotaProgressTone(value, window.state === 'exhausted')
|
||||
}
|
||||
|
||||
function quotaFillStyle(window: CredentialQuotaWindowDto): Record<string, string> {
|
||||
const value = remainingPercent(window)
|
||||
return value === undefined ? {} : { width: `${value}%` }
|
||||
}
|
||||
|
||||
function toggleDetails(): void {
|
||||
if (props.detailBusy) return
|
||||
detailsExpanded.value = !detailsExpanded.value
|
||||
@@ -515,23 +568,93 @@ function runMenuAction(
|
||||
aria-live="polite"
|
||||
>
|
||||
<span class="sr-only">{{ t('group.credentials.subscription.syncingQuota') }}</span>
|
||||
<div class="subscription-account__refresh-skeleton-top" aria-hidden="true">
|
||||
<div>
|
||||
<SkeletonBlock width="92px" height="24px" />
|
||||
<SkeletonBlock width="64px" height="24px" />
|
||||
<div class="subscription-account__refresh-skeleton-header">
|
||||
<div class="subscription-account__refresh-skeleton-top" aria-hidden="true">
|
||||
<div>
|
||||
<SkeletonBlock width="20px" height="20px" />
|
||||
<SkeletonBlock width="92px" height="24px" />
|
||||
<SkeletonBlock width="64px" height="24px" />
|
||||
</div>
|
||||
<div>
|
||||
<SkeletonBlock width="58px" height="12px" />
|
||||
<SkeletonBlock width="32px" height="32px" />
|
||||
<SkeletonBlock width="32px" height="32px" />
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonBlock width="32px" height="32px" />
|
||||
<SkeletonBlock width="62%" height="20px" aria-hidden="true" />
|
||||
</div>
|
||||
<SkeletonBlock width="58%" height="18px" aria-hidden="true" />
|
||||
<div class="subscription-account__refresh-skeleton-quotas" aria-hidden="true">
|
||||
<div v-for="index in refreshSkeletonRows" :key="index">
|
||||
<SkeletonBlock width="76px" height="12px" />
|
||||
<SkeletonBlock height="8px" />
|
||||
<SkeletonBlock width="54px" height="12px" />
|
||||
<div
|
||||
v-if="supportsQuotaObservation && quotaWindows.length"
|
||||
class="subscription-account__refresh-skeleton-quotas"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
v-for="index in refreshSkeletonRows"
|
||||
:key="index"
|
||||
class="subscription-account__refresh-skeleton-quota"
|
||||
>
|
||||
<SkeletonBlock width="44%" height="13px" />
|
||||
<span>
|
||||
<SkeletonBlock width="64px" height="12px" />
|
||||
<SkeletonBlock width="82px" height="12px" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonBlock
|
||||
v-else-if="supportsQuotaObservation"
|
||||
class="subscription-account__refresh-skeleton-empty-quota"
|
||||
width="48%"
|
||||
height="16px"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<SkeletonBlock
|
||||
v-if="unifiedStatus === 'quota_exhausted'"
|
||||
class="subscription-account__refresh-skeleton-hint"
|
||||
width="82%"
|
||||
height="16px"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
v-if="hasResetCredits"
|
||||
class="subscription-account__refresh-skeleton-credits"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<SkeletonBlock width="52px" height="14px" />
|
||||
<SkeletonBlock width="72px" height="14px" />
|
||||
<SkeletonBlock
|
||||
v-if="nearestResetCredit && nearestResetCredit.expires_at_ms !== undefined"
|
||||
class="subscription-account__refresh-skeleton-credits-expiry"
|
||||
width="68px"
|
||||
height="14px"
|
||||
/>
|
||||
<SkeletonBlock width="26px" height="26px" />
|
||||
</div>
|
||||
<div class="subscription-account__refresh-skeleton-detail-control" aria-hidden="true">
|
||||
<span></span>
|
||||
<SkeletonBlock width="30px" height="30px" />
|
||||
<span></span>
|
||||
</div>
|
||||
<div
|
||||
v-if="detailsExpanded"
|
||||
class="subscription-account__refresh-skeleton-detail"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
v-if="supportsQuotaObservation && hasUsageQuotaWindows"
|
||||
class="subscription-account__skeleton-section"
|
||||
>
|
||||
<span class="subscription-account__skeleton-title">
|
||||
<SkeletonBlock width="140px" height="11px" />
|
||||
</span>
|
||||
<SkeletonBlock :height="windowSkeletonHeight" />
|
||||
</div>
|
||||
<div class="subscription-account__skeleton-section">
|
||||
<span class="subscription-account__skeleton-title">
|
||||
<SkeletonBlock width="88px" height="11px" />
|
||||
</span>
|
||||
<SkeletonBlock height="var(--subscription-detail-overview-height)" />
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonBlock height="38px" aria-hidden="true" />
|
||||
<SkeletonBlock width="100%" height="44px" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="subscription-account__main">
|
||||
<header class="subscription-account__top">
|
||||
@@ -574,6 +697,18 @@ function runMenuAction(
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<div class="subscription-account__actions">
|
||||
<span
|
||||
v-if="supportsQuotaObservation && observation?.observed_at_ms != null"
|
||||
class="subscription-account__sync-age"
|
||||
>
|
||||
<AppRelativeTime
|
||||
:instant="observation.observed_at_ms"
|
||||
:locale="locale"
|
||||
:empty-label="t('group.credentials.subscription.unknown')"
|
||||
:tooltip-content="syncTimeTooltip"
|
||||
hint
|
||||
/>
|
||||
</span>
|
||||
<AppTooltip
|
||||
v-if="supportsQuotaObservation"
|
||||
:content="t('group.credentials.subscription.sync')"
|
||||
@@ -662,18 +797,6 @@ function runMenuAction(
|
||||
<OverflowTooltip class="subscription-account__mail" :content="accountName">
|
||||
{{ accountName }}
|
||||
</OverflowTooltip>
|
||||
<span
|
||||
v-if="supportsQuotaObservation && observation?.observed_at_ms != null"
|
||||
class="subscription-account__sync-age"
|
||||
>
|
||||
<AppRelativeTime
|
||||
:instant="observation.observed_at_ms"
|
||||
:locale="locale"
|
||||
:empty-label="t('group.credentials.subscription.unknown')"
|
||||
:tooltip-content="syncTimeTooltip"
|
||||
hint
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -688,38 +811,66 @@ function runMenuAction(
|
||||
v-if="supportsQuotaObservation && quotaWindows.length"
|
||||
class="subscription-account__quotas"
|
||||
>
|
||||
<div v-for="window in quotaWindows" :key="window.id" class="subscription-account__quota">
|
||||
<div
|
||||
v-for="window in quotaWindows"
|
||||
:key="window.id"
|
||||
class="subscription-account__quota"
|
||||
:class="`subscription-account__quota--${quotaTone(window) ?? 'unknown'}`"
|
||||
>
|
||||
<span
|
||||
class="subscription-account__quota-meter"
|
||||
:role="remainingPercent(window) === undefined ? 'img' : 'progressbar'"
|
||||
:aria-label="
|
||||
remainingPercent(window) === undefined
|
||||
? `${quotaWindowLabel(window)}: ${quotaValueLabel(window)}`
|
||||
: quotaWindowLabel(window)
|
||||
"
|
||||
:aria-valuemin="remainingPercent(window) === undefined ? undefined : 0"
|
||||
:aria-valuemax="remainingPercent(window) === undefined ? undefined : 100"
|
||||
:aria-valuenow="remainingPercent(window)"
|
||||
:aria-valuetext="
|
||||
remainingPercent(window) === undefined ? undefined : quotaValueLabel(window)
|
||||
"
|
||||
>
|
||||
<span
|
||||
v-if="remainingPercent(window) !== undefined"
|
||||
class="subscription-account__quota-fill"
|
||||
:style="quotaFillStyle(window)"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</span>
|
||||
<OverflowTooltip
|
||||
class="subscription-account__quota-name"
|
||||
:content="quotaWindowLabel(window)"
|
||||
>
|
||||
{{ quotaWindowLabel(window) }}
|
||||
</OverflowTooltip>
|
||||
<QuotaProgressBar
|
||||
:value="remainingPercent(window)"
|
||||
:tone="quotaTone(window)"
|
||||
:label="quotaWindowLabel(window)"
|
||||
:value-text="quotaValueLabel(window)"
|
||||
/>
|
||||
<span class="subscription-account__quota-value">{{ quotaValueLabel(window) }}</span>
|
||||
<span class="subscription-account__quota-reset">
|
||||
<AppTooltip
|
||||
v-if="window.reset_at_ms && quotaWindowNeedsRefresh(window)"
|
||||
:content="quotaPeriodTooltip(window) ?? ''"
|
||||
>
|
||||
<span class="subscription-account__quota-reset-pending" tabindex="0">
|
||||
{{ t('group.credentials.subscription.quotaPendingRefresh') }}
|
||||
<span class="subscription-account__quota-meta">
|
||||
<strong>{{ quotaValueLabel(window) }}</strong>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span class="subscription-account__quota-reset">
|
||||
<AppTooltip
|
||||
v-if="window.reset_at_ms && quotaWindowNeedsRefresh(window)"
|
||||
:content="quotaPeriodTooltip(window) ?? ''"
|
||||
>
|
||||
<span class="subscription-account__quota-reset-pending" tabindex="0">
|
||||
{{ t('group.credentials.subscription.quotaPendingRefresh') }}
|
||||
</span>
|
||||
</AppTooltip>
|
||||
<span v-else-if="window.reset_at_ms" class="subscription-account__quota-reset-time">
|
||||
<span v-if="quotaResetPrefix" class="subscription-account__quota-reset-prefix">
|
||||
{{ quotaResetPrefix }}
|
||||
</span>
|
||||
<AppRelativeTime
|
||||
:instant="window.reset_at_ms"
|
||||
:locale="locale"
|
||||
:empty-label="t('group.credentials.subscription.unknown')"
|
||||
:tooltip-content="quotaPeriodTooltip(window)"
|
||||
hint
|
||||
/><span v-if="quotaResetSuffix">{{ quotaResetSuffix }}</span>
|
||||
</span>
|
||||
</AppTooltip>
|
||||
<AppRelativeTime
|
||||
v-else-if="window.reset_at_ms"
|
||||
:instant="window.reset_at_ms"
|
||||
:locale="locale"
|
||||
:empty-label="t('group.credentials.subscription.unknown')"
|
||||
:tooltip-content="quotaPeriodTooltip(window)"
|
||||
hint
|
||||
/>
|
||||
<template v-else>—</template>
|
||||
<template v-else>—</template>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -842,12 +993,6 @@ function runMenuAction(
|
||||
aria-live="polite"
|
||||
>
|
||||
<div v-if="!detailLoaded && !detailError" class="subscription-account__skeleton">
|
||||
<div class="subscription-account__skeleton-section">
|
||||
<span class="subscription-account__skeleton-title">
|
||||
<SkeletonBlock width="88px" height="11px" />
|
||||
</span>
|
||||
<SkeletonBlock height="var(--subscription-detail-activity-height)" />
|
||||
</div>
|
||||
<div
|
||||
v-if="supportsQuotaObservation && hasUsageQuotaWindows"
|
||||
class="subscription-account__skeleton-section"
|
||||
@@ -861,7 +1006,7 @@ function runMenuAction(
|
||||
<span class="subscription-account__skeleton-title">
|
||||
<SkeletonBlock width="88px" height="11px" />
|
||||
</span>
|
||||
<SkeletonBlock height="var(--subscription-detail-diagnostics-height)" />
|
||||
<SkeletonBlock height="var(--subscription-detail-overview-height)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -873,41 +1018,6 @@ function runMenuAction(
|
||||
</div>
|
||||
|
||||
<div v-else class="subscription-account__detail-content">
|
||||
<section class="subscription-account__detail-section">
|
||||
<h3>{{ t('group.credentials.subscription.activity') }}</h3>
|
||||
<div class="subscription-account__activity">
|
||||
<span class="subscription-account__activity-last">
|
||||
{{ t('group.credentials.subscription.lastUsed') }}
|
||||
<AppRelativeTime
|
||||
:instant="item.last_used_at_ms ?? null"
|
||||
:locale="locale"
|
||||
:empty-label="t('group.credentials.subscription.unknown')"
|
||||
hint
|
||||
/>
|
||||
</span>
|
||||
<span v-if="dailyUsage" class="subscription-account__activity-daily">
|
||||
<span class="subscription-account__activity-window">
|
||||
{{ t('group.credentials.subscription.dailyWindow') }}
|
||||
</span>
|
||||
<span :title="dailyIncompleteHint">
|
||||
{{ t('group.credentials.subscription.dailySuccess') }}
|
||||
<strong class="subscription-account__daily-success">{{
|
||||
n(dailyUsage.success_count)
|
||||
}}</strong>
|
||||
</span>
|
||||
<span :title="dailyIncompleteHint">
|
||||
{{ t('group.credentials.subscription.dailyFailure') }}
|
||||
<strong class="subscription-account__daily-failure">{{
|
||||
n(dailyUsage.failure_count)
|
||||
}}</strong>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="subscription-account__activity-daily">
|
||||
{{ t('group.credentials.subscription.estimate.unavailable') }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="supportsQuotaObservation && hasUsageQuotaWindows"
|
||||
class="subscription-account__detail-section"
|
||||
@@ -990,23 +1100,56 @@ function runMenuAction(
|
||||
</section>
|
||||
|
||||
<section class="subscription-account__detail-section">
|
||||
<h3>{{ t('group.credentials.subscription.diagnostics') }}</h3>
|
||||
<h3>{{ t('group.credentials.subscription.overview') }}</h3>
|
||||
<div class="subscription-account__diagnostics">
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.subscription.lastUsed') }}</dt>
|
||||
<dd>
|
||||
<AppRelativeTime
|
||||
:instant="item.last_used_at_ms ?? null"
|
||||
:locale="locale"
|
||||
:empty-label="t('group.credentials.subscription.unknown')"
|
||||
hint
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.subscription.dailySuccessSummary') }}</dt>
|
||||
<dd
|
||||
:class="{ 'subscription-account__daily-success': dailyUsage }"
|
||||
:title="dailyIncompleteHint"
|
||||
>
|
||||
{{
|
||||
dailyUsage
|
||||
? n(dailyUsage.success_count)
|
||||
: t('group.credentials.subscription.estimate.unavailable')
|
||||
}}
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.subscription.dailyFailureSummary') }}</dt>
|
||||
<dd
|
||||
:class="{ 'subscription-account__daily-failure': dailyUsage }"
|
||||
:title="dailyIncompleteHint"
|
||||
>
|
||||
{{
|
||||
dailyUsage
|
||||
? n(dailyUsage.failure_count)
|
||||
: t('group.credentials.subscription.estimate.unavailable')
|
||||
}}
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.detailsFailure') }}</dt>
|
||||
<dd>{{ failureLabel }}</dd>
|
||||
</dl>
|
||||
<dl v-if="supportsQuotaObservation">
|
||||
<dt>{{ t('group.credentials.subscription.lastQuotaSync') }}</dt>
|
||||
<dd>
|
||||
<AppRelativeTime
|
||||
:instant="observation?.observed_at_ms ?? null"
|
||||
:locale="locale"
|
||||
:empty-label="t('group.credentials.subscription.unknown')"
|
||||
:tooltip-content="syncExactTimeTooltip"
|
||||
hint
|
||||
/>
|
||||
</dd>
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.detailsConsecutive') }}</dt>
|
||||
<dd>{{ n(item.consecutive_failure_count) }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.subscription.lastError') }}</dt>
|
||||
<dd>{{ observationErrorLabel(observation?.last_error_code) }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.subscription.lastTokenRefresh') }}</dt>
|
||||
@@ -1019,14 +1162,6 @@ function runMenuAction(
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.detailsConsecutive') }}</dt>
|
||||
<dd>{{ n(item.consecutive_failure_count) }}</dd>
|
||||
</dl>
|
||||
<dl v-if="supportsQuotaObservation">
|
||||
<dt>{{ t('group.credentials.subscription.lastError') }}</dt>
|
||||
<dd>{{ observationErrorLabel(observation?.last_error_code) }}</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.subscription.tokenExpiresAt') }}</dt>
|
||||
<dd>
|
||||
@@ -1039,6 +1174,18 @@ function runMenuAction(
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>{{ t('group.credentials.subscription.lastQuotaSync') }}</dt>
|
||||
<dd>
|
||||
<AppRelativeTime
|
||||
:instant="observation?.observed_at_ms ?? null"
|
||||
:locale="locale"
|
||||
:empty-label="t('group.credentials.subscription.unknown')"
|
||||
:tooltip-content="syncExactTimeTooltip"
|
||||
hint
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div
|
||||
v-if="supportsQuotaObservation && constrainedModels.length"
|
||||
@@ -1047,24 +1194,6 @@ function runMenuAction(
|
||||
<span>{{ t('group.credentials.subscription.modelConstraints') }}</span>
|
||||
<code v-for="model in constrainedModels" :key="model">{{ model }}</code>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasResetCredits && availableResetCreditDetails.length"
|
||||
class="subscription-account__reset-credit-list"
|
||||
>
|
||||
<span>{{ t('group.credentials.subscription.resetCreditExpirations') }}</span>
|
||||
<template v-for="(credit, index) in availableResetCreditDetails" :key="index">
|
||||
<span v-if="credit.expires_at_ms !== undefined && credit.expires_at_ms <= nowMs">
|
||||
{{ t('group.credentials.subscription.resetCreditExpired') }}
|
||||
</span>
|
||||
<AppRelativeTime
|
||||
v-else
|
||||
:instant="credit.expires_at_ms ?? null"
|
||||
:locale="locale"
|
||||
:empty-label="t('group.credentials.subscription.resetCreditPermanent')"
|
||||
hint
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1073,6 +1202,7 @@ function runMenuAction(
|
||||
|
||||
<style scoped>
|
||||
.subscription-account {
|
||||
container-type: inline-size;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--color-action) 24%, var(--color-border-subtle));
|
||||
@@ -1088,6 +1218,7 @@ function runMenuAction(
|
||||
visibility: hidden;
|
||||
}
|
||||
.subscription-account__refresh-skeleton {
|
||||
--subscription-account-refresh-inline: var(--space-4);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset: 0;
|
||||
@@ -1095,7 +1226,12 @@ function runMenuAction(
|
||||
align-content: start;
|
||||
gap: var(--space-3);
|
||||
background: color-mix(in srgb, var(--color-action-soft) 46%, var(--color-surface));
|
||||
padding: var(--space-3) var(--space-4);
|
||||
padding: var(--space-3) var(--subscription-account-refresh-inline);
|
||||
}
|
||||
.subscription-account__refresh-skeleton-header {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
.subscription-account__refresh-skeleton-top {
|
||||
display: flex;
|
||||
@@ -1113,11 +1249,60 @@ function runMenuAction(
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.subscription-account__refresh-skeleton-quotas > div {
|
||||
.subscription-account__refresh-skeleton-quota {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(72px, 98px) minmax(0, 1fr) 54px;
|
||||
min-height: 34px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface-sunken);
|
||||
padding: 7px 10px;
|
||||
}
|
||||
.subscription-account__refresh-skeleton-quota > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.subscription-account__refresh-skeleton-empty-quota,
|
||||
.subscription-account__refresh-skeleton-hint {
|
||||
align-self: center;
|
||||
}
|
||||
.subscription-account__refresh-skeleton-credits {
|
||||
display: flex;
|
||||
min-height: var(--control-compact);
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-surface);
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.subscription-account__refresh-skeleton-credits-expiry {
|
||||
margin-left: var(--space-1);
|
||||
}
|
||||
.subscription-account__refresh-skeleton-credits > :last-child {
|
||||
margin-left: auto;
|
||||
flex: none;
|
||||
}
|
||||
.subscription-account__refresh-skeleton-detail-control {
|
||||
display: grid;
|
||||
height: 44px;
|
||||
grid-template-columns: 1fr 44px 1fr;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.subscription-account__refresh-skeleton-detail-control > span {
|
||||
height: 1px;
|
||||
background: var(--color-border-subtle);
|
||||
}
|
||||
.subscription-account__refresh-skeleton-detail {
|
||||
display: grid;
|
||||
gap: 13px;
|
||||
margin: calc(-1 * var(--space-3)) calc(-1 * var(--subscription-account-refresh-inline));
|
||||
border-top: 1px solid color-mix(in srgb, var(--color-action) 18%, var(--color-border-subtle));
|
||||
background: color-mix(in srgb, var(--color-action-soft) 72%, var(--color-surface));
|
||||
padding: 14px 18px 16px;
|
||||
}
|
||||
.subscription-account--success {
|
||||
border-left-color: var(--color-success);
|
||||
@@ -1136,9 +1321,9 @@ function runMenuAction(
|
||||
.subscription-account--disabled .subscription-account__status {
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-neutral) 28%, transparent);
|
||||
}
|
||||
.subscription-account--disabled :deep(.quota-progress) {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.58;
|
||||
.subscription-account--disabled .subscription-account__quota {
|
||||
--quota-accent: var(--color-neutral);
|
||||
--quota-tint: var(--color-surface-sunken);
|
||||
}
|
||||
.subscription-account__main {
|
||||
display: grid;
|
||||
@@ -1217,7 +1402,7 @@ function runMenuAction(
|
||||
}
|
||||
.subscription-account__sync-age {
|
||||
flex: none;
|
||||
margin-left: auto;
|
||||
margin-right: var(--space-1);
|
||||
color: var(--color-text-faint);
|
||||
font-size: var(--text-label-xs);
|
||||
white-space: nowrap;
|
||||
@@ -1279,42 +1464,97 @@ function runMenuAction(
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.subscription-account__quota {
|
||||
/* 强调色用于左竖条与行底进度线,单值同时适配明暗;淡底只做状态提示 */
|
||||
--quota-accent: var(--color-border-control);
|
||||
--quota-tint: var(--color-surface-sunken);
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(72px, 98px) minmax(0, 1fr) 88px 56px;
|
||||
min-height: 34px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
row-gap: var(--space-2);
|
||||
column-gap: var(--space-1);
|
||||
gap: var(--space-3);
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: var(--quota-tint);
|
||||
/* 左竖条用 inset 阴影而非 border:border 会把 inset:0 的进度线整体右推 3px,
|
||||
导致左下圆角处出现断口。阴影不占盒模型,细线可贯通到最左侧与竖条重叠。 */
|
||||
box-shadow: inset 3px 0 0 var(--quota-accent);
|
||||
padding: 7px 10px 7px 13px;
|
||||
}
|
||||
.subscription-account__quota--success {
|
||||
--quota-accent: oklch(70% 0.16 158);
|
||||
--quota-tint: light-dark(#dcfeea, #112b21);
|
||||
}
|
||||
.subscription-account__quota--warning {
|
||||
--quota-accent: oklch(75% 0.152 75);
|
||||
--quota-tint: light-dark(#fff2e2, #302212);
|
||||
}
|
||||
.subscription-account__quota--danger {
|
||||
--quota-accent: oklch(65% 0.2 22);
|
||||
--quota-tint: light-dark(#fef0f0, #371a1d);
|
||||
}
|
||||
.subscription-account__quota-meter {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
}
|
||||
.subscription-account--disabled .subscription-account__quota-meter {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.58;
|
||||
}
|
||||
.subscription-account__quota-fill {
|
||||
position: absolute;
|
||||
inset: auto auto 0 0;
|
||||
height: 3px;
|
||||
background: var(--quota-accent);
|
||||
transition: width var(--duration-fast) var(--easing-standard);
|
||||
}
|
||||
.subscription-account__quota-name {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--color-text-muted);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.subscription-account__quota-value {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding-left: var(--space-1);
|
||||
font-family: var(--font-mono);
|
||||
.subscription-account__quota-meta {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
min-width: max-content;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 650;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.subscription-account__quota-meta strong {
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 650;
|
||||
}
|
||||
.subscription-account__quota-reset {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--color-text-faint);
|
||||
font-size: var(--text-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: ellipsis;
|
||||
text-align: right;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--color-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.subscription-account__quota-reset-time {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.subscription-account__quota-reset-prefix {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
.subscription-account__quota-reset-pending {
|
||||
cursor: help;
|
||||
text-decoration: underline dotted;
|
||||
@@ -1506,8 +1746,7 @@ function runMenuAction(
|
||||
padding: 14px 18px 16px;
|
||||
}
|
||||
.subscription-account__skeleton {
|
||||
--subscription-detail-activity-height: 34px;
|
||||
--subscription-detail-diagnostics-height: 61px;
|
||||
--subscription-detail-overview-height: 92px;
|
||||
}
|
||||
.subscription-account__skeleton,
|
||||
.subscription-account__detail-content {
|
||||
@@ -1533,44 +1772,6 @@ function runMenuAction(
|
||||
font-weight: 680;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.subscription-account__activity {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: baseline;
|
||||
gap: var(--space-3);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-surface);
|
||||
padding: 7px 10px;
|
||||
}
|
||||
.subscription-account__activity-last {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.subscription-account__activity-last :deep(.app-relative-time) {
|
||||
margin-left: var(--space-1);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.subscription-account__activity-daily {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 9px;
|
||||
color: var(--color-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.subscription-account__activity-window {
|
||||
border-radius: var(--radius-tag);
|
||||
background: var(--color-surface-sunken);
|
||||
padding: 1px 6px;
|
||||
color: var(--color-text-faint);
|
||||
font-size: var(--text-label-xs);
|
||||
}
|
||||
.subscription-account__activity-daily strong {
|
||||
margin-left: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-meta);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.subscription-account__daily-success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
@@ -1655,8 +1856,7 @@ function runMenuAction(
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.subscription-account__models,
|
||||
.subscription-account__reset-credit-list {
|
||||
.subscription-account__models {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
@@ -1683,6 +1883,18 @@ function runMenuAction(
|
||||
color: var(--color-danger);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
/* 双列列表接近最小卡片宽度时,操作不能被右侧裁切;同步时间与操作组整体换行。 */
|
||||
@container (max-width: 480px) {
|
||||
.subscription-account__top > .subscription-account__top-row:first-child {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.subscription-account__top
|
||||
> .subscription-account__top-row:first-child
|
||||
.subscription-account__actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
@keyframes subscription-account-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
@@ -1697,24 +1909,11 @@ function runMenuAction(
|
||||
.subscription-account__actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
.subscription-account__quota {
|
||||
grid-template-columns: 68px minmax(0, 1fr) 76px;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.subscription-account__quota-reset {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
.subscription-account__activity {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--space-2);
|
||||
.subscription-account__refresh-skeleton {
|
||||
--subscription-account-refresh-inline: 14px;
|
||||
}
|
||||
.subscription-account__skeleton {
|
||||
--subscription-detail-activity-height: 60px;
|
||||
--subscription-detail-diagnostics-height: 92px;
|
||||
}
|
||||
.subscription-account__activity-daily {
|
||||
justify-content: space-between;
|
||||
gap: var(--space-1);
|
||||
--subscription-detail-overview-height: 154px;
|
||||
}
|
||||
.subscription-account__window-row {
|
||||
grid-template-columns: 42px repeat(4, minmax(48px, 1fr));
|
||||
@@ -1730,6 +1929,9 @@ function runMenuAction(
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.subscription-account__quota-fill {
|
||||
transition: none;
|
||||
}
|
||||
.subscription-account__detail-spinner,
|
||||
.subscription-account__sync-icon--spinning {
|
||||
animation: none;
|
||||
|
||||
@@ -412,15 +412,6 @@ function onSearchKeydown(event: KeyboardEvent): void {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InlineFeedback
|
||||
v-if="!initialLoading && !loadFailed && activeConnectionType === 'subscription'"
|
||||
class="channel-picker__subscription-risk"
|
||||
tone="warning"
|
||||
appearance="ledger"
|
||||
>
|
||||
{{ t('import.presets.subscriptionRisk') }}
|
||||
</InlineFeedback>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -662,10 +653,6 @@ function onSearchKeydown(event: KeyboardEvent): void {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.channel-picker__subscription-risk {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.channel-picker__selector {
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -444,16 +444,14 @@ const credentialStepTitle = computed(() =>
|
||||
{ channel: subscriptionChannelName.value },
|
||||
),
|
||||
)
|
||||
const credentialStepDescription = computed(() =>
|
||||
t(
|
||||
isSubscription.value
|
||||
? 'import.subscription.description'
|
||||
: structuredCredentials.value
|
||||
? 'import.credentials.structuredDescription'
|
||||
: 'import.credentials.description',
|
||||
{ channel: subscriptionChannelName.value },
|
||||
),
|
||||
)
|
||||
const credentialStepDescription = computed(() => {
|
||||
if (isSubscription.value) return undefined
|
||||
return t(
|
||||
structuredCredentials.value
|
||||
? 'import.credentials.structuredDescription'
|
||||
: 'import.credentials.description',
|
||||
)
|
||||
})
|
||||
const credentialStepSummary = computed(() => {
|
||||
if (!isSubscription.value && credentialAnalysis.value.tooManyCredentials) {
|
||||
return t('import.credentials.tooMany')
|
||||
|
||||
@@ -496,7 +496,6 @@ onBeforeUnmount(() => {
|
||||
heading-id="subscription-stager-title"
|
||||
:step="step"
|
||||
:title="t('import.subscription.title')"
|
||||
:description="t('import.subscription.description', { channel: channelLabel })"
|
||||
>
|
||||
<template v-if="readyCount" #actions>
|
||||
<span class="subscription-stager__count">
|
||||
@@ -505,14 +504,16 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</PanelHeader>
|
||||
|
||||
<InlineFeedback
|
||||
v-for="notice in notices"
|
||||
:key="notice.id"
|
||||
:tone="notice.tone"
|
||||
appearance="ledger"
|
||||
>
|
||||
{{ t(`import.subscription.channelNotice.${notice.id}`) }}
|
||||
</InlineFeedback>
|
||||
<template v-if="!hasAccounts">
|
||||
<InlineFeedback
|
||||
v-for="notice in notices"
|
||||
:key="notice.id"
|
||||
:tone="notice.tone"
|
||||
appearance="ledger"
|
||||
>
|
||||
{{ t(`import.subscription.channelNotice.${notice.id}`) }}
|
||||
</InlineFeedback>
|
||||
</template>
|
||||
|
||||
<div v-if="hasAccounts" class="subscription-stager__accounts">
|
||||
<article
|
||||
@@ -722,8 +723,9 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<div v-if="hasEntryMethod" class="subscription-stager__entry">
|
||||
<div v-if="supportsInteractiveOAuth" class="subscription-stager__entry-primary">
|
||||
<div class="subscription-stager__entry-actions">
|
||||
<AppButton
|
||||
v-if="supportsInteractiveOAuth"
|
||||
class="subscription-stager__primary"
|
||||
:variant="hasAccounts ? 'secondary' : 'primary'"
|
||||
:size="hasAccounts ? 'sm' : 'lg'"
|
||||
@@ -738,26 +740,48 @@ onBeforeUnmount(() => {
|
||||
: t('import.subscription.authorize', { channel: channelLabel })
|
||||
}}
|
||||
</AppButton>
|
||||
<span v-if="!hasAccounts" class="subscription-stager__entry-hint">
|
||||
{{ t('import.subscription.authorizeHint', { channel: channelLabel }) }}
|
||||
</span>
|
||||
<label
|
||||
v-if="supportsOAuthFile"
|
||||
class="subscription-stager__file subscription-stager__entry-file"
|
||||
:class="{
|
||||
'subscription-stager__entry-file--compact': hasAccounts,
|
||||
'subscription-stager__entry-file--primary': !supportsInteractiveOAuth,
|
||||
'is-disabled': entryBusy,
|
||||
}"
|
||||
>
|
||||
<FileJson :size="16" aria-hidden="true" />
|
||||
{{
|
||||
busyAction === 'import'
|
||||
? t('import.subscription.importing')
|
||||
: t('import.subscription.importFile')
|
||||
}}
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
multiple
|
||||
:disabled="entryBusy"
|
||||
@change="importFile"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DisclosurePanel
|
||||
v-if="supportsOAuthFile"
|
||||
:summary="
|
||||
supportsInteractiveOAuth
|
||||
? t('import.subscription.orImport')
|
||||
: t('import.subscription.oauthJSONLabel')
|
||||
"
|
||||
:open="!supportsInteractiveOAuth || jsonImportOpen"
|
||||
@update:open="jsonImportOpen = supportsInteractiveOAuth ? $event : true"
|
||||
:summary="t('import.subscription.pasteJSON')"
|
||||
indicator-position="inline"
|
||||
:open="jsonImportOpen"
|
||||
@update:open="jsonImportOpen = $event"
|
||||
>
|
||||
<template #summary>
|
||||
<span class="subscription-stager__json-disclosure-summary">
|
||||
<FileJson :size="16" aria-hidden="true" />
|
||||
<span>{{ t('import.subscription.pasteJSON') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<div class="subscription-stager__json">
|
||||
<FormField
|
||||
id="subscription-oauth-json"
|
||||
:label="t('import.subscription.oauthJSONLabel')"
|
||||
:description="t('import.subscription.oauthJSONDescription')"
|
||||
size="compact"
|
||||
>
|
||||
<template #default="field">
|
||||
@@ -784,34 +808,17 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<FileJson :size="16" aria-hidden="true" />{{ t('import.subscription.importText') }}
|
||||
</AppButton>
|
||||
<label class="subscription-stager__file" :class="{ 'is-disabled': entryBusy }">
|
||||
<FileJson :size="16" aria-hidden="true" />
|
||||
{{
|
||||
busyAction === 'import'
|
||||
? t('import.subscription.importing')
|
||||
: t('import.subscription.importFile')
|
||||
}}
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
multiple
|
||||
:disabled="entryBusy"
|
||||
@change="importFile"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</div>
|
||||
|
||||
<!-- 安全说明只在还没连上账号时引导;已有账号后它就只是噪音 -->
|
||||
<InlineFeedback
|
||||
v-if="!hasAccounts && hasEntryMethod"
|
||||
tone="neutral"
|
||||
v-if="!hasAccounts && hasEntryMethod && notices.length === 0"
|
||||
tone="warning"
|
||||
appearance="ledger-hint"
|
||||
glyph="i"
|
||||
>
|
||||
{{ t(`import.subscription.securityNotice.${context}`) }}
|
||||
{{ t('import.subscription.riskNotice') }}
|
||||
</InlineFeedback>
|
||||
|
||||
<InlineFeedback v-if="feedbackKey" tone="danger" appearance="ledger">
|
||||
@@ -1034,24 +1041,29 @@ onBeforeUnmount(() => {
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
}
|
||||
.subscription-stager__entry-primary {
|
||||
.subscription-stager__entry-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2) var(--space-3);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.subscription-stager__primary {
|
||||
flex: none;
|
||||
}
|
||||
.subscription-stager__entry-hint {
|
||||
min-width: 0;
|
||||
color: var(--color-text-faint);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.subscription-stager__json {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.subscription-stager__json-disclosure-summary {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.subscription-stager__json-disclosure-summary svg {
|
||||
flex: none;
|
||||
color: var(--color-action);
|
||||
}
|
||||
.subscription-stager__json textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
@@ -1078,10 +1090,30 @@ onBeforeUnmount(() => {
|
||||
font-weight: 560;
|
||||
cursor: pointer;
|
||||
}
|
||||
.subscription-stager__entry-file {
|
||||
min-height: var(--control-lg);
|
||||
padding-inline: 18px;
|
||||
}
|
||||
.subscription-stager__entry-file--compact {
|
||||
min-height: var(--control-sm);
|
||||
padding-inline: 12px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.subscription-stager__entry-file--primary {
|
||||
border-color: var(--color-action);
|
||||
background: var(--color-action);
|
||||
color: var(--color-action-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
.subscription-stager__file:hover:not(.is-disabled) {
|
||||
border-color: var(--color-text-faint);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.subscription-stager__entry-file--primary:hover:not(.is-disabled) {
|
||||
border-color: var(--color-action-hover);
|
||||
background: var(--color-action-hover);
|
||||
color: var(--color-action-ink);
|
||||
}
|
||||
.subscription-stager__file:focus-within {
|
||||
outline: 2px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
|
||||
@@ -389,9 +389,8 @@ export default {
|
||||
refreshCredential: 'Refresh credential',
|
||||
refreshCredentialSucceeded: 'Credential refreshed',
|
||||
refreshCredentialFailed: 'Unable to refresh the credential',
|
||||
dailyWindow: 'Last 24 hours',
|
||||
dailySuccess: 'Success',
|
||||
dailyFailure: 'Failure',
|
||||
dailySuccessSummary: '24h success',
|
||||
dailyFailureSummary: '24h failure',
|
||||
dailyIncomplete: 'Statistics do not cover the full 24 hours, so the counts may be low',
|
||||
syncFailed: 'Unable to sync account information',
|
||||
moreActions: 'More actions',
|
||||
@@ -405,7 +404,6 @@ export default {
|
||||
resetCreditPermanent: 'No expiry',
|
||||
resetCreditExpired: 'Expired',
|
||||
nearestResetCredit: 'Nearest expiry',
|
||||
resetCreditExpirations: 'Reset credit expirations',
|
||||
resetCreditsActionTooltip: 'Reset quota',
|
||||
consumeResetCredit: 'Use reset credit',
|
||||
consumeResetCreditTitle: 'Reset quota?',
|
||||
@@ -419,6 +417,8 @@ export default {
|
||||
lastTokenRefresh: 'Last credential refresh',
|
||||
noQuota: 'The upstream has not supplied any displayable quota window',
|
||||
quotaPendingRefresh: 'Refresh pending',
|
||||
quotaResetPrefix: 'Resets',
|
||||
quotaResetSuffix: '',
|
||||
quotaLabels: {
|
||||
session: 'Session',
|
||||
weekly: 'Weekly',
|
||||
@@ -437,8 +437,7 @@ export default {
|
||||
'{period}\nThis period has ended. Quota still shows the last synced result; refresh manually for current information.',
|
||||
quotaExhaustedHint:
|
||||
'Quota information is display-only. Requests use normal scheduling; an upstream limit response cools this account down and switches accounts.',
|
||||
diagnostics: 'Diagnostics',
|
||||
activity: 'Recent activity',
|
||||
overview: 'Runtime overview',
|
||||
expandDetails: 'Expand statistics',
|
||||
collapseDetails: 'Collapse statistics',
|
||||
synced: 'Synced',
|
||||
|
||||
@@ -14,8 +14,6 @@ export default {
|
||||
loadFailed: 'Unable to load channels',
|
||||
channelRequired: 'Select a channel',
|
||||
connectionType: 'Credential type',
|
||||
subscriptionRisk:
|
||||
'Subscription-account access depends on provider authorization flows rather than a stable public API contract. Upstream policy or protocol changes may invalidate authorization, interrupt access, or restrict the account; assess the risks before use.',
|
||||
},
|
||||
mode: {
|
||||
label: 'Import destination',
|
||||
@@ -108,21 +106,15 @@ export default {
|
||||
},
|
||||
subscription: {
|
||||
title: 'Connect a subscription account',
|
||||
description: 'Sign in with {channel}, or import an existing OAuth JSON file',
|
||||
riskNotice:
|
||||
'Subscription accounts depend on provider authorization. Rule changes may invalidate access or restrict the account.',
|
||||
channelNotice: {
|
||||
claude_oauth_risk:
|
||||
'Claude OAuth relies on a Claude Code compatibility protocol rather than a stable public API contract. Upstream changes may interrupt access or affect the account; evaluate this risk before authorizing.',
|
||||
antigravity_oauth_risk:
|
||||
'Antigravity OAuth depends on Google OAuth and Cloud Code interfaces that may change. Upstream policy, permission, or implementation changes can invalidate authorization, interrupt access, or affect the account. Connect only accounts you are authorized to manage and evaluate this risk first.',
|
||||
},
|
||||
securityNotice: {
|
||||
create:
|
||||
'The browser never stores the access credential. The server encrypts and holds it, and only writes it in when the group is created.',
|
||||
connect:
|
||||
'The browser never stores the access credential. The server encrypts and holds it, and writes it into this group once authorization finishes.',
|
||||
},
|
||||
authorize: 'Sign in with {channel}',
|
||||
authorizeHint: 'Opens the {channel} authorization page and returns here automatically',
|
||||
addAnother: 'Connect another account',
|
||||
authorizeFailed: 'Unable to complete {channel} sign-in',
|
||||
popupBlocked:
|
||||
@@ -156,12 +148,10 @@ export default {
|
||||
'Copy the whole URL from the address bar, not just part of it. It is fine if that page does not load.',
|
||||
submitCallback: 'Submit',
|
||||
callbackFailed: 'This URL is invalid, already used, or belongs to another authorization',
|
||||
orImport: 'Have an OAuth JSON file? Import it directly',
|
||||
pasteJSON: 'Paste JSON content',
|
||||
oauthJSONLabel: 'OAuth JSON content',
|
||||
oauthJSONDescription:
|
||||
'Paste one account OAuth credential JSON, or choose one or more local files. Each file must contain one account.',
|
||||
importText: 'Import pasted content',
|
||||
importFile: 'Choose one or more files',
|
||||
importFile: 'Import credential files',
|
||||
importing: 'Importing…',
|
||||
importResult: 'Import complete: {succeeded} succeeded, {failed} failed',
|
||||
importFailed: 'The OAuth JSON is invalid or unsupported',
|
||||
|
||||
@@ -391,9 +391,8 @@ export default {
|
||||
refreshCredential: '認証情報を更新',
|
||||
refreshCredentialSucceeded: '認証情報を更新しました',
|
||||
refreshCredentialFailed: '認証情報を更新できません',
|
||||
dailyWindow: '直近 24 時間',
|
||||
dailySuccess: '成功',
|
||||
dailyFailure: '失敗',
|
||||
dailySuccessSummary: '24 時間の成功',
|
||||
dailyFailureSummary: '24 時間の失敗',
|
||||
dailyIncomplete:
|
||||
'統計データが 24 時間全体をカバーしていないため、件数が少なく出る場合があります',
|
||||
syncFailed: 'アカウント情報を同期できません',
|
||||
@@ -408,7 +407,6 @@ export default {
|
||||
resetCreditPermanent: '期限なし',
|
||||
resetCreditExpired: '期限切れ',
|
||||
nearestResetCredit: '直近の期限',
|
||||
resetCreditExpirations: 'リセットクレジットの期限',
|
||||
resetCreditsActionTooltip: '利用枠をリセット',
|
||||
consumeResetCredit: 'リセットクレジットを使用',
|
||||
consumeResetCreditTitle: '利用枠をリセットしますか?',
|
||||
@@ -422,6 +420,8 @@ export default {
|
||||
lastTokenRefresh: '認証情報の最終更新',
|
||||
noQuota: '上流から表示可能な利用枠が提供されていません',
|
||||
quotaPendingRefresh: '更新待ち',
|
||||
quotaResetPrefix: '',
|
||||
quotaResetSuffix: 'にリセット',
|
||||
quotaLabels: {
|
||||
session: 'セッション',
|
||||
weekly: '週間',
|
||||
@@ -440,8 +440,7 @@ export default {
|
||||
'{period}\n現在の期間は終了しています。利用枠は前回同期時のままです。最新情報は手動で更新してください。',
|
||||
quotaExhaustedHint:
|
||||
'利用枠情報は表示専用です。リクエストは通常どおりスケジュールされ、上流の制限応答後にアカウントをクールダウンして切り替えます。',
|
||||
diagnostics: '診断情報',
|
||||
activity: '最近のアクティビティ',
|
||||
overview: '稼働概要',
|
||||
expandDetails: '統計データを展開',
|
||||
collapseDetails: '統計データを折りたたむ',
|
||||
synced: '同期',
|
||||
|
||||
@@ -14,8 +14,6 @@ export default {
|
||||
loadFailed: 'チャネルを読み込めません',
|
||||
channelRequired: 'チャネルを選択してください',
|
||||
connectionType: '認証情報の種類',
|
||||
subscriptionRisk:
|
||||
'サブスクリプションアカウント接続は、安定した公開 API 契約ではなくプロバイダーの認証フローに依存します。上流のポリシーやプロトコル変更により、認証の失効、接続中断、アカウント制限が発生する可能性があるため、利用前にリスクを評価してください。',
|
||||
},
|
||||
mode: {
|
||||
label: 'インポート先',
|
||||
@@ -106,21 +104,15 @@ export default {
|
||||
},
|
||||
subscription: {
|
||||
title: 'サブスクリプションアカウントを接続',
|
||||
description: '{channel} でログインするか、既存の OAuth JSON を読み込みます',
|
||||
riskNotice:
|
||||
'サブスクリプションアカウントはプロバイダー認証に依存し、ルール変更で認証失効やアカウント制限が起こる場合があります。',
|
||||
channelNotice: {
|
||||
claude_oauth_risk:
|
||||
'Claude OAuth は安定した公開 API 契約ではなく、Claude Code 互換プロトコルに依存します。上流の変更による接続停止やアカウントへの影響を、認証前に評価してください。',
|
||||
antigravity_oauth_risk:
|
||||
'Antigravity OAuth は Google OAuth と変更される可能性がある Cloud Code インターフェースに依存します。上流のポリシー、権限、実装の変更により認証失効、接続中断、アカウントへの影響が生じる場合があります。管理権限のあるアカウントだけを接続し、事前にリスクを評価してください。',
|
||||
},
|
||||
securityNotice: {
|
||||
create:
|
||||
'ブラウザーはアクセス認証情報を保存しません。サーバー側で暗号化して保持し、グループ作成時にのみ書き込みます。',
|
||||
connect:
|
||||
'ブラウザーはアクセス認証情報を保存しません。サーバー側で暗号化して保持し、認証完了後にこのグループへ書き込みます。',
|
||||
},
|
||||
authorize: '{channel} でログイン',
|
||||
authorizeHint: '{channel} の認証ページを開き、完了すると自動でここに戻ります',
|
||||
addAnother: 'もう 1 つ接続',
|
||||
authorizeFailed: '{channel} ログインを完了できません',
|
||||
popupBlocked:
|
||||
@@ -153,12 +145,10 @@ export default {
|
||||
'アドレスバーの URL を一部ではなく全体をコピーして貼り付けてください。そのページが開けなくても問題ありません。',
|
||||
submitCallback: '送信',
|
||||
callbackFailed: 'この URL は無効、使用済み、または別の認証に属しています',
|
||||
orImport: 'OAuth JSON をお持ちの場合はこちらから読み込み',
|
||||
pasteJSON: 'JSON の内容を貼り付け',
|
||||
oauthJSONLabel: 'OAuth JSON の内容',
|
||||
oauthJSONDescription:
|
||||
'1 アカウント分の OAuth 認証情報 JSON を貼り付けるか、1 つ以上のローカルファイルを選択します。各ファイルには 1 アカウントのみを含めてください。',
|
||||
importText: '貼り付けた内容をインポート',
|
||||
importFile: '1 つ以上のファイルを選択',
|
||||
importFile: '認証情報ファイルを読み込む',
|
||||
importing: 'インポート中…',
|
||||
importResult: 'インポート完了:成功 {succeeded} 件、失敗 {failed} 件',
|
||||
importFailed: 'OAuth JSON が無効または未対応です',
|
||||
|
||||
@@ -379,9 +379,8 @@ export default {
|
||||
refreshCredential: '刷新凭据',
|
||||
refreshCredentialSucceeded: '凭据已刷新',
|
||||
refreshCredentialFailed: '无法刷新凭据',
|
||||
dailyWindow: '近 24 小时',
|
||||
dailySuccess: '成功',
|
||||
dailyFailure: '失败',
|
||||
dailySuccessSummary: '24 小时成功',
|
||||
dailyFailureSummary: '24 小时失败',
|
||||
dailyIncomplete: '统计数据未覆盖完整 24 小时,计数可能偏低',
|
||||
syncFailed: '无法同步账号信息',
|
||||
moreActions: '更多操作',
|
||||
@@ -395,7 +394,6 @@ export default {
|
||||
resetCreditPermanent: '永久有效',
|
||||
resetCreditExpired: '已过期',
|
||||
nearestResetCredit: '最近一张',
|
||||
resetCreditExpirations: '重置卡到期',
|
||||
resetCreditsActionTooltip: '重置额度',
|
||||
consumeResetCredit: '使用重置卡',
|
||||
consumeResetCreditTitle: '重置额度?',
|
||||
@@ -407,6 +405,8 @@ export default {
|
||||
lastTokenRefresh: '上次凭据刷新',
|
||||
noQuota: '上游尚未提供可展示的额度窗口',
|
||||
quotaPendingRefresh: '待刷新',
|
||||
quotaResetPrefix: '',
|
||||
quotaResetSuffix: '重置',
|
||||
quotaLabels: {
|
||||
session: '会话',
|
||||
weekly: '每周',
|
||||
@@ -419,14 +419,13 @@ export default {
|
||||
unknown: '上游未提供',
|
||||
remaining: '剩余 {remaining} / {limit}',
|
||||
remainingAmount: '剩余 {remaining}',
|
||||
remainingPercent: '剩 {value}%',
|
||||
remainingPercent: '剩余 {value}%',
|
||||
quotaPeriod: '当前周期\n{start} → {end}',
|
||||
quotaPendingHint:
|
||||
'{period}\n当前周期已结束,额度仍显示上次同步结果;请手动刷新获取最新信息。',
|
||||
quotaExhaustedHint:
|
||||
'额度信息仅用于展示;请求仍按正常策略调度,命中上游限额后才会冷却并切换账号。',
|
||||
diagnostics: '诊断信息',
|
||||
activity: '最近活动',
|
||||
overview: '运行概况',
|
||||
expandDetails: '展开统计数据',
|
||||
collapseDetails: '收起统计数据',
|
||||
synced: '同步',
|
||||
|
||||
@@ -14,8 +14,6 @@ export default {
|
||||
loadFailed: '无法加载渠道列表',
|
||||
channelRequired: '请选择渠道',
|
||||
connectionType: '凭据类型',
|
||||
subscriptionRisk:
|
||||
'订阅账号接入依赖服务商的授权机制,并非稳定的公开 API 契约;上游规则或协议变化可能导致授权失效、接入中断或账号受限,请在使用前自行评估风险。',
|
||||
},
|
||||
mode: {
|
||||
label: '导入目标',
|
||||
@@ -102,19 +100,14 @@ export default {
|
||||
},
|
||||
subscription: {
|
||||
title: '连接订阅账号',
|
||||
description: '使用 {channel} 登录授权,或导入已有的 OAuth JSON 文件',
|
||||
riskNotice: '订阅账号依赖服务商授权,规则变化可能导致授权失效或账号受限。',
|
||||
channelNotice: {
|
||||
claude_oauth_risk:
|
||||
'Claude OAuth 依赖 Claude Code 的兼容协议,并非稳定的公开 API 契约;上游变化可能导致接入中断或影响账号,请在授权前自行评估风险。',
|
||||
antigravity_oauth_risk:
|
||||
'Antigravity OAuth 依赖 Google OAuth 与可能变化的 Cloud Code 接口;上游策略、权限或实现变化可能导致授权失效、接入中断或账号受限。请仅连接你有权管理的账号,并自行评估风险。',
|
||||
},
|
||||
securityNotice: {
|
||||
create: '浏览器不会保存访问凭据;服务端加密暂存,创建分组时才正式写入。',
|
||||
connect: '浏览器不会保存访问凭据;服务端加密暂存,授权完成后写入当前分组。',
|
||||
},
|
||||
authorize: '使用 {channel} 登录',
|
||||
authorizeHint: '会打开 {channel} 授权页面,完成后自动回到这里',
|
||||
addAnother: '再连一个账号',
|
||||
authorizeFailed: '无法完成 {channel} 登录',
|
||||
popupBlocked: '新窗口被浏览器拦截了,请用下面的授权链接手动打开',
|
||||
@@ -143,12 +136,10 @@ export default {
|
||||
callbackHelp: '从地址栏复制完整网址粘到这里,不要只复制其中一段。页面打不开也没关系。',
|
||||
submitCallback: '提交',
|
||||
callbackFailed: '这段网址无效、已使用,或不属于当前这次授权',
|
||||
orImport: '已有 OAuth JSON 文件?直接导入',
|
||||
pasteJSON: '粘贴 JSON 内容',
|
||||
oauthJSONLabel: 'OAuth JSON 内容',
|
||||
oauthJSONDescription:
|
||||
'可直接粘贴单个账号的 OAuth 凭据 JSON,也可选择一个或多个本地文件;每个文件只包含一个账号。',
|
||||
importText: '导入已粘贴内容',
|
||||
importFile: '选择一个或多个文件',
|
||||
importFile: '导入凭据文件',
|
||||
importing: '正在导入…',
|
||||
importResult: '导入完成:成功 {succeeded},失败 {failed}',
|
||||
importFailed: 'OAuth JSON 无效或不受支持',
|
||||
|
||||
Reference in new issue
Block a user