feat: add i18n for Risk Control pages
- Add nav.risk key (en: Risk Control, zh: 风控中心) - Add admin.risk.* keys for all UI text in view and components - Replace all hardcoded English strings with t() calls
This commit is contained in:
parent
52ad76e6a4
commit
3333307ec1
@ -17,7 +17,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Account Risk Detail</h2>
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('admin.risk.drawer.title') }}</h2>
|
||||||
<button
|
<button
|
||||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
@ -29,28 +29,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="riskStore.loading && !detail">
|
<template v-if="riskStore.loading && !detail">
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">Loading…</div>
|
<div class="text-sm text-gray-500 dark:text-gray-400">{{ t('admin.risk.loading') }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="detail">
|
<template v-else-if="detail">
|
||||||
<!-- Basic info -->
|
<!-- Basic info -->
|
||||||
<div class="space-y-2 mb-6">
|
<div class="space-y-2 mb-6">
|
||||||
<div class="flex justify-between text-sm">
|
<div class="flex justify-between text-sm">
|
||||||
<span class="text-gray-500 dark:text-gray-400">Email</span>
|
<span class="text-gray-500 dark:text-gray-400">{{ t('admin.risk.drawer.email') }}</span>
|
||||||
<span class="font-medium text-gray-900 dark:text-white">{{ detail.email }}</span>
|
<span class="font-medium text-gray-900 dark:text-white">{{ detail.email }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-sm">
|
<div class="flex justify-between text-sm">
|
||||||
<span class="text-gray-500 dark:text-gray-400">Platform</span>
|
<span class="text-gray-500 dark:text-gray-400">{{ t('admin.risk.drawer.platform') }}</span>
|
||||||
<span class="font-medium text-gray-900 dark:text-white">{{ detail.platform }}</span>
|
<span class="font-medium text-gray-900 dark:text-white">{{ detail.platform }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-sm">
|
<div class="flex justify-between text-sm">
|
||||||
<span class="text-gray-500 dark:text-gray-400">Risk Level</span>
|
<span class="text-gray-500 dark:text-gray-400">{{ t('admin.risk.drawer.riskLevel') }}</span>
|
||||||
<span :class="levelClass(detail.risk_level)" class="font-semibold capitalize">
|
<span :class="levelClass(detail.risk_level)" class="font-semibold capitalize">
|
||||||
{{ detail.risk_level }}
|
{{ detail.risk_level }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<span class="text-gray-500 dark:text-gray-400">Risk Score</span>
|
<span class="text-gray-500 dark:text-gray-400">{{ t('admin.risk.drawer.riskScore') }}</span>
|
||||||
<span class="font-medium text-gray-900 dark:text-white">
|
<span class="font-medium text-gray-900 dark:text-white">
|
||||||
{{ (detail.risk_score * 100).toFixed(1) }}%
|
{{ (detail.risk_score * 100).toFixed(1) }}%
|
||||||
</span>
|
</span>
|
||||||
@ -67,7 +67,7 @@
|
|||||||
|
|
||||||
<!-- Override form -->
|
<!-- Override form -->
|
||||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4 mb-6">
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-4 mb-6">
|
||||||
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-3">Override Risk Level</h3>
|
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-3">{{ t('admin.risk.drawer.overrideTitle') }}</h3>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<select
|
<select
|
||||||
v-model="overrideLevel"
|
v-model="overrideLevel"
|
||||||
@ -80,7 +80,7 @@
|
|||||||
<input
|
<input
|
||||||
v-model="overrideReason"
|
v-model="overrideReason"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Reason (required)"
|
:placeholder="t('admin.risk.drawer.overrideReason')"
|
||||||
class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm text-gray-900 dark:text-white px-3 py-1.5"
|
class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm text-gray-900 dark:text-white px-3 py-1.5"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@ -88,14 +88,14 @@
|
|||||||
class="w-full rounded bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
class="w-full rounded bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
@click="onOverride"
|
@click="onOverride"
|
||||||
>
|
>
|
||||||
{{ overriding ? 'Saving…' : 'Apply Override' }}
|
{{ overriding ? t('admin.risk.drawer.applying') : t('admin.risk.drawer.applyOverride') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 24h behavior trend -->
|
<!-- 24h behavior trend -->
|
||||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||||
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-2">24h Behavior</h3>
|
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-2">{{ t('admin.risk.drawer.behavior24h') }}</h3>
|
||||||
<div class="h-20">
|
<div class="h-20">
|
||||||
<RiskTrendChart :hours="detail.behavior_24h ?? []" />
|
<RiskTrendChart :hours="detail.behavior_24h ?? []" />
|
||||||
</div>
|
</div>
|
||||||
@ -108,6 +108,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRiskStore } from '@/stores/risk'
|
import { useRiskStore } from '@/stores/risk'
|
||||||
import RiskTrendChart from './RiskTrendChart.vue'
|
import RiskTrendChart from './RiskTrendChart.vue'
|
||||||
|
|
||||||
@ -122,6 +123,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const riskStore = useRiskStore()
|
const riskStore = useRiskStore()
|
||||||
|
const { t } = useI18n()
|
||||||
const detail = ref(riskStore.accountDetail)
|
const detail = ref(riskStore.accountDetail)
|
||||||
const overrideLevel = ref('low')
|
const overrideLevel = ref('low')
|
||||||
const overrideReason = ref('')
|
const overrideReason = ref('')
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Total Monitored</p>
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">{{ t('admin.risk.summaryCards.totalMonitored') }}</p>
|
||||||
<p class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
|
<p class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||||
{{ summary?.total_monitored ?? '—' }}
|
{{ summary?.total_monitored ?? '—' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-4">
|
<div class="rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-4">
|
||||||
<p class="text-xs text-red-600 dark:text-red-400 uppercase tracking-wide">High Risk</p>
|
<p class="text-xs text-red-600 dark:text-red-400 uppercase tracking-wide">{{ t('admin.risk.summaryCards.highRisk') }}</p>
|
||||||
<p class="mt-1 text-2xl font-semibold text-red-700 dark:text-red-300">
|
<p class="mt-1 text-2xl font-semibold text-red-700 dark:text-red-300">
|
||||||
{{ summary?.high_risk_count ?? '—' }}
|
{{ summary?.high_risk_count ?? '—' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-yellow-200 dark:border-yellow-800 bg-yellow-50 dark:bg-yellow-900/20 p-4">
|
<div class="rounded-lg border border-yellow-200 dark:border-yellow-800 bg-yellow-50 dark:bg-yellow-900/20 p-4">
|
||||||
<p class="text-xs text-yellow-600 dark:text-yellow-400 uppercase tracking-wide">Medium Risk</p>
|
<p class="text-xs text-yellow-600 dark:text-yellow-400 uppercase tracking-wide">{{ t('admin.risk.summaryCards.mediumRisk') }}</p>
|
||||||
<p class="mt-1 text-2xl font-semibold text-yellow-700 dark:text-yellow-300">
|
<p class="mt-1 text-2xl font-semibold text-yellow-700 dark:text-yellow-300">
|
||||||
{{ summary?.medium_risk_count ?? '—' }}
|
{{ summary?.medium_risk_count ?? '—' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Blocked</p>
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">{{ t('admin.risk.summaryCards.blocked') }}</p>
|
||||||
<p class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
|
<p class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||||
{{ summary?.blocked_count ?? '—' }}
|
{{ summary?.blocked_count ?? '—' }}
|
||||||
</p>
|
</p>
|
||||||
@ -28,9 +28,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { RiskSummary } from '@/api/admin/risk'
|
import type { RiskSummary } from '@/api/admin/risk'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
summary: RiskSummary | null
|
summary: RiskSummary | null
|
||||||
}>()
|
}>()
|
||||||
|
const { t } = useI18n()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4">
|
||||||
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-3">Risk System Settings</h3>
|
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-3">{{ t('admin.risk.settings.title') }}</h3>
|
||||||
<form class="space-y-3" @submit.prevent="onSave">
|
<form class="space-y-3" @submit.prevent="onSave">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Phase</label>
|
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">{{ t('admin.risk.settings.phase') }}</label>
|
||||||
<select
|
<select
|
||||||
v-model="form.phase"
|
v-model="form.phase"
|
||||||
class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm text-gray-900 dark:text-white px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm text-gray-900 dark:text-white px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<option value="off">Off</option>
|
<option value="off">Off</option>
|
||||||
<option value="observe">Observe</option>
|
<option value="observe">{{ t('admin.risk.settings.phaseObserve') }}</option>
|
||||||
<option value="enforce">Enforce</option>
|
<option value="enforce">{{ t('admin.risk.settings.phaseEnforce') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">
|
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||||
Medium Threshold ({{ form.medium_threshold }})
|
{{ t('admin.risk.settings.mediumThreshold', { v: form.medium_threshold }) }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
v-model.number="form.medium_threshold"
|
v-model.number="form.medium_threshold"
|
||||||
@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">
|
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||||
High Threshold ({{ form.high_threshold }})
|
{{ t('admin.risk.settings.highThreshold', { v: form.high_threshold }) }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
v-model.number="form.high_threshold"
|
v-model.number="form.high_threshold"
|
||||||
@ -46,22 +46,23 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="rounded border-gray-300 text-blue-600"
|
class="rounded border-gray-300 text-blue-600"
|
||||||
/>
|
/>
|
||||||
<label for="risk-enabled" class="text-xs text-gray-600 dark:text-gray-400">Enabled</label>
|
<label for="risk-enabled" class="text-xs text-gray-600 dark:text-gray-400">{{ t('admin.risk.settings.enabled') }}</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
class="w-full rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
class="w-full rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{{ saving ? 'Saving…' : 'Save Settings' }}
|
{{ saving ? t('admin.risk.settings.saving') : t('admin.risk.settings.save') }}
|
||||||
</button>
|
</button>
|
||||||
<p v-if="saved" class="text-xs text-green-600 dark:text-green-400 text-center">Saved</p>
|
<p v-if="saved" class="text-xs text-green-600 dark:text-green-400 text-center">{{ t('admin.risk.settings.saved') }}</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRiskStore } from '@/stores/risk'
|
import { useRiskStore } from '@/stores/risk'
|
||||||
import type { RiskSettings } from '@/api/admin/risk'
|
import type { RiskSettings } from '@/api/admin/risk'
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const riskStore = useRiskStore()
|
const riskStore = useRiskStore()
|
||||||
|
const { t } = useI18n()
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const saved = ref(false)
|
const saved = ref(false)
|
||||||
|
|
||||||
|
|||||||
@ -591,7 +591,7 @@ const adminNavItems = computed((): NavItem[] => {
|
|||||||
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true },
|
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true },
|
||||||
{ path: '/admin/promo-codes', label: t('nav.promoCodes'), icon: GiftIcon, hideInSimpleMode: true },
|
{ path: '/admin/promo-codes', label: t('nav.promoCodes'), icon: GiftIcon, hideInSimpleMode: true },
|
||||||
{ path: '/admin/usage', label: t('nav.usage'), icon: ChartIcon },
|
{ path: '/admin/usage', label: t('nav.usage'), icon: ChartIcon },
|
||||||
{ path: '/admin/risk', label: 'Risk Control', icon: ShieldExclamationIcon }
|
{ path: '/admin/risk', label: t('nav.risk'), icon: ShieldExclamationIcon }
|
||||||
]
|
]
|
||||||
|
|
||||||
// 简单模式下,在系统设置前插入 API密钥
|
// 简单模式下,在系统设置前插入 API密钥
|
||||||
|
|||||||
@ -352,7 +352,8 @@ export default {
|
|||||||
mySubscriptions: 'My Subscriptions',
|
mySubscriptions: 'My Subscriptions',
|
||||||
buySubscription: 'Recharge / Subscription',
|
buySubscription: 'Recharge / Subscription',
|
||||||
docs: 'Docs',
|
docs: 'Docs',
|
||||||
sora: 'Sora Studio'
|
sora: 'Sora Studio',
|
||||||
|
risk: 'Risk Control'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
@ -4630,6 +4631,57 @@ export default {
|
|||||||
loadFailed: 'Failed to load profiles',
|
loadFailed: 'Failed to load profiles',
|
||||||
saveFailed: 'Failed to save profile',
|
saveFailed: 'Failed to save profile',
|
||||||
deleteFailed: 'Failed to delete profile'
|
deleteFailed: 'Failed to delete profile'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Risk Control
|
||||||
|
risk: {
|
||||||
|
title: 'Risk Control',
|
||||||
|
allLevels: 'All Levels',
|
||||||
|
allPlatforms: 'All Platforms',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
email: 'Email',
|
||||||
|
platform: 'Platform',
|
||||||
|
level: 'Level',
|
||||||
|
score: 'Score',
|
||||||
|
detail: 'Detail',
|
||||||
|
loading: 'Loading…',
|
||||||
|
noAccounts: 'No accounts found',
|
||||||
|
total: '{n} total',
|
||||||
|
prev: 'Prev',
|
||||||
|
next: 'Next',
|
||||||
|
overridden: '(overridden)',
|
||||||
|
riskDistribution: 'Risk Distribution',
|
||||||
|
summaryCards: {
|
||||||
|
totalMonitored: 'Total Monitored',
|
||||||
|
highRisk: 'High Risk',
|
||||||
|
mediumRisk: 'Medium Risk',
|
||||||
|
blocked: 'Blocked'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: 'Risk System Settings',
|
||||||
|
phase: 'Phase',
|
||||||
|
phaseOff: 'Off',
|
||||||
|
phaseObserve: 'Observe',
|
||||||
|
phaseEnforce: 'Enforce',
|
||||||
|
mediumThreshold: 'Medium Threshold ({v})',
|
||||||
|
highThreshold: 'High Threshold ({v})',
|
||||||
|
enabled: 'Enabled',
|
||||||
|
save: 'Save Settings',
|
||||||
|
saving: 'Saving…',
|
||||||
|
saved: 'Saved'
|
||||||
|
},
|
||||||
|
drawer: {
|
||||||
|
title: 'Account Risk Detail',
|
||||||
|
email: 'Email',
|
||||||
|
platform: 'Platform',
|
||||||
|
riskLevel: 'Risk Level',
|
||||||
|
riskScore: 'Risk Score',
|
||||||
|
overrideTitle: 'Override Risk Level',
|
||||||
|
overrideReason: 'Reason (required)',
|
||||||
|
applyOverride: 'Apply Override',
|
||||||
|
applying: 'Saving…',
|
||||||
|
behavior24h: '24h Behavior'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -352,7 +352,8 @@ export default {
|
|||||||
mySubscriptions: '我的订阅',
|
mySubscriptions: '我的订阅',
|
||||||
buySubscription: '充值/订阅',
|
buySubscription: '充值/订阅',
|
||||||
docs: '文档',
|
docs: '文档',
|
||||||
sora: 'Sora 创作'
|
sora: 'Sora 创作',
|
||||||
|
risk: '风控中心'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
@ -4794,6 +4795,57 @@ export default {
|
|||||||
loadFailed: '加载模板失败',
|
loadFailed: '加载模板失败',
|
||||||
saveFailed: '保存模板失败',
|
saveFailed: '保存模板失败',
|
||||||
deleteFailed: '删除模板失败'
|
deleteFailed: '删除模板失败'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 风控中心
|
||||||
|
risk: {
|
||||||
|
title: '风控中心',
|
||||||
|
allLevels: '全部风险等级',
|
||||||
|
allPlatforms: '全部平台',
|
||||||
|
refresh: '刷新',
|
||||||
|
email: '邮箱',
|
||||||
|
platform: '平台',
|
||||||
|
level: '风险等级',
|
||||||
|
score: '风险分',
|
||||||
|
detail: '详情',
|
||||||
|
loading: '加载中…',
|
||||||
|
noAccounts: '暂无账号',
|
||||||
|
total: '共 {n} 条',
|
||||||
|
prev: '上一页',
|
||||||
|
next: '下一页',
|
||||||
|
overridden: '(已覆盖)',
|
||||||
|
riskDistribution: '风险分布',
|
||||||
|
summaryCards: {
|
||||||
|
totalMonitored: '监控总数',
|
||||||
|
highRisk: '高风险',
|
||||||
|
mediumRisk: '中风险',
|
||||||
|
blocked: '已封禁'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
title: '风控系统设置',
|
||||||
|
phase: '运行阶段',
|
||||||
|
phaseOff: '关闭',
|
||||||
|
phaseObserve: '观察',
|
||||||
|
phaseEnforce: '执法',
|
||||||
|
mediumThreshold: '中风险阈值({v})',
|
||||||
|
highThreshold: '高风险阈值({v})',
|
||||||
|
enabled: '启用',
|
||||||
|
save: '保存设置',
|
||||||
|
saving: '保存中…',
|
||||||
|
saved: '已保存'
|
||||||
|
},
|
||||||
|
drawer: {
|
||||||
|
title: '账号风控详情',
|
||||||
|
email: '邮箱',
|
||||||
|
platform: '平台',
|
||||||
|
riskLevel: '风险等级',
|
||||||
|
riskScore: '风险分',
|
||||||
|
overrideTitle: '覆盖风险等级',
|
||||||
|
overrideReason: '原因(必填)',
|
||||||
|
applyOverride: '应用覆盖',
|
||||||
|
applying: '保存中…',
|
||||||
|
behavior24h: '24小时行为'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
class="rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm text-gray-900 dark:text-white px-3 py-1.5"
|
class="rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm text-gray-900 dark:text-white px-3 py-1.5"
|
||||||
@change="applyFilter"
|
@change="applyFilter"
|
||||||
>
|
>
|
||||||
<option value="">All Levels</option>
|
<option value="">{{ t('admin.risk.allLevels') }}</option>
|
||||||
<option value="low">Low</option>
|
<option value="low">Low</option>
|
||||||
<option value="medium">Medium</option>
|
<option value="medium">Medium</option>
|
||||||
<option value="high">High</option>
|
<option value="high">High</option>
|
||||||
@ -24,7 +24,7 @@
|
|||||||
class="rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm text-gray-900 dark:text-white px-3 py-1.5"
|
class="rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm text-gray-900 dark:text-white px-3 py-1.5"
|
||||||
@change="applyFilter"
|
@change="applyFilter"
|
||||||
>
|
>
|
||||||
<option value="">All Platforms</option>
|
<option value="">{{ t('admin.risk.allPlatforms') }}</option>
|
||||||
<option value="claude">Claude</option>
|
<option value="claude">Claude</option>
|
||||||
<option value="openai">OpenAI</option>
|
<option value="openai">OpenAI</option>
|
||||||
<option value="gemini">Gemini</option>
|
<option value="gemini">Gemini</option>
|
||||||
@ -34,7 +34,7 @@
|
|||||||
class="ml-auto rounded bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
|
class="ml-auto rounded bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700"
|
||||||
@click="applyFilter"
|
@click="applyFilter"
|
||||||
>
|
>
|
||||||
Refresh
|
{{ t('admin.risk.refresh') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -43,10 +43,10 @@
|
|||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead class="bg-gray-50 dark:bg-gray-700/50">
|
<thead class="bg-gray-50 dark:bg-gray-700/50">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Email</th>
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{{ t('admin.risk.email') }}</th>
|
||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Platform</th>
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{{ t('admin.risk.platform') }}</th>
|
||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Level</th>
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{{ t('admin.risk.level') }}</th>
|
||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Score</th>
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">{{ t('admin.risk.score') }}</th>
|
||||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide"></th>
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -59,7 +59,7 @@
|
|||||||
>
|
>
|
||||||
<td class="px-4 py-3 text-gray-900 dark:text-white truncate max-w-[200px]">
|
<td class="px-4 py-3 text-gray-900 dark:text-white truncate max-w-[200px]">
|
||||||
{{ item.email }}
|
{{ item.email }}
|
||||||
<span v-if="item.is_overridden" class="ml-1 text-xs text-blue-500">(overridden)</span>
|
<span v-if="item.is_overridden" class="ml-1 text-xs text-blue-500">{{ t('admin.risk.overridden') }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-gray-600 dark:text-gray-400 capitalize">{{ item.platform }}</td>
|
<td class="px-4 py-3 text-gray-600 dark:text-gray-400 capitalize">{{ item.platform }}</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
@ -71,12 +71,12 @@
|
|||||||
{{ (item.risk_score * 100).toFixed(1) }}%
|
{{ (item.risk_score * 100).toFixed(1) }}%
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-4 py-3 text-right">
|
||||||
<button class="text-blue-600 hover:text-blue-700 text-xs">Detail →</button>
|
<button class="text-blue-600 hover:text-blue-700 text-xs">{{ t('admin.risk.detail') }} →</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!riskStore.accounts?.items?.length">
|
<tr v-if="!riskStore.accounts?.items?.length">
|
||||||
<td colspan="5" class="px-4 py-8 text-center text-gray-400 dark:text-gray-500 text-sm">
|
<td colspan="5" class="px-4 py-8 text-center text-gray-400 dark:text-gray-500 text-sm">
|
||||||
{{ riskStore.loading ? 'Loading…' : 'No accounts found' }}
|
{{ riskStore.loading ? t('admin.risk.loading') : t('admin.risk.noAccounts') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -88,7 +88,7 @@
|
|||||||
class="border-t border-gray-200 dark:border-gray-700 px-4 py-3 flex items-center justify-between"
|
class="border-t border-gray-200 dark:border-gray-700 px-4 py-3 flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{{ riskStore.accounts?.total ?? 0 }} total
|
{{ t('admin.risk.total', { n: riskStore.accounts?.total ?? 0 }) }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@ -96,14 +96,14 @@
|
|||||||
class="rounded border border-gray-300 dark:border-gray-600 px-2 py-1 text-xs disabled:opacity-40"
|
class="rounded border border-gray-300 dark:border-gray-600 px-2 py-1 text-xs disabled:opacity-40"
|
||||||
@click="changePage(filter.page - 1)"
|
@click="changePage(filter.page - 1)"
|
||||||
>
|
>
|
||||||
Prev
|
{{ t('admin.risk.prev') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:disabled="(filter.page * filter.limit) >= (riskStore.accounts?.total ?? 0)"
|
:disabled="(filter.page * filter.limit) >= (riskStore.accounts?.total ?? 0)"
|
||||||
class="rounded border border-gray-300 dark:border-gray-600 px-2 py-1 text-xs disabled:opacity-40"
|
class="rounded border border-gray-300 dark:border-gray-600 px-2 py-1 text-xs disabled:opacity-40"
|
||||||
@click="changePage(filter.page + 1)"
|
@click="changePage(filter.page + 1)"
|
||||||
>
|
>
|
||||||
Next
|
{{ t('admin.risk.next') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -115,7 +115,7 @@
|
|||||||
<RiskSystemStatusCard :settings="riskStore.settings" />
|
<RiskSystemStatusCard :settings="riskStore.settings" />
|
||||||
|
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-3">Risk Distribution</h3>
|
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-3">{{ t('admin.risk.riskDistribution') }}</h3>
|
||||||
<RiskDistributionChart :summary="riskStore.summary" />
|
<RiskDistributionChart :summary="riskStore.summary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -134,6 +134,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRiskStore } from '@/stores/risk'
|
import { useRiskStore } from '@/stores/risk'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||||
import RiskSummaryCards from '@/components/admin/risk/RiskSummaryCards.vue'
|
import RiskSummaryCards from '@/components/admin/risk/RiskSummaryCards.vue'
|
||||||
@ -142,6 +143,7 @@ import RiskSystemStatusCard from '@/components/admin/risk/RiskSystemStatusCard.v
|
|||||||
import RiskAccountDrawer from '@/components/admin/risk/RiskAccountDrawer.vue'
|
import RiskAccountDrawer from '@/components/admin/risk/RiskAccountDrawer.vue'
|
||||||
|
|
||||||
const riskStore = useRiskStore()
|
const riskStore = useRiskStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const drawerOpen = ref(false)
|
const drawerOpen = ref(false)
|
||||||
const selectedAccountId = ref<number | null>(null)
|
const selectedAccountId = ref<number | null>(null)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user