mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 09:57:47 +08:00
refactor(frontend): modularize i18n architecture
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||
{{ title }}
|
||||
{{ displayTitle }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
class="h-10 px-5"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ cancelText }}
|
||||
{{ displayCancelText }}
|
||||
</Button>
|
||||
|
||||
<!-- 确认按钮 -->
|
||||
@@ -58,7 +58,7 @@
|
||||
v-if="loading"
|
||||
class="animate-spin h-4 w-4 mr-2"
|
||||
/>
|
||||
{{ confirmText }}
|
||||
{{ displayConfirmText }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -69,6 +69,7 @@ import { computed } from 'vue'
|
||||
import { Dialog } from '@/components/ui'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { AlertTriangle, AlertCircle, Info, Trash2, HelpCircle, Loader2 } from 'lucide-vue-next'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
export type AlertType = 'danger' | 'destructive' | 'warning' | 'info' | 'question'
|
||||
|
||||
@@ -96,10 +97,15 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
const displayTitle = computed(() => legacyT(props.title))
|
||||
const displayConfirmText = computed(() => legacyT(props.confirmText))
|
||||
const displayCancelText = computed(() => legacyT(props.cancelText))
|
||||
|
||||
// 解析描述文本为多行
|
||||
const descriptionLines = computed(() => {
|
||||
return props.description.split('\n').filter(line => line.trim())
|
||||
return legacyT(props.description).split('\n').filter(line => line.trim())
|
||||
})
|
||||
|
||||
function escapeHtml(raw: string): string {
|
||||
|
||||
@@ -16,18 +16,18 @@
|
||||
|
||||
<!-- 标题 -->
|
||||
<h3
|
||||
v-if="title"
|
||||
v-if="displayTitle"
|
||||
:class="titleClasses"
|
||||
>
|
||||
{{ title }}
|
||||
{{ displayTitle }}
|
||||
</h3>
|
||||
|
||||
<!-- 描述 -->
|
||||
<p
|
||||
v-if="description"
|
||||
v-if="displayDescription"
|
||||
:class="descriptionClasses"
|
||||
>
|
||||
{{ description }}
|
||||
{{ displayDescription }}
|
||||
</p>
|
||||
|
||||
<!-- 自定义内容插槽 -->
|
||||
@@ -40,12 +40,12 @@
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div
|
||||
v-if="$slots.actions || actionText"
|
||||
v-if="$slots.actions || displayActionText"
|
||||
class="mt-6 flex flex-wrap items-center justify-center gap-3"
|
||||
>
|
||||
<slot name="actions">
|
||||
<Button
|
||||
v-if="actionText"
|
||||
v-if="displayActionText"
|
||||
:variant="actionVariant"
|
||||
:size="actionSize"
|
||||
@click="handleAction"
|
||||
@@ -55,7 +55,7 @@
|
||||
v-if="actionIcon"
|
||||
class="mr-2 h-4 w-4"
|
||||
/>
|
||||
{{ actionText }}
|
||||
{{ displayActionText }}
|
||||
</Button>
|
||||
</slot>
|
||||
</div>
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
Filter
|
||||
} from 'lucide-vue-next'
|
||||
import type { Component } from 'vue'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
type EmptyStateType = 'default' | 'search' | 'filter' | 'error' | 'empty' | 'notFound'
|
||||
type ButtonVariant = 'default' | 'outline' | 'secondary' | 'ghost' | 'link' | 'destructive'
|
||||
@@ -128,6 +129,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
// 根据类型获取默认配置
|
||||
const typeConfig = computed(() => {
|
||||
@@ -169,6 +171,9 @@ const typeConfig = computed(() => {
|
||||
|
||||
// 默认图标
|
||||
const defaultIcon = computed(() => typeConfig.value.icon)
|
||||
const displayTitle = computed(() => legacyT(props.title || typeConfig.value.title))
|
||||
const displayDescription = computed(() => legacyT(props.description || typeConfig.value.description))
|
||||
const displayActionText = computed(() => props.actionText ? legacyT(props.actionText) : '')
|
||||
|
||||
// 容器样式
|
||||
const containerClasses = computed(() => {
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-xs font-medium">
|
||||
{{ dropTitle }}
|
||||
{{ localizedDropTitle }}
|
||||
</p>
|
||||
<p class="text-[11px] text-muted-foreground mt-0.5">
|
||||
{{ dropHint }}
|
||||
{{ localizedDropHint }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,12 +40,12 @@
|
||||
class="space-y-1.5"
|
||||
>
|
||||
<Label v-if="manualLabel">
|
||||
{{ manualLabel }}
|
||||
{{ localizedManualLabel }}
|
||||
</Label>
|
||||
<Textarea
|
||||
:model-value="modelValue"
|
||||
:disabled="disabled"
|
||||
:placeholder="manualPlaceholder"
|
||||
:placeholder="localizedManualPlaceholder"
|
||||
:class="textareaClass"
|
||||
spellcheck="false"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@@ -54,7 +54,7 @@
|
||||
v-if="manualDescription"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
{{ manualDescription }}
|
||||
{{ localizedManualDescription }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
@click="showManualInput = true"
|
||||
>
|
||||
{{ pasteToggleText }}
|
||||
{{ localizedPasteToggleText }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@@ -73,7 +73,7 @@
|
||||
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
@click="switchToFileMode"
|
||||
>
|
||||
{{ fileToggleText }}
|
||||
{{ localizedFileToggleText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,6 +83,7 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Upload } from 'lucide-vue-next'
|
||||
import { Label, Textarea } from '@/components/ui'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
interface ImportInputErrorPayload {
|
||||
message: string
|
||||
@@ -122,10 +123,18 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
error: [payload: ImportInputErrorPayload]
|
||||
}>()
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
const showManualInput = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
const localizedDropTitle = computed(() => legacyT(props.dropTitle))
|
||||
const localizedDropHint = computed(() => legacyT(props.dropHint))
|
||||
const localizedManualLabel = computed(() => props.manualLabel ? legacyT(props.manualLabel) : '')
|
||||
const localizedManualPlaceholder = computed(() => props.manualPlaceholder ? legacyT(props.manualPlaceholder) : '')
|
||||
const localizedManualDescription = computed(() => props.manualDescription ? legacyT(props.manualDescription) : '')
|
||||
const localizedPasteToggleText = computed(() => legacyT(props.pasteToggleText))
|
||||
const localizedFileToggleText = computed(() => legacyT(props.fileToggleText))
|
||||
|
||||
const acceptParts = computed(() => {
|
||||
return props.accept
|
||||
@@ -146,7 +155,10 @@ function resetUiState() {
|
||||
}
|
||||
|
||||
function emitError(message: string, title?: string) {
|
||||
emit('error', { message, title })
|
||||
emit('error', {
|
||||
message: legacyT(message),
|
||||
title: title ? legacyT(title) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
function isValidFileType(file: File): boolean {
|
||||
@@ -180,9 +192,9 @@ function readFileAsText(file: File): Promise<string> {
|
||||
resolve(content)
|
||||
return
|
||||
}
|
||||
reject(new Error('读取失败'))
|
||||
reject(new Error(legacyT('读取失败')))
|
||||
}
|
||||
reader.onerror = () => reject(new Error('读取失败'))
|
||||
reader.onerror = () => reject(new Error(legacyT('读取失败')))
|
||||
reader.readAsText(file)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
||||
:aria-label="t('common.language')"
|
||||
:title="t('common.language')"
|
||||
type="button"
|
||||
>
|
||||
<Languages class="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
class="min-w-36"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
class="justify-between gap-3"
|
||||
@select="setLocale(option.value)"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
<Check
|
||||
v-if="locale === option.value"
|
||||
class="h-4 w-4 text-primary"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Check, Languages } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useI18n, useLocaleOptions, type Locale } from '@/i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { locale, setLocale } = useLocaleOptions()
|
||||
|
||||
const options = computed<Array<{ value: Locale; label: string }>>(() => [
|
||||
{ value: 'zh-CN', label: t('common.chinese') },
|
||||
{ value: 'en-US', label: t('common.english') },
|
||||
])
|
||||
</script>
|
||||
@@ -20,7 +20,7 @@
|
||||
<span
|
||||
v-if="invalidItems.length"
|
||||
class="text-destructive"
|
||||
>({{ invalidItems.length }} 个已失效)</span>
|
||||
>{{ invalidSummaryText }}</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
class="h-4 w-4 shrink-0 text-muted-foreground transition-transform"
|
||||
@@ -47,7 +47,7 @@
|
||||
/>
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
:placeholder="searchPlaceholder"
|
||||
:placeholder="localizedSearchPlaceholder"
|
||||
class="h-9 rounded-xl border-border/60 bg-background/80 pl-9 pr-3 text-sm"
|
||||
@keydown.stop
|
||||
/>
|
||||
@@ -64,12 +64,12 @@
|
||||
type="checkbox"
|
||||
:checked="isAllSelected"
|
||||
:indeterminate="isPartiallySelected"
|
||||
aria-label="全选"
|
||||
:aria-label="legacyT('全选')"
|
||||
class="h-4 w-4 shrink-0 cursor-pointer rounded border-border/60 bg-card/80 text-primary shadow-sm accent-primary focus:ring-2 focus:ring-primary/40 focus:ring-offset-1"
|
||||
@click.stop
|
||||
@change="toggleAll"
|
||||
>
|
||||
<span class="min-w-0 truncate text-sm">全选</span>
|
||||
<span class="min-w-0 truncate text-sm">{{ legacyT('全选') }}</span>
|
||||
<span class="ml-auto shrink-0 text-xs text-muted-foreground">
|
||||
{{ selectedOptionCount }}/{{ options.length }}
|
||||
</span>
|
||||
@@ -89,7 +89,7 @@
|
||||
@change="remove(item)"
|
||||
>
|
||||
<span class="min-w-0 truncate text-sm text-destructive">{{ item }}</span>
|
||||
<span class="shrink-0 text-xs text-destructive/70">(已失效)</span>
|
||||
<span class="shrink-0 text-xs text-destructive/70">{{ legacyT('(已失效)') }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -111,7 +111,7 @@
|
||||
v-if="filteredOptions.length === 0 && filteredInvalidItems.length === 0"
|
||||
class="px-3 py-2 text-sm text-muted-foreground"
|
||||
>
|
||||
{{ searchQuery.trim() ? noResultsText : emptyText }}
|
||||
{{ searchQuery.trim() ? localizedNoResultsText : localizedEmptyText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,6 +124,7 @@ import { ChevronDown, Search } from 'lucide-vue-next'
|
||||
import { Input } from '@/components/ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { matchesSearchQuery } from '@/utils/search'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
export interface MultiSelectOption {
|
||||
value: string
|
||||
@@ -160,6 +161,7 @@ const props = withDefaults(
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string[]]
|
||||
}>()
|
||||
const { legacyT, locale } = useI18n()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const searchQuery = ref('')
|
||||
@@ -190,6 +192,15 @@ const showSearch = computed(
|
||||
() => props.searchable && totalCount.value >= props.searchThreshold,
|
||||
)
|
||||
|
||||
const localizedPlaceholder = computed(() => legacyT(props.placeholder))
|
||||
const localizedEmptyText = computed(() => legacyT(props.emptyText))
|
||||
const localizedNoResultsText = computed(() => legacyT(props.noResultsText))
|
||||
const localizedSearchPlaceholder = computed(() => legacyT(props.searchPlaceholder))
|
||||
const invalidSummaryText = computed(() => {
|
||||
const count = invalidItems.value.length
|
||||
return locale.value === 'en-US' ? `(${count} invalid)` : `(${count} 个已失效)`
|
||||
})
|
||||
|
||||
const filteredInvalidItems = computed(() => {
|
||||
if (!showSearch.value || !searchQuery.value.trim()) {
|
||||
return invalidItems.value
|
||||
@@ -210,13 +221,15 @@ const filteredOptions = computed(() => {
|
||||
})
|
||||
|
||||
const displayText = computed(() => {
|
||||
if (props.modelValue.length === 0) return props.placeholder
|
||||
if (props.modelValue.length === 0) return localizedPlaceholder.value
|
||||
if (props.modelValue.length <= 2) {
|
||||
return props.modelValue
|
||||
.map((v) => props.options.find((o) => o.value === v)?.label ?? v)
|
||||
.join(', ')
|
||||
}
|
||||
return `已选择 ${props.modelValue.length} 项`
|
||||
return locale.value === 'en-US'
|
||||
? `${props.modelValue.length} selected`
|
||||
: `已选择 ${props.modelValue.length} 项`
|
||||
})
|
||||
|
||||
watch(isOpen, (open) => {
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<DialogTitle>
|
||||
{{ title }}
|
||||
{{ displayTitle }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ description }}
|
||||
{{ displayDescription }}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<Badge
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
支付方式
|
||||
{{ legacyT('支付方式') }}
|
||||
</div>
|
||||
<div class="text-sm text-foreground">
|
||||
{{ stripeDisplayName }}
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
应付金额
|
||||
{{ legacyT('应付金额') }}
|
||||
</div>
|
||||
<div class="text-sm text-foreground">
|
||||
{{ stripeAmountLabel }}
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
支付通道
|
||||
{{ legacyT('支付通道') }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<Badge
|
||||
@@ -87,7 +87,7 @@
|
||||
class="absolute inset-3 flex items-center justify-center gap-2 rounded-lg bg-background/80 text-sm text-muted-foreground backdrop-blur-sm"
|
||||
>
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
正在加载 Stripe 支付组件...
|
||||
{{ legacyT('正在加载 Stripe 支付组件...') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
:disabled="initializing || submitting"
|
||||
@click="closeDialog"
|
||||
>
|
||||
关闭
|
||||
{{ legacyT('关闭') }}
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="!canSubmit"
|
||||
@@ -115,7 +115,7 @@
|
||||
v-if="submitting"
|
||||
class="mr-2 h-4 w-4 animate-spin"
|
||||
/>
|
||||
{{ submitting ? '支付中...' : confirmText }}
|
||||
{{ submitting ? legacyT('支付中...') : displayConfirmText }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -131,6 +131,7 @@ import {
|
||||
type PaymentInstructionMap,
|
||||
} from '@/utils/paymentInstructions'
|
||||
import { paymentMethodLabel } from '@/utils/walletDisplay'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
@@ -152,11 +153,15 @@ const emit = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
success: [payload: { intentId: string; status?: string | null }]
|
||||
}>()
|
||||
const { legacyT } = useI18n()
|
||||
|
||||
const dialogOpen = computed({
|
||||
get: () => props.open,
|
||||
set: value => emit('update:open', value),
|
||||
})
|
||||
const displayTitle = computed(() => legacyT(props.title))
|
||||
const displayDescription = computed(() => legacyT(props.description))
|
||||
const displayConfirmText = computed(() => legacyT(props.confirmText))
|
||||
|
||||
const paymentElementRoot = ref<HTMLDivElement | null>(null)
|
||||
const stripeInstance = ref<Stripe | null>(null)
|
||||
@@ -218,7 +223,7 @@ async function initializeStripe() {
|
||||
const instructions = stripeInstructions.value
|
||||
if (!props.open) return
|
||||
if (!instructions) {
|
||||
errorMessage.value = '缺少 Stripe 支付参数'
|
||||
errorMessage.value = legacyT('缺少 Stripe 支付参数')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -243,12 +248,12 @@ async function initializeStripe() {
|
||||
try {
|
||||
await nextTick()
|
||||
if (!paymentElementRoot.value) {
|
||||
throw new Error('支付容器未准备好')
|
||||
throw new Error(legacyT('支付容器未准备好'))
|
||||
}
|
||||
|
||||
const stripe = await loadStripeCached(instructions.publishableKey)
|
||||
if (!stripe) {
|
||||
throw new Error('Stripe 初始化失败')
|
||||
throw new Error(legacyT('Stripe 初始化失败'))
|
||||
}
|
||||
if (sequence !== mountSequence) return
|
||||
|
||||
@@ -277,7 +282,7 @@ async function initializeStripe() {
|
||||
async function submitPayment() {
|
||||
const instructions = stripeInstructions.value
|
||||
if (!instructions || !stripeInstance.value || !elementsInstance.value) {
|
||||
errorMessage.value = 'Stripe 支付组件未就绪'
|
||||
errorMessage.value = legacyT('Stripe 支付组件未就绪')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -287,7 +292,7 @@ async function submitPayment() {
|
||||
try {
|
||||
const submitResult = await elementsInstance.value.submit()
|
||||
if (submitResult.error) {
|
||||
errorMessage.value = submitResult.error.message || '请检查支付信息'
|
||||
errorMessage.value = submitResult.error.message || legacyT('请检查支付信息')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -300,7 +305,7 @@ async function submitPayment() {
|
||||
})
|
||||
|
||||
if (error) {
|
||||
errorMessage.value = error.message || '支付失败'
|
||||
errorMessage.value = error.message || legacyT('支付失败')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -315,8 +320,8 @@ async function submitPayment() {
|
||||
}
|
||||
|
||||
errorMessage.value = paymentIntent?.status
|
||||
? `当前支付状态: ${paymentIntent.status}`
|
||||
: '支付已提交,请稍后刷新订单状态'
|
||||
? `${legacyT('当前支付状态')}: ${paymentIntent.status}`
|
||||
: legacyT('支付已提交,请稍后刷新订单状态')
|
||||
} catch (error) {
|
||||
errorMessage.value = formatStripeError(error)
|
||||
} finally {
|
||||
@@ -370,18 +375,19 @@ function formatStripeError(error: unknown): string {
|
||||
return message.trim()
|
||||
}
|
||||
}
|
||||
return 'Stripe 支付处理失败'
|
||||
return legacyT('Stripe 支付处理失败')
|
||||
}
|
||||
|
||||
function paymentMethodTypeLabel(method: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
card: '银行卡/信用卡',
|
||||
alipay: '支付宝',
|
||||
wechat_pay: '微信支付',
|
||||
card: legacyT('银行卡/信用卡'),
|
||||
alipay: legacyT('支付宝'),
|
||||
wechat_pay: legacyT('微信支付'),
|
||||
link: 'Link',
|
||||
us_bank_account: '美国银行账户',
|
||||
us_bank_account: legacyT('美国银行账户'),
|
||||
}
|
||||
return labels[method] || paymentMethodLabel(method) || method
|
||||
const fallback = paymentMethodLabel(method)
|
||||
return labels[method] || (fallback ? legacyT(fallback) : method)
|
||||
}
|
||||
|
||||
function buildReturnUrl(): string {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class="h-8 w-32 text-xs border-border/60"
|
||||
:class="[presetTriggerClass]"
|
||||
>
|
||||
<SelectValue placeholder="选择时间段" />
|
||||
<SelectValue :placeholder="legacyT('选择时间段')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent :searchable="false">
|
||||
<SelectItem
|
||||
@@ -29,7 +29,7 @@
|
||||
type="date"
|
||||
class="h-8 w-36 text-xs border-border/60"
|
||||
/>
|
||||
<span class="text-xs text-muted-foreground">至</span>
|
||||
<span class="text-xs text-muted-foreground">{{ legacyT('至') }}</span>
|
||||
<Input
|
||||
v-model="endDate"
|
||||
type="date"
|
||||
@@ -42,23 +42,23 @@
|
||||
v-model="selectedGranularity"
|
||||
>
|
||||
<SelectTrigger class="h-8 w-24 text-xs border-border/60">
|
||||
<SelectValue placeholder="粒度" />
|
||||
<SelectValue :placeholder="legacyT('粒度')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-if="allowHourly && canUseHourly"
|
||||
value="hour"
|
||||
>
|
||||
小时
|
||||
{{ legacyT('小时') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="day">
|
||||
天
|
||||
{{ legacyT('天') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="week">
|
||||
周
|
||||
{{ legacyT('周') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="month">
|
||||
月
|
||||
{{ legacyT('月') }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
Input
|
||||
} from '@/components/ui'
|
||||
import type { DateRangeParams } from '@/features/usage/types'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: DateRangeParams
|
||||
@@ -89,17 +90,18 @@ const props = withDefaults(defineProps<{
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: DateRangeParams]
|
||||
}>()
|
||||
const { legacyT } = useI18n()
|
||||
const selectablePresets = ['today', 'yesterday', 'last7days', 'last30days', 'last90days', 'custom'] as const
|
||||
type SelectablePreset = typeof selectablePresets[number]
|
||||
|
||||
const presetLabels: Record<SelectablePreset, string> = {
|
||||
today: '今天',
|
||||
yesterday: '昨天',
|
||||
last7days: '最近7天',
|
||||
last30days: '最近30天',
|
||||
last90days: '最近90天',
|
||||
custom: '自定义'
|
||||
}
|
||||
const presetLabels = computed<Record<SelectablePreset, string>>(() => ({
|
||||
today: legacyT('今天'),
|
||||
yesterday: legacyT('昨天'),
|
||||
last7days: legacyT('最近7天'),
|
||||
last30days: legacyT('最近30天'),
|
||||
last90days: legacyT('最近90天'),
|
||||
custom: legacyT('自定义')
|
||||
}))
|
||||
|
||||
const activePresetOptions = computed<SelectablePreset[]>(() => {
|
||||
const unique = new Set(props.presetOptions)
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
<!-- Reconnecting State -->
|
||||
<template v-if="updatePhase === 'reconnecting'">
|
||||
<h2 class="text-xl font-semibold text-foreground mt-4 mb-2">
|
||||
正在重启服务
|
||||
{{ legacyT('正在重启服务') }}
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground max-w-xs mt-2 mb-2">
|
||||
服务正在切换版本并重启,请稍候...
|
||||
{{ legacyT('服务正在切换版本并重启,请稍候...') }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 text-primary mt-2 mb-4">
|
||||
<svg
|
||||
@@ -71,10 +71,10 @@
|
||||
v-if="publishedAt"
|
||||
class="mb-2 text-left text-xs text-muted-foreground"
|
||||
>
|
||||
发布于 {{ formattedPublishedAt }}
|
||||
{{ legacyT('发布于') }} {{ formattedPublishedAt }}
|
||||
</div>
|
||||
<div class="mb-2 text-left text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground/80">
|
||||
更新内容
|
||||
{{ legacyT('更新内容') }}
|
||||
</div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
@@ -96,7 +96,7 @@
|
||||
v-if="updatePhase === 'restart'"
|
||||
class="mt-1 text-xs text-primary"
|
||||
>
|
||||
更新包已下载,点击"立即重启"完成安装
|
||||
{{ legacyT('更新包已下载,点击"立即重启"完成安装') }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
@@ -132,7 +132,7 @@
|
||||
class="mt-3 w-full max-w-sm rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-left"
|
||||
>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
在 docker-compose.yml 所在目录执行
|
||||
{{ legacyT('在 docker-compose.yml 所在目录执行') }}
|
||||
</p>
|
||||
<code class="mt-1 block break-all rounded bg-background/70 px-2 py-1.5 font-mono text-xs text-foreground">
|
||||
{{ dockerUpdateCommand }}
|
||||
@@ -152,7 +152,7 @@
|
||||
:disabled="updating || rollingBack"
|
||||
@click="handleLater"
|
||||
>
|
||||
稍后提醒
|
||||
{{ legacyT('稍后提醒') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="rollbackAvailable"
|
||||
@@ -161,7 +161,7 @@
|
||||
:disabled="updating || rollingBack"
|
||||
@click="handleRollback"
|
||||
>
|
||||
{{ rollingBack ? '回滚中...' : '回滚上一版本' }}
|
||||
{{ rollingBack ? legacyT('回滚中...') : legacyT('回滚上一版本') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
@@ -194,6 +194,7 @@ import { formatDisplayVersion } from '@/utils/version'
|
||||
import { normalizeReleaseNotesForDisplay } from '@/utils/releaseNotes'
|
||||
import { sanitizeMarkdown } from '@/utils/sanitize'
|
||||
import { marked } from 'marked'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -224,6 +225,7 @@ const emit = defineEmits<{
|
||||
applyUpdate: []
|
||||
rollback: []
|
||||
}>()
|
||||
const { legacyT, locale } = useI18n()
|
||||
|
||||
const SOURCE_BUILD_UPDATE_HINT = '当前为源码构建,请使用 git pull 后重新编译。'
|
||||
|
||||
@@ -237,19 +239,19 @@ const updateStrategy = computed(() => props.updateStrategy ?? 'manual')
|
||||
const isDockerUpdate = computed(() => updateStrategy.value === 'docker' && !canApplyUpdate.value)
|
||||
const dockerUpdateCommand = computed(() => props.dockerUpdateCommand || '')
|
||||
const updateBlockerText = computed(() => {
|
||||
if (!updateSupported.value) return props.updateBlocker || SOURCE_BUILD_UPDATE_HINT
|
||||
return props.updateBlocker || '当前版本暂不支持在线更新'
|
||||
if (!updateSupported.value) return legacyT(props.updateBlocker || SOURCE_BUILD_UPDATE_HINT)
|
||||
return legacyT(props.updateBlocker || '当前版本暂不支持在线更新')
|
||||
})
|
||||
const reconnectMessage = computed(() => props.reconnectMessage ?? '等待服务恢复...')
|
||||
const reconnectMessage = computed(() => legacyT(props.reconnectMessage ?? '等待服务恢复...'))
|
||||
const rollbackAvailable = computed(() => props.rollbackAvailable ?? false)
|
||||
const rollingBack = computed(() => props.rollingBack ?? false)
|
||||
const downloadProgressText = computed(() => props.downloadProgressText || '正在下载更新包...')
|
||||
const dialogTitleText = computed(() => props.dialogTitle ?? '发现新版本')
|
||||
const versionLabelText = computed(() => props.versionLabel ?? '最新版本')
|
||||
const releaseLinkLabelText = computed(() => props.releaseLinkLabel ?? '查看发布')
|
||||
const downloadProgressText = computed(() => legacyT(props.downloadProgressText || '正在下载更新包...'))
|
||||
const dialogTitleText = computed(() => legacyT(props.dialogTitle ?? '发现新版本'))
|
||||
const versionLabelText = computed(() => legacyT(props.versionLabel ?? '最新版本'))
|
||||
const releaseLinkLabelText = computed(() => legacyT(props.releaseLinkLabel ?? '查看发布'))
|
||||
const fallbackDescriptionText = computed(() => {
|
||||
if (!canApplyUpdate.value) return updateBlockerText.value
|
||||
return '新版本已发布,建议更新以获得最新功能和安全修复'
|
||||
return legacyT('新版本已发布,建议更新以获得最新功能和安全修复')
|
||||
})
|
||||
const downloadProgressPercent = computed(() => {
|
||||
const value = props.downloadProgressPercent
|
||||
@@ -262,9 +264,9 @@ const progressBarWidth = computed(() => {
|
||||
})
|
||||
const actionButtonLabel = computed(() => {
|
||||
if (updating.value) {
|
||||
return updatePhase.value === 'restart' ? '重启中...' : '下载中...'
|
||||
return legacyT(updatePhase.value === 'restart' ? '重启中...' : '下载中...')
|
||||
}
|
||||
return updatePhase.value === 'restart' ? '立即重启' : '立即更新'
|
||||
return legacyT(updatePhase.value === 'restart' ? '立即重启' : '立即更新')
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
@@ -279,7 +281,7 @@ const formattedPublishedAt = computed(() => {
|
||||
if (!props.publishedAt) return ''
|
||||
try {
|
||||
const date = new Date(props.publishedAt)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
return date.toLocaleDateString(locale.value, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
class="flex h-9 w-9 items-center justify-center rounded-lg transition"
|
||||
:class="buttonClass"
|
||||
:title="buttonTitle"
|
||||
aria-label="版本信息"
|
||||
:aria-label="$legacyT('版本信息')"
|
||||
>
|
||||
<Info
|
||||
v-if="!isReconnecting"
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="flex items-center justify-between gap-3 border-b border-border/60 bg-muted/30 px-3 py-2.5">
|
||||
<div>
|
||||
<div class="text-xs font-semibold text-foreground">
|
||||
版本信息
|
||||
{{ $legacyT('版本信息') }}
|
||||
</div>
|
||||
<div class="mt-0.5 text-[10px] uppercase tracking-[0.3em] text-muted-foreground">
|
||||
System version
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="rounded-lg border border-border/60 bg-muted/20 px-3 py-2.5">
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
当前版本
|
||||
{{ $legacyT('当前版本') }}
|
||||
</p>
|
||||
<p class="mt-1 break-all font-mono text-sm text-foreground">
|
||||
{{ currentVersionLabel }}
|
||||
@@ -59,7 +59,7 @@
|
||||
class="mt-2"
|
||||
>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
最新版本
|
||||
{{ $legacyT('最新版本') }}
|
||||
</p>
|
||||
<p class="mt-1 break-all font-mono text-sm text-foreground">
|
||||
{{ latestVersionLabel }}
|
||||
@@ -71,7 +71,7 @@
|
||||
v-if="status?.error"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
检查更新失败:{{ status.error }}
|
||||
{{ $legacyT('检查更新失败:') }}{{ $legacyT(status.error) }}
|
||||
</p>
|
||||
|
||||
<p
|
||||
@@ -87,7 +87,7 @@
|
||||
class="flex items-center justify-center gap-2 rounded-lg border border-primary/20 bg-primary/5 px-3 py-2 text-primary"
|
||||
>
|
||||
<RefreshCw class="h-3.5 w-3.5 animate-spin" />
|
||||
<span class="text-xs font-medium">服务重启中,请稍候...</span>
|
||||
<span class="text-xs font-medium">{{ $legacyT('服务重启中,请稍候...') }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -126,7 +126,7 @@
|
||||
class="mr-2 h-3.5 w-3.5"
|
||||
:class="loading ? 'animate-spin' : ''"
|
||||
/>
|
||||
重新检查
|
||||
{{ $legacyT('重新检查') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="rollbackAvailable"
|
||||
@@ -173,7 +173,7 @@
|
||||
class="h-3 w-3 transition-transform"
|
||||
:class="showReleases ? 'rotate-90' : ''"
|
||||
/>
|
||||
历史版本
|
||||
{{ $legacyT('历史版本') }}
|
||||
<span
|
||||
v-if="releases.length > 0"
|
||||
class="text-[10px] text-muted-foreground/60"
|
||||
@@ -209,15 +209,15 @@
|
||||
<span
|
||||
v-if="release.is_current"
|
||||
class="shrink-0 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"
|
||||
>当前</span>
|
||||
>{{ $legacyT('当前') }}</span>
|
||||
<span
|
||||
v-else-if="release.is_newer"
|
||||
class="shrink-0 rounded-full bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-medium text-emerald-600 dark:text-emerald-400"
|
||||
>新</span>
|
||||
>{{ $legacyT('新') }}</span>
|
||||
<span
|
||||
v-if="release.is_newer && release.updatable === false"
|
||||
class="shrink-0 rounded-full bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400"
|
||||
>不可在线更新</span>
|
||||
>{{ $legacyT('不可在线更新') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="release.published_at"
|
||||
@@ -233,14 +233,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-2 shrink-0 text-[10px] font-medium text-muted-foreground">
|
||||
详情
|
||||
{{ $legacyT('详情') }}
|
||||
</span>
|
||||
</button>
|
||||
<p
|
||||
v-if="!loadingReleases && releases.length === 0 && !releasesError"
|
||||
class="py-2 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
暂无版本信息
|
||||
{{ $legacyT('暂无版本信息') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,19 +267,19 @@
|
||||
v-if="selectedRelease.is_current"
|
||||
class="rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-medium text-primary"
|
||||
>
|
||||
当前运行版本
|
||||
{{ $legacyT('当前运行版本') }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="selectedRelease.is_newer"
|
||||
class="rounded-full bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-600 dark:text-emerald-400"
|
||||
>
|
||||
可升级版本
|
||||
{{ $legacyT('可升级版本') }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground"
|
||||
>
|
||||
历史版本
|
||||
{{ $legacyT('历史版本') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -299,7 +299,7 @@
|
||||
v-else
|
||||
class="rounded-lg bg-muted/30 px-3 py-4 text-sm text-muted-foreground"
|
||||
>
|
||||
这个版本没有附带更新说明。
|
||||
{{ $legacyT('这个版本没有附带更新说明。') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -308,7 +308,7 @@
|
||||
variant="outline"
|
||||
@click="showReleaseDetails = false"
|
||||
>
|
||||
关闭
|
||||
{{ $legacyT('关闭') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="selectedRelease?.release_url"
|
||||
@@ -316,7 +316,7 @@
|
||||
@click="handleOpenSelectedReleasePage"
|
||||
>
|
||||
<ExternalLink class="mr-2 h-3.5 w-3.5" />
|
||||
查看标签页
|
||||
{{ $legacyT('查看标签页') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="canUseSelectedRelease"
|
||||
@@ -342,6 +342,7 @@ import { normalizeReleaseNotesForDisplay } from '@/utils/releaseNotes'
|
||||
import { formatDisplayVersion } from '@/utils/version'
|
||||
import { describeUpdateStatus } from '@/utils/updateStatus'
|
||||
import { sanitizeMarkdown } from '@/utils/sanitize'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { marked } from 'marked'
|
||||
import { ChevronRight, ExternalLink, Info, RefreshCw } from 'lucide-vue-next'
|
||||
|
||||
@@ -363,6 +364,7 @@ const emit = defineEmits<{
|
||||
previewRelease: [release: ReleaseEntry]
|
||||
rollback: []
|
||||
}>()
|
||||
const { legacyT, locale } = useI18n()
|
||||
const SOURCE_BUILD_UPDATE_HINT = '当前为源码构建,请使用 git pull 后重新编译。'
|
||||
const SOURCE_BUILD_RELEASE_HINT = '当前为源码构建,请手动切换到对应标签后重新编译。'
|
||||
|
||||
@@ -385,7 +387,7 @@ const isReconnecting = computed(() => updatePhase.value === 'reconnecting')
|
||||
const isDownloadingUpdate = computed(() => updating.value && updatePhase.value === 'download')
|
||||
const isBusy = computed(() => updating.value || rollingBack.value || isReconnecting.value)
|
||||
const canApplyUpdate = computed(() => updateSupported.value && props.status?.updatable !== false)
|
||||
const downloadProgressText = computed(() => props.downloadProgressText || '正在下载更新包...')
|
||||
const downloadProgressText = computed(() => legacyT(props.downloadProgressText || '正在下载更新包...'))
|
||||
const downloadProgressPercent = computed(() => {
|
||||
const value = props.downloadProgressPercent
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
@@ -397,11 +399,11 @@ const progressBarWidth = computed(() => {
|
||||
})
|
||||
const updateBlockerText = computed(() => {
|
||||
if (!updateSupported.value) {
|
||||
return props.status?.update_blocker || SOURCE_BUILD_UPDATE_HINT
|
||||
return legacyT(props.status?.update_blocker || SOURCE_BUILD_UPDATE_HINT)
|
||||
}
|
||||
return props.status?.update_blocker || '当前版本暂不支持在线更新'
|
||||
return legacyT(props.status?.update_blocker || '当前版本暂不支持在线更新')
|
||||
})
|
||||
const releaseButtonLabel = computed(() => updateSupported.value ? '查看更新' : '查看发布')
|
||||
const releaseButtonLabel = computed(() => legacyT(updateSupported.value ? '查看更新' : '查看发布'))
|
||||
const buttonClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
@@ -427,15 +429,15 @@ const buttonClass = computed(() => {
|
||||
return classes
|
||||
})
|
||||
const statusLabel = computed(() => {
|
||||
if (isReconnecting.value) return '重启中'
|
||||
if (rollingBack.value) return '回滚中'
|
||||
if (updating.value) return '更新中'
|
||||
return describeUpdateStatus(props.status)
|
||||
if (isReconnecting.value) return legacyT('重启中')
|
||||
if (rollingBack.value) return legacyT('回滚中')
|
||||
if (updating.value) return legacyT('更新中')
|
||||
return legacyT(describeUpdateStatus(props.status))
|
||||
})
|
||||
const currentVersionLabel = computed(() => {
|
||||
return props.status?.current_version
|
||||
? formatDisplayVersion(props.status.current_version)
|
||||
: '加载中...'
|
||||
: legacyT('加载中...')
|
||||
})
|
||||
const latestVersionLabel = computed(() => {
|
||||
return props.status?.latest_version
|
||||
@@ -452,25 +454,25 @@ const statusPillClass = computed(() => {
|
||||
return 'border-border/60 bg-background/70 text-muted-foreground'
|
||||
})
|
||||
const buttonTitle = computed(() => {
|
||||
if (isReconnecting.value) return '服务重启中...'
|
||||
if (!props.status) return '版本信息'
|
||||
return `版本信息:${statusLabel.value}`
|
||||
if (isReconnecting.value) return legacyT('服务重启中...')
|
||||
if (!props.status) return legacyT('版本信息')
|
||||
return `${legacyT('版本信息:')}${statusLabel.value}`
|
||||
})
|
||||
const actionButtonLabel = computed(() => {
|
||||
if (updating.value) {
|
||||
return updatePhase.value === 'restart' ? '重启中...' : '下载中...'
|
||||
return legacyT(updatePhase.value === 'restart' ? '重启中...' : '下载中...')
|
||||
}
|
||||
return updatePhase.value === 'restart' ? '立即重启' : '立即更新'
|
||||
return legacyT(updatePhase.value === 'restart' ? '立即重启' : '立即更新')
|
||||
})
|
||||
const selectedReleaseTitle = computed(() => {
|
||||
return selectedRelease.value
|
||||
? `版本详情 · ${formatDisplayVersion(selectedRelease.value.version)}`
|
||||
: '版本详情'
|
||||
? `${legacyT('版本详情')} · ${formatDisplayVersion(selectedRelease.value.version)}`
|
||||
: legacyT('版本详情')
|
||||
})
|
||||
const selectedReleaseDescription = computed(() => {
|
||||
return selectedRelease.value?.published_at
|
||||
? `发布于 ${formatDate(selectedRelease.value.published_at)}`
|
||||
: '查看该版本的发布说明'
|
||||
? `${legacyT('发布于')} ${formatDate(selectedRelease.value.published_at)}`
|
||||
: legacyT('查看该版本的发布说明')
|
||||
})
|
||||
const canUseSelectedRelease = computed(() => {
|
||||
return !!selectedRelease.value &&
|
||||
@@ -479,19 +481,19 @@ const canUseSelectedRelease = computed(() => {
|
||||
updateSupported.value
|
||||
})
|
||||
const selectedReleaseActionLabel = computed(() => {
|
||||
if (!selectedRelease.value) return '切换到此版本'
|
||||
return selectedRelease.value.is_newer ? '更新到此版本' : '切换到此版本'
|
||||
if (!selectedRelease.value) return legacyT('切换到此版本')
|
||||
return legacyT(selectedRelease.value.is_newer ? '更新到此版本' : '切换到此版本')
|
||||
})
|
||||
const selectedReleaseHelpText = computed(() => {
|
||||
if (!selectedRelease.value) return ''
|
||||
if (selectedRelease.value.is_current) return '当前正在运行这个版本。'
|
||||
if (selectedRelease.value.is_current) return legacyT('当前正在运行这个版本。')
|
||||
if (!updateSupported.value) {
|
||||
return selectedRelease.value.update_blocker || SOURCE_BUILD_RELEASE_HINT
|
||||
return legacyT(selectedRelease.value.update_blocker || SOURCE_BUILD_RELEASE_HINT)
|
||||
}
|
||||
if (selectedRelease.value.update_blocker) return selectedRelease.value.update_blocker
|
||||
return selectedRelease.value.is_newer
|
||||
if (selectedRelease.value.update_blocker) return legacyT(selectedRelease.value.update_blocker)
|
||||
return legacyT(selectedRelease.value.is_newer
|
||||
? '将这个版本作为在线更新目标。'
|
||||
: '将切换到这个历史版本。'
|
||||
: '将切换到这个历史版本。')
|
||||
})
|
||||
const selectedReleaseDisplayNotes = computed(() => {
|
||||
return normalizeReleaseNotesForDisplay(selectedRelease.value?.release_notes)
|
||||
@@ -516,7 +518,7 @@ const selectedReleaseNotesHtml = computed(() => {
|
||||
function formatDate(dateStr: string): string {
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
return date.toLocaleDateString(locale.value, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
@@ -533,10 +535,10 @@ async function fetchReleases(force = false) {
|
||||
try {
|
||||
const data = await adminApi.getSystemReleases(force)
|
||||
releases.value = data.releases
|
||||
releasesError.value = data.error
|
||||
releasesError.value = data.error ? legacyT(data.error) : null
|
||||
releasesFetched = true
|
||||
} catch (err) {
|
||||
releasesError.value = err instanceof Error ? err.message : '获取版本列表失败'
|
||||
releasesError.value = legacyT(err instanceof Error ? err.message : '获取版本列表失败')
|
||||
} finally {
|
||||
loadingReleases.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user