fix: localize English UI strings

This commit is contained in:
Heatherm Huang
2026-06-30 20:05:23 +08:00
parent d3acd8e96e
commit 6e54776509
24 changed files with 322 additions and 196 deletions
+8 -8
View File
@@ -155,7 +155,7 @@
class="channel-tab"
:class="activeTab === 'basic' ? 'channel-tab-active' : 'channel-tab-inactive'"
>
{{ t('admin.channels.form.basicSettings', '基础设置') }}
{{ t('admin.channels.form.basicSettings', 'Basic Settings') }}
</button>
<!-- Platform Tabs (only enabled) -->
<button
@@ -230,7 +230,7 @@
<!-- Platform Management -->
<div class="space-y-3">
<label class="input-label mb-0">{{ t('admin.channels.form.platformConfig', '平台配置') }}</label>
<label class="input-label mb-0">{{ t('admin.channels.form.platformConfig', 'Platform Configuration') }}</label>
<div class="flex flex-wrap gap-2">
<label
v-for="p in platformOrder"
@@ -283,7 +283,7 @@
<label class="input-label text-xs">
{{ t('admin.channels.form.groups', 'Associated Groups') }} <span class="text-red-500">*</span>
<span v-if="section.group_ids.length > 0" class="ml-1 font-normal text-gray-400">
({{ t('admin.channels.form.selectedCount', { count: section.group_ids.length }, `已选 ${section.group_ids.length} 个`) }})
({{ t('admin.channels.form.selectedCount', { count: section.group_ids.length }, `${section.group_ids.length} selected`) }})
</span>
</label>
<div class="max-h-40 overflow-auto rounded-lg border border-gray-200 bg-gray-50 p-2 dark:border-dark-600 dark:bg-dark-900">
@@ -1434,14 +1434,14 @@ async function handleSubmit() {
for (const section of form.platforms.filter(s => s.enabled)) {
if (section.group_ids.length === 0) {
const platformLabel = t('admin.groups.platforms.' + section.platform, section.platform)
appStore.showError(t('admin.channels.noGroupsSelected', { platform: platformLabel }, `${platformLabel} 平台未选择分组,请至少选择一个分组或禁用该平台`))
appStore.showError(t('admin.channels.noGroupsSelected', { platform: platformLabel }, `${platformLabel} has no selected group. Select at least one group or disable this platform.`))
activeTab.value = section.platform
return
}
for (const entry of section.model_pricing) {
if (entry.models.length === 0) {
const platformLabel = t('admin.groups.platforms.' + section.platform, section.platform)
appStore.showError(t('admin.channels.emptyModelsInPricing', { platform: platformLabel }, `${platformLabel} 平台下有定价条目未添加模型,请添加模型或删除该条目`))
appStore.showError(t('admin.channels.emptyModelsInPricing', { platform: platformLabel }, `${platformLabel} has a pricing entry without models. Add models or remove the entry.`))
activeTab.value = section.platform
return
}
@@ -1460,7 +1460,7 @@ async function handleSubmit() {
appStore.showError(
t('admin.channels.modelConflict',
{ model1: pricingConflict[0], model2: pricingConflict[1] },
`模型模式 '${pricingConflict[0]}' 和 '${pricingConflict[1]}' 冲突:匹配范围重叠`)
`Model patterns '${pricingConflict[0]}' and '${pricingConflict[1]}' conflict: overlapping match range.`)
)
activeTab.value = section.platform
return
@@ -1473,7 +1473,7 @@ async function handleSubmit() {
appStore.showError(
t('admin.channels.mappingConflict',
{ model1: mappingConflict[0], model2: mappingConflict[1] },
`模型映射源 '${mappingConflict[0]}' 和 '${mappingConflict[1]}' 冲突:匹配范围重叠`)
`Mapping source patterns '${mappingConflict[0]}' and '${mappingConflict[1]}' conflict: overlapping match range.`)
)
activeTab.value = section.platform
return
@@ -1488,7 +1488,7 @@ async function handleSubmit() {
if ((entry.billing_mode === 'per_request' || entry.billing_mode === 'image') &&
(entry.per_request_price == null || entry.per_request_price === '') &&
(!entry.intervals || entry.intervals.length === 0)) {
appStore.showError(t('admin.channels.form.perRequestPriceRequired', '按次/图片计费模式必须设置默认价格或至少一个计费层级'))
appStore.showError(t('admin.channels.form.perRequestPriceRequired', 'Per-request price or billing tiers required for per-request/image billing mode'))
return
}
}
+32 -24
View File
@@ -683,8 +683,12 @@
class="flex items-center justify-between gap-2 border-b border-gray-200 bg-gray-50 px-3 py-2 text-xs dark:border-dark-600 dark:bg-dark-800"
>
<span class="text-gray-500 dark:text-gray-400">
已选 {{ createModelsListSelectedCount }} /
{{ createModelsListState.items.length }}
{{
t("admin.groups.modelsList.selectedSummary", {
selected: createModelsListSelectedCount,
total: createModelsListState.items.length,
})
}}
</span>
<div class="flex items-center gap-1.5">
<button
@@ -692,14 +696,14 @@
class="rounded px-2 py-1 font-medium text-primary-600 transition-colors hover:bg-primary-50 dark:text-primary-400 dark:hover:bg-primary-900/20"
@click="selectAllModelsListItems(createModelsListState)"
>
全选
{{ t("admin.groups.modelsList.selectAll") }}
</button>
<button
type="button"
class="rounded px-2 py-1 font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dark-700"
@click="invertModelsListSelection(createModelsListState)"
>
反选
{{ t("admin.groups.modelsList.invertSelection") }}
</button>
</div>
</div>
@@ -1297,20 +1301,20 @@
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4 space-y-4"
>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
账号过滤控制
{{ t("admin.groups.accountFilters.title") }}
</h4>
<!-- require_oauth_only toggle -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400"
>仅允许 OAuth 账号</label
>{{ t("admin.groups.accountFilters.oauthOnly") }}</label
>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{
createForm.require_oauth_only
? "已启用 — 排除 API Key 类型账号"
: "未启用"
? t("admin.groups.accountFilters.oauthOnlyEnabled")
: t("admin.groups.accountFilters.disabled")
}}
</p>
</div>
@@ -1341,13 +1345,13 @@
<div class="flex items-center justify-between">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400"
>仅允许隐私保护已设置的账号</label
>{{ t("admin.groups.accountFilters.privacySetOnly") }}</label
>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{
createForm.require_privacy_set
? "已启用 — Privacy 未设置的账号将被排除"
: "未启用"
? t("admin.groups.accountFilters.privacySetOnlyEnabled")
: t("admin.groups.accountFilters.disabled")
}}
</p>
</div>
@@ -1971,8 +1975,12 @@
class="flex items-center justify-between gap-2 border-b border-gray-200 bg-gray-50 px-3 py-2 text-xs dark:border-dark-600 dark:bg-dark-800"
>
<span class="text-gray-500 dark:text-gray-400">
已选 {{ editModelsListSelectedCount }} /
{{ editModelsListState.items.length }}
{{
t("admin.groups.modelsList.selectedSummary", {
selected: editModelsListSelectedCount,
total: editModelsListState.items.length,
})
}}
</span>
<div class="flex items-center gap-1.5">
<button
@@ -1980,14 +1988,14 @@
class="rounded px-2 py-1 font-medium text-primary-600 transition-colors hover:bg-primary-50 dark:text-primary-400 dark:hover:bg-primary-900/20"
@click="selectAllModelsListItems(editModelsListState)"
>
全选
{{ t("admin.groups.modelsList.selectAll") }}
</button>
<button
type="button"
class="rounded px-2 py-1 font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-dark-700"
@click="invertModelsListSelection(editModelsListState)"
>
反选
{{ t("admin.groups.modelsList.invertSelection") }}
</button>
</div>
</div>
@@ -2581,20 +2589,20 @@
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4 space-y-4"
>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
账号过滤控制
{{ t("admin.groups.accountFilters.title") }}
</h4>
<!-- require_oauth_only toggle -->
<div class="flex items-center justify-between">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400"
>仅允许 OAuth 账号</label
>{{ t("admin.groups.accountFilters.oauthOnly") }}</label
>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{
editForm.require_oauth_only
? "已启用 — 排除 API Key 类型账号"
: "未启用"
? t("admin.groups.accountFilters.oauthOnlyEnabled")
: t("admin.groups.accountFilters.disabled")
}}
</p>
</div>
@@ -2625,13 +2633,13 @@
<div class="flex items-center justify-between">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400"
>仅允许隐私保护已设置的账号</label
>{{ t("admin.groups.accountFilters.privacySetOnly") }}</label
>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{
editForm.require_privacy_set
? "已启用 — Privacy 未设置的账号将被排除"
: "未启用"
? t("admin.groups.accountFilters.privacySetOnlyEnabled")
: t("admin.groups.accountFilters.disabled")
}}
</p>
</div>
@@ -3241,7 +3249,7 @@ const copyAccountsGroupOptions = computed(() => {
);
return eligibleGroups.map((g) => ({
value: g.id,
label: `${g.name} (${g.account_count || 0} 个账号)`,
label: `${g.name} (${t("admin.groups.accountsCount", { count: g.account_count || 0 })})`,
}));
});
@@ -3256,7 +3264,7 @@ const copyAccountsGroupOptionsForEdit = computed(() => {
);
return eligibleGroups.map((g) => ({
value: g.id,
label: `${g.name} (${g.account_count || 0} 个账号)`,
label: `${g.name} (${t("admin.groups.accountsCount", { count: g.account_count || 0 })})`,
}));
});
+4 -3
View File
@@ -1184,6 +1184,7 @@ const riskThresholdCategories = Object.keys(riskThresholdDefaults)
const { t } = useI18n()
const appStore = useAppStore()
const defaultBlockMessage = () => t('admin.riskControl.defaultBlockMessage')
const loading = ref(true)
const saving = ref(false)
@@ -1230,7 +1231,7 @@ const configForm = reactive({
worker_count: 4,
queue_size: 32768,
block_status: 403,
block_message: '内容审计命中风险规则,请调整输入后重试',
block_message: defaultBlockMessage(),
email_on_hit: true,
auto_ban_enabled: true,
cyber_policy_exclude_from_ban_count: false,
@@ -1707,7 +1708,7 @@ function applyConfig(config: ContentModerationConfig) {
configForm.worker_count = config.worker_count || 4
configForm.queue_size = config.queue_size || 32768
configForm.block_status = config.block_status || 403
configForm.block_message = config.block_message || '内容审计命中风险规则,请调整输入后重试'
configForm.block_message = config.block_message || defaultBlockMessage()
configForm.email_on_hit = config.email_on_hit ?? true
configForm.auto_ban_enabled = config.auto_ban_enabled ?? true
configForm.cyber_policy_exclude_from_ban_count = config.cyber_policy_exclude_from_ban_count ?? false
@@ -1788,7 +1789,7 @@ async function saveConfig() {
worker_count: Number(configForm.worker_count) || 4,
queue_size: Number(configForm.queue_size) || 32768,
block_status: Number(configForm.block_status) || 403,
block_message: configForm.block_message || '内容审计命中风险规则,请调整输入后重试',
block_message: configForm.block_message || defaultBlockMessage(),
email_on_hit: configForm.email_on_hit,
auto_ban_enabled: configForm.auto_ban_enabled,
cyber_policy_exclude_from_ban_count: configForm.cyber_policy_exclude_from_ban_count,
+11 -11
View File
@@ -2537,7 +2537,7 @@
<input
v-model="form.dingtalk_connect_sync_display_name_attr_name"
type="text"
placeholder="钉钉姓名"
:placeholder="localText('钉钉姓名', 'DingTalk Name')"
class="input text-sm flex-1 max-w-xs"
/>
</div>
@@ -2583,7 +2583,7 @@
<input
v-model="form.dingtalk_connect_sync_corp_email_attr_name"
type="text"
placeholder="钉钉企业邮箱"
:placeholder="localText('钉钉企业邮箱', 'DingTalk Corporate Email')"
class="input text-sm flex-1 max-w-xs"
/>
</div>
@@ -2629,7 +2629,7 @@
<input
v-model="form.dingtalk_connect_sync_dept_attr_name"
type="text"
placeholder="钉钉部门"
:placeholder="localText('钉钉部门', 'DingTalk Department')"
class="input text-sm flex-1 max-w-xs"
/>
</div>
@@ -7435,22 +7435,22 @@ function defaultLoginAgreementDocuments(): LoginAgreementDocument[] {
return [
{
id: "terms",
title: "服务条款",
title: localText("服务条款", "Terms of Service"),
content_md: "",
},
{
id: "usage-policy",
title: "使用政策",
title: localText("使用政策", "Usage Policy"),
content_md: "",
},
{
id: "supported-regions",
title: "支持的国家和地区",
title: localText("支持的国家和地区", "Supported Countries and Regions"),
content_md: "",
},
{
id: "service-specific-terms",
title: "服务特定条款",
title: localText("服务特定条款", "Service-Specific Terms"),
content_md: "",
},
];
@@ -7989,9 +7989,9 @@ const form = reactive<SettingsForm>({
dingtalk_connect_sync_corp_email_attr_key: "dingtalk_email",
dingtalk_connect_sync_display_name_attr_key: "dingtalk_name",
dingtalk_connect_sync_dept_attr_key: "dingtalk_department",
dingtalk_connect_sync_corp_email_attr_name: "钉钉企业邮箱",
dingtalk_connect_sync_display_name_attr_name: "钉钉姓名",
dingtalk_connect_sync_dept_attr_name: "钉钉部门",
dingtalk_connect_sync_corp_email_attr_name: localText("钉钉企业邮箱", "DingTalk Corporate Email"),
dingtalk_connect_sync_display_name_attr_name: localText("钉钉姓名", "DingTalk Name"),
dingtalk_connect_sync_dept_attr_name: localText("钉钉部门", "DingTalk Department"),
wechat_connect_enabled: false,
wechat_connect_app_id: "",
wechat_connect_app_secret: "",
@@ -8148,7 +8148,7 @@ const authSourceDefaultsMeta = computed(() => [
},
{
source: "dingtalk" as AuthSourceType,
title: "钉钉",
title: t("auth.dingtalkProviderName"),
description: localText(
"通过钉钉首次注册或首次绑定时应用。",
"Applied on first signup or first bind through DingTalk.",
@@ -889,7 +889,7 @@ function handleToolbarRefresh() {
<template v-if="props.autoRefreshEnabled && props.autoRefreshCountdown !== undefined">
<span>·</span>
<span>剩余 {{ props.autoRefreshCountdown }}s</span>
<span>{{ t('admin.ops.autoRefreshRemaining', { seconds: props.autoRefreshCountdown }) }}</span>
</template>
</div>
</div>
@@ -82,7 +82,7 @@ const timeRangeOptions = [
]
const filterLevelOptions = [
{ value: '', label: '全部' },
{ value: '', label: 'All' },
{ value: 'debug', label: 'debug' },
{ value: 'info', label: 'info' },
{ value: 'warn', label: 'warn' },
@@ -203,7 +203,7 @@ const fetchLogs = async () => {
total.value = res.total || 0
} catch (err: any) {
console.error('[OpsSystemLogTable] Failed to fetch logs', err)
appStore.showError(err?.response?.data?.detail || '系统日志加载失败')
appStore.showError(err?.response?.data?.detail || 'Failed to load system logs')
} finally {
loading.value = false
}
@@ -246,17 +246,17 @@ const saveRuntimeConfig = async () => {
runtimeConfig.caller = saved.caller
runtimeConfig.stacktrace_level = saved.stacktrace_level
runtimeConfig.retention_days = saved.retention_days
appStore.showSuccess('日志运行时配置已生效')
appStore.showSuccess('Runtime log configuration is active')
} catch (err: any) {
console.error('[OpsSystemLogTable] Failed to save runtime log config', err)
appStore.showError(err?.response?.data?.detail || '保存日志配置失败')
appStore.showError(err?.response?.data?.detail || 'Failed to save log configuration')
} finally {
runtimeSaving.value = false
}
}
const resetRuntimeConfig = async () => {
const ok = window.confirm('确认回滚为启动配置(env/yaml)并立即生效?')
const ok = window.confirm('Reset to startup configuration (env/yaml) and apply immediately?')
if (!ok) return
runtimeSaving.value = true
@@ -269,18 +269,18 @@ const resetRuntimeConfig = async () => {
runtimeConfig.caller = saved.caller
runtimeConfig.stacktrace_level = saved.stacktrace_level
runtimeConfig.retention_days = saved.retention_days
appStore.showSuccess('已回滚到启动日志配置')
appStore.showSuccess('Reset to startup log configuration')
await fetchHealth()
} catch (err: any) {
console.error('[OpsSystemLogTable] Failed to reset runtime log config', err)
appStore.showError(err?.response?.data?.detail || '回滚日志配置失败')
appStore.showError(err?.response?.data?.detail || 'Failed to reset log configuration')
} finally {
runtimeSaving.value = false
}
}
const cleanupCurrentFilter = async () => {
const ok = window.confirm('确认按当前筛选条件清理系统日志?该操作不可撤销。')
const ok = window.confirm('Clean up system logs matching the current filters? This cannot be undone.')
if (!ok) return
try {
const payload = {
@@ -298,12 +298,12 @@ const cleanupCurrentFilter = async () => {
q: filters.q.trim() || undefined
}
const res = await opsAPI.cleanupSystemLogs(payload)
appStore.showSuccess(`清理完成,删除 ${res.deleted || 0} 条日志`)
appStore.showSuccess(`Cleanup complete. Deleted ${res.deleted || 0} log entries.`)
page.value = 1
await Promise.all([fetchLogs(), fetchHealth()])
} catch (err: any) {
console.error('[OpsSystemLogTable] Failed to cleanup logs', err)
appStore.showError(err?.response?.data?.detail || '清理系统日志失败')
appStore.showError(err?.response?.data?.detail || 'Failed to clean up system logs')
}
}
@@ -368,41 +368,41 @@ onMounted(async () => {
<section class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-dark-700 dark:bg-dark-900/60">
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="text-sm font-bold text-gray-900 dark:text-white">系统日志</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">默认按最新时间倒序,支持筛选搜索与按条件清理。</p>
<h3 class="text-sm font-bold text-gray-900 dark:text-white">System Logs</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Newest logs are shown first. Filter, search, and clean up by condition.</p>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs">
<span class="rounded-md bg-gray-100 px-2 py-1 text-gray-700 dark:bg-dark-700 dark:text-gray-200">队列 {{ health.queue_depth }}/{{ health.queue_capacity }}</span>
<span class="rounded-md bg-gray-100 px-2 py-1 text-gray-700 dark:bg-dark-700 dark:text-gray-200">写入 {{ health.written_count }}</span>
<span class="rounded-md bg-amber-100 px-2 py-1 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">丢弃 {{ health.dropped_count }}</span>
<span class="rounded-md bg-red-100 px-2 py-1 text-red-700 dark:bg-red-900/30 dark:text-red-300">失败 {{ health.write_failed_count }}</span>
<span class="rounded-md bg-gray-100 px-2 py-1 text-gray-700 dark:bg-dark-700 dark:text-gray-200">Queue {{ health.queue_depth }}/{{ health.queue_capacity }}</span>
<span class="rounded-md bg-gray-100 px-2 py-1 text-gray-700 dark:bg-dark-700 dark:text-gray-200">Written {{ health.written_count }}</span>
<span class="rounded-md bg-amber-100 px-2 py-1 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">Dropped {{ health.dropped_count }}</span>
<span class="rounded-md bg-red-100 px-2 py-1 text-red-700 dark:bg-red-900/30 dark:text-red-300">Failed {{ health.write_failed_count }}</span>
</div>
</div>
<div class="mb-4 rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-dark-700 dark:bg-dark-800/70">
<div class="mb-2 flex items-center justify-between">
<div class="text-xs font-semibold text-gray-700 dark:text-gray-200">运行时日志配置(实时生效)</div>
<span v-if="runtimeLoading" class="text-xs text-gray-500">加载中...</span>
<div class="text-xs font-semibold text-gray-700 dark:text-gray-200">Runtime Log Configuration (applies immediately)</div>
<span v-if="runtimeLoading" class="text-xs text-gray-500">Loading...</span>
</div>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-6">
<label class="text-xs text-gray-600 dark:text-gray-300">
级别
Level
<Select v-model="runtimeConfig.level" class="mt-1" :options="runtimeLevelOptions" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
堆栈阈值
Stacktrace threshold
<Select v-model="runtimeConfig.stacktrace_level" class="mt-1" :options="stacktraceLevelOptions" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
采样初始
Sampling initial
<input v-model.number="runtimeConfig.sampling_initial" type="number" min="1" class="input mt-1" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
采样后续
Sampling thereafter
<input v-model.number="runtimeConfig.sampling_thereafter" type="number" min="1" class="input mt-1" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
保留天数
Retention days
<input v-model.number="runtimeConfig.retention_days" type="number" min="1" max="3650" class="input mt-1" />
</label>
<div class="md:col-span-2 xl:col-span-6">
@@ -419,38 +419,38 @@ onMounted(async () => {
</div>
<div class="flex flex-wrap items-center gap-2 lg:justify-end">
<button type="button" class="btn btn-primary btn-sm" :disabled="runtimeSaving" @click="saveRuntimeConfig">
{{ runtimeSaving ? '保存中...' : '保存并生效' }}
{{ runtimeSaving ? 'Saving...' : 'Save and apply' }}
</button>
<button type="button" class="btn btn-secondary btn-sm" :disabled="runtimeSaving" @click="resetRuntimeConfig">
回滚默认值
Reset defaults
</button>
</div>
</div>
</div>
</div>
<p v-if="health.last_error" class="mt-2 text-xs text-red-600 dark:text-red-400">最近写入错误:{{ health.last_error }}</p>
<p v-if="health.last_error" class="mt-2 text-xs text-red-600 dark:text-red-400">Latest write error: {{ health.last_error }}</p>
</div>
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-5">
<label class="text-xs text-gray-600 dark:text-gray-300">
时间范围
Time range
<Select v-model="filters.time_range" class="mt-1" :options="timeRangeOptions" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
开始时间(可选)
Start time (optional)
<input v-model="filters.start_time" type="datetime-local" class="input mt-1" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
结束时间(可选)
End time (optional)
<input v-model="filters.end_time" type="datetime-local" class="input mt-1" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
级别
Level
<Select v-model="filters.level" class="mt-1" :options="filterLevelOptions" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
组件
<input v-model="filters.component" type="text" class="input mt-1" placeholder="如 http.access" />
Component
<input v-model="filters.component" type="text" class="input mt-1" placeholder="e.g. http.access" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
request_id
@@ -473,36 +473,36 @@ onMounted(async () => {
<input v-model="filters.account_id" type="text" class="input mt-1" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
平台
Platform
<input v-model="filters.platform" type="text" class="input mt-1" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
模型
Model
<input v-model="filters.model" type="text" class="input mt-1" />
</label>
<label class="text-xs text-gray-600 dark:text-gray-300">
关键词
<input v-model="filters.q" type="text" class="input mt-1" placeholder="消息/request_id" />
Keyword
<input v-model="filters.q" type="text" class="input mt-1" placeholder="message/request_id" />
</label>
</div>
<div class="mb-3 flex flex-wrap gap-2">
<button type="button" class="btn btn-primary btn-sm" @click="applyFilters">查询</button>
<button type="button" class="btn btn-secondary btn-sm" @click="resetFilters">重置</button>
<button type="button" class="btn btn-danger btn-sm" @click="cleanupCurrentFilter">按当前筛选清理</button>
<button type="button" class="btn btn-secondary btn-sm" @click="fetchHealth">刷新健康指标</button>
<button type="button" class="btn btn-primary btn-sm" @click="applyFilters">Search</button>
<button type="button" class="btn btn-secondary btn-sm" @click="resetFilters">Reset</button>
<button type="button" class="btn btn-danger btn-sm" @click="cleanupCurrentFilter">Clean current filters</button>
<button type="button" class="btn btn-secondary btn-sm" @click="fetchHealth">Refresh health</button>
</div>
<div class="overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700">
<div v-if="loading" class="px-4 py-8 text-center text-sm text-gray-500">加载中...</div>
<div v-else-if="!hasData" class="px-4 py-8 text-center text-sm text-gray-500">暂无系统日志</div>
<div v-if="loading" class="px-4 py-8 text-center text-sm text-gray-500">Loading...</div>
<div v-else-if="!hasData" class="px-4 py-8 text-center text-sm text-gray-500">No system logs</div>
<div v-else class="overflow-auto">
<table class="min-w-full table-fixed divide-y divide-gray-200 dark:divide-dark-700">
<thead class="bg-gray-50 dark:bg-dark-900">
<tr>
<th class="w-[170px] px-3 py-2 text-left text-[11px] font-semibold text-gray-500">时间</th>
<th class="w-[80px] px-3 py-2 text-left text-[11px] font-semibold text-gray-500">级别</th>
<th class="px-3 py-2 text-left text-[11px] font-semibold text-gray-500">日志详细信息</th>
<th class="w-[170px] px-3 py-2 text-left text-[11px] font-semibold text-gray-500">Time</th>
<th class="w-[80px] px-3 py-2 text-left text-[11px] font-semibold text-gray-500">Level</th>
<th class="px-3 py-2 text-left text-[11px] font-semibold text-gray-500">Log Details</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-dark-800">
@@ -295,7 +295,7 @@ const totpTempToken = ref('')
const totpCode = ref('')
const totpError = ref('')
const totpUserEmailMasked = ref('')
const providerName = '钉钉'
const providerName = t('auth.dingtalkProviderName')
const needsCreateAccount = computed(() => pendingAccountAction.value === 'create_account')
const needsChooser = computed(() => pendingAccountAction.value === 'choose_account_action')
+2 -2
View File
@@ -397,7 +397,7 @@ function rejectLoginAgreement(): void {
localStorage.removeItem(LOGIN_AGREEMENT_STORAGE_KEY)
agreementAccepted.value = false
showAgreementModal.value = false
appStore.showWarning('未同意最新条款前,无法输入账号密码或使用快捷登录。')
appStore.showWarning(t('legal.loginAgreementPrompt.loginRejectedWarning'))
}
// ==================== Turnstile Handlers ====================
@@ -428,7 +428,7 @@ function validateForm(): boolean {
let isValid = true
if (agreementGateActive.value) {
appStore.showWarning('请先阅读并同意最新条款后再登录。')
appStore.showWarning(t('legal.loginAgreementPrompt.loginRequiredWarning'))
if (loginAgreementMode.value !== 'checkbox') {
showAgreementModal.value = true
}
+2 -2
View File
@@ -567,7 +567,7 @@ function rejectLoginAgreement(): void {
localStorage.removeItem(LOGIN_AGREEMENT_STORAGE_KEY)
agreementAccepted.value = false
showAgreementModal.value = false
appStore.showWarning('未同意最新条款前,无法注册或使用快捷登录。')
appStore.showWarning(t('legal.loginAgreementPrompt.registerRejectedWarning'))
}
// ==================== Promo Code Validation ====================
@@ -757,7 +757,7 @@ function validateForm(): boolean {
let isValid = true
if (agreementGateActive.value) {
appStore.showWarning('请先阅读并同意最新条款后再注册。')
appStore.showWarning(t('legal.loginAgreementPrompt.registerRequiredWarning'))
if (loginAgreementMode.value !== 'checkbox') {
showAgreementModal.value = true
}
+7 -7
View File
@@ -35,7 +35,7 @@
class="toc-sidebar"
>
<div class="toc-header">
<span class="toc-title">目录</span>
<span class="toc-title">{{ t('customPage.tableOfContents') }}</span>
<button class="toc-close-btn" @click="tocVisible = false">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
</button>
@@ -64,7 +64,7 @@
@click="tocVisible = true"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h18M3 6h18M3 18h18"/></svg>
<span class="ml-1 text-xs">目录</span>
<span class="ml-1 text-xs">{{ t('customPage.tableOfContents') }}</span>
</button>
<!-- Content -->
@@ -317,16 +317,16 @@ function injectCopyButtons() {
if (pre.querySelector('.copy-btn')) return
const btn = document.createElement('button')
btn.className = 'copy-btn'
btn.textContent = '复制'
btn.textContent = t('customPage.copyCode')
btn.addEventListener('click', async () => {
const code = pre.querySelector('code')?.textContent ?? pre.textContent ?? ''
try {
await navigator.clipboard.writeText(code)
btn.textContent = '已复制 ✓'
setTimeout(() => { btn.textContent = '复制' }, 2000)
btn.textContent = t('customPage.copiedCode')
setTimeout(() => { btn.textContent = t('customPage.copyCode') }, 2000)
} catch {
btn.textContent = '失败'
setTimeout(() => { btn.textContent = '复制' }, 2000)
btn.textContent = t('customPage.copyCodeFailed')
setTimeout(() => { btn.textContent = t('customPage.copyCode') }, 2000)
}
})
pre.style.position = 'relative'