feat: add comprehensive i18n support across all pages

- Add i18n support to admin sidebar, breadcrumbs, and nav-footer
- Add i18n support to admin pages: system-settings, users, versions, risk-control, sessions, cloud-variables, cloud-constants
- Add i18n support to auth pages: login, register
- Add i18n support to install and profile pages
- Add i18n support to agent pages: dashboard, finance, users, apps, cards
- Replace all hard-coded Chinese text with i18n translation keys
- Add comprehensive translation keys to zh.json and en.json
- Fix date formatting to use locale-agnostic toLocaleDateString()
- Pass t function as parameter for i18n in non-component files
This commit is contained in:
2026-05-07 23:06:58 +08:00
parent f1f2a9dc9d
commit c816f4d85c
26 changed files with 1817 additions and 658 deletions
+37 -34
View File
@@ -1,26 +1,29 @@
<script lang="ts" setup>
import { Boxes, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, HardDrive, Hash, Key, Mail, Megaphone, MessageSquare, Monitor, Network, Plug, ScrollText, Settings, Shield, Smartphone, Users, Variable, Share2 } from 'lucide-vue-next'
import { onMounted, onUnmounted, reactive } from 'vue'
import { computed, onMounted, onUnmounted, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import NavTeam from '@/components/app-sidebar/nav-team.vue'
import TeamSwitcher from '@/components/app-sidebar/team-switcher.vue'
import NavFooter from '@/components/admin-sidebar/nav-footer.vue'
const { t } = useI18n()
const user = reactive({
name: '管理员',
name: t('nav.admin'),
email: 'admin@example.com',
avatar: '/avatars/admin.jpg',
role: 'admin',
})
const siteSettings = reactive({
name: '管理后台',
name: t('nav.adminDashboard'),
logo: '',
})
const teams = reactive([
{
name: '管理后台',
name: t('nav.adminDashboard'),
logo: Code,
},
])
@@ -80,148 +83,148 @@ onUnmounted(() => {
window.removeEventListener('system-settings-changed', handleSettingsChange as EventListener)
})
const navMain = [
const navMain = computed(() => [
{
title: '主要功能',
title: t('nav.mainFeatures'),
items: [
{
title: '控制台',
title: t('nav.console'),
url: '/admin',
icon: Gauge,
},
{
title: '应用管理',
title: t('nav.applications'),
url: '/admin/applications',
icon: Boxes,
},
{
title: '卡类管理',
title: t('nav.cardTypes'),
url: '/admin/card-types',
icon: Hash,
},
{
title: '卡密管理',
title: t('nav.cards'),
url: '/admin/cards',
icon: Key,
},
{
title: '公告管理',
title: t('nav.announcements'),
url: '/admin/announcements',
icon: Megaphone,
},
{
title: '版本管理',
title: t('nav.versions'),
url: '/admin/versions',
icon: GitBranch,
},
{
title: '用户管理',
title: t('nav.users'),
url: '/admin/users',
icon: Users,
},
{
title: '设备管理',
title: t('nav.devices'),
url: '/admin/devices',
icon: Smartphone,
},
{
title: '在线实例',
title: t('nav.sessions'),
url: '/admin/sessions',
icon: Monitor,
},
{
title: '代理管理',
title: t('nav.agents'),
url: '/admin/agents',
icon: Network,
},
{
title: '授权管理',
title: t('nav.agentApps'),
url: '/admin/agent-apps',
icon: Share2,
},
],
},
{
title: '业务管理',
title: t('nav.businessManagement'),
items: [
{
title: '财务管理',
title: t('nav.finance'),
url: '/admin/finance',
icon: DollarSign,
},
{
title: '日志记录',
title: t('nav.logs'),
url: '/admin/logs',
icon: ScrollText,
},
{
title: '工单系统',
title: t('nav.tickets'),
url: '/admin/tickets',
icon: MessageSquare,
},
],
},
{
title: '高级功能',
title: t('nav.advancedFeatures'),
items: [
{
title: '云端常量',
title: t('nav.cloudConstants'),
url: '/admin/cloud-constants',
icon: FileLock,
},
{
title: '云端变量',
title: t('nav.cloudVariables'),
url: '/admin/cloud-variables',
icon: Variable,
},
{
title: '云端函数',
title: t('nav.cloudFunction'),
url: '/admin/cloud-function',
icon: Code,
},
{
title: '风控管理',
title: t('nav.riskControl'),
url: '/admin/risk-control',
icon: Shield,
},
{
title: '扩展配置',
title: t('nav.extension'),
url: '/admin/extension',
icon: Plug,
},
],
},
{
title: '系统管理',
title: t('nav.systemManagement'),
items: [
{
title: '系统设置',
title: t('nav.systemSettings'),
url: '/admin/system-settings',
icon: Settings,
},
{
title: '支付渠道',
title: t('nav.paymentChannels'),
url: '/admin/payment-channels',
icon: CreditCard,
},
{
title: '邮箱配置',
title: t('nav.emailSettings'),
url: '/admin/email-settings',
icon: Mail,
},
{
title: '短信配置',
title: t('nav.smsSettings'),
url: '/admin/sms-settings',
icon: Smartphone,
},
{
title: '存储管理',
title: t('nav.storageConfigs'),
url: '/admin/storage-configs',
icon: HardDrive,
},
],
},
]
])
</script>
<template>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { Crown, ChevronsUpDown, Database, LogOut, Ticket, UserRoundCog, Zap } from 'lucide-vue-next'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import type { User } from '@/components/app-sidebar/types'
@@ -8,14 +9,15 @@ import type { User } from '@/components/app-sidebar/types'
import { useSidebar } from '@/components/ui/sidebar'
const router = useRouter()
const { t } = useI18n()
const props = defineProps<{ user: User }>()
const { isMobile, open } = useSidebar()
const subscription = ref({
plan: '基础版',
status: '正常',
plan: t('nav.basicPlan'),
status: t('nav.normal'),
apiQuota: 10000,
apiUsed: 0,
storageQuota: 100 * 1024 * 1024,
@@ -52,8 +54,8 @@ function handleLogout() {
<span class="text-xs font-medium">{{ subscription.plan }}</span>
</div>
<UiBadge
:variant="subscription.status === '正常' ? 'default' : 'secondary'"
:class="subscription.status === '正常' ? 'bg-green-500/10 text-green-500' : ''"
:variant="subscription.status === t('nav.normal') ? 'default' : 'secondary'"
:class="subscription.status === t('nav.normal') ? 'bg-green-500/10 text-green-500' : ''"
class="text-[10px]"
>
{{ subscription.status }}
@@ -80,7 +82,7 @@ function handleLogout() {
<div class="flex items-center justify-between text-[10px]">
<span class="text-muted-foreground flex items-center space-x-1">
<Database class="size-3" />
<span>存储</span>
<span>{{ t('nav.storage') }}</span>
</span>
<span class="font-medium">{{ formatStorage(subscription.storageUsed) }} / {{ formatStorage(subscription.storageQuota) }}</span>
</div>
@@ -140,18 +142,18 @@ function handleLogout() {
<UiDropdownMenuGroup>
<UiDropdownMenuItem @click="router.push('/admin/profile')">
<UserRoundCog class="mr-2 h-4 w-4" />
个人中心
{{ t('nav.profile') }}
</UiDropdownMenuItem>
<UiDropdownMenuItem @click="router.push('/admin/tickets')">
<Ticket class="mr-2 h-4 w-4" />
工单系统
{{ t('nav.tickets') }}
</UiDropdownMenuItem>
</UiDropdownMenuGroup>
<UiDropdownMenuSeparator />
<UiDropdownMenuItem class="text-red-500" @click="handleLogout">
<LogOut class="mr-2 h-4 w-4" />
退出登录
{{ t('nav.logout') }}
</UiDropdownMenuItem>
</UiDropdownMenuContent>
</UiDropdownMenu>
+36 -33
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import AdminSidebar from '@/components/admin-sidebar/index.vue'
@@ -10,44 +11,46 @@ import api from '@/services/api'
import { getFileUrl } from '@/utils/config'
const router = useRouter()
const { t } = useI18n()
const breadcrumbs = computed(() => {
const path = router.currentRoute.value.path
const parts = path.split('/').filter(Boolean)
const crumbs = [{ title: '控制台', path: '/admin' }]
const crumbs = [{ title: t('nav.console'), path: '/admin' }]
const titleMap: Record<string, string> = {
applications: '应用管理',
cards: '卡密管理',
users: '用户管理',
devices: '设备管理',
sessions: '在线实例',
announcements: '公告管理',
versions: '版本管理',
'card-types': '卡类管理',
'agent-apps': '授权管理',
agents: '代理管理',
finance: '财务管理',
logs: '日志记录',
tickets: '工单系统',
'cloud-constants': '云端常量',
'cloud-variables': '云端变量',
'cloud-function': '云端函数',
'risk-control': '风控管理',
extension: '扩展配置',
profile: '个人中心',
'system-settings': '系统设置',
'payment-channels': '支付渠道',
'email-settings': '邮箱配置',
'sms-settings': '短信配置',
'storage-configs': '存储管理',
settings: '基本设置',
security: '安全设置',
create: '创建',
edit: '编辑',
recharge: '充值',
request: '申请授权',
invite: '邀请授权',
applications: t('nav.applications'),
cards: t('nav.cards'),
users: t('nav.users'),
devices: t('nav.devices'),
sessions: t('nav.sessions'),
announcements: t('nav.announcements'),
versions: t('nav.versions'),
'card-types': t('nav.cardTypes'),
'agent-apps': t('nav.agentApps'),
agents: t('nav.agents'),
finance: t('nav.finance'),
logs: t('nav.logs'),
tickets: t('nav.tickets'),
'cloud-constants': t('nav.cloudConstants'),
'cloud-variables': t('nav.cloudVariables'),
'cloud-function': t('nav.cloudFunction'),
'risk-control': t('nav.riskControl'),
extension: t('nav.extension'),
profile: t('nav.profile'),
'system-settings': t('nav.systemSettings'),
'payment-channels': t('nav.paymentChannels'),
'email-settings': t('nav.emailSettings'),
'sms-settings': t('nav.smsSettings'),
'storage-configs': t('nav.storageConfigs'),
settings: t('nav.basicSettings'),
security: t('nav.security'),
create: t('nav.create'),
edit: t('nav.edit'),
recharge: t('nav.recharge'),
request: t('nav.request'),
invite: t('nav.invite'),
records: t('nav.records'),
}
let currentPath = '/admin'
@@ -63,7 +66,7 @@ const breadcrumbs = computed(() => {
}
else if (!isNaN(Number(part)) && i === parts.length - 1) {
crumbs.push({
title: '详情',
title: t('nav.detail'),
path: currentPath,
})
}
+23 -21
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { Boxes, CreditCard, Gauge, Key, LogOut, User, Users } from 'lucide-vue-next'
import { computed, onMounted, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import NavTeam from '@/components/app-sidebar/nav-team.vue'
@@ -10,9 +11,10 @@ import ThemePopover from '@/components/custom-theme/theme-popover.vue'
import ToggleTheme from '@/components/toggle-theme.vue'
const router = useRouter()
const { t } = useI18n()
const user = reactive({
name: '代理商',
name: t('nav.agent'),
email: 'agent@example.com',
avatar: '/avatars/agent.jpg',
role: 'agent',
@@ -36,62 +38,62 @@ onMounted(() => {
const teams = [
{
name: '代理商后台',
name: t('nav.agentDashboard'),
logo: Users,
plan: 'Agent',
},
]
const navMain = [
const navMain = computed(() => [
{
title: '主要功能',
title: t('nav.mainFeatures'),
items: [
{
title: '控制台',
title: t('nav.console'),
url: '/agent',
icon: Gauge,
},
{
title: '应用管理',
title: t('nav.applications'),
url: '/agent/apps',
icon: Boxes,
},
{
title: '卡密管理',
title: t('nav.cards'),
url: '/agent/cards',
icon: Key,
},
{
title: '用户管理',
title: t('nav.users'),
url: '/agent/users',
icon: Users,
},
],
},
{
title: '财务',
title: t('nav.financeGroup'),
items: [
{
title: '财务管理',
title: t('nav.finance'),
url: '/agent/finance',
icon: CreditCard,
},
],
},
]
])
const breadcrumbs = computed(() => {
const path = router.currentRoute.value.path
const parts = path.split('/').filter(Boolean)
const crumbs = [{ title: '控制台', path: '/agent' }]
const crumbs = [{ title: t('nav.console'), path: '/agent' }]
const titleMap: Record<string, string> = {
apps: '应用管理',
cards: '卡密管理',
users: '用户管理',
finance: '财务管理',
profile: '个人中心',
create: '创建',
apps: t('nav.applications'),
cards: t('nav.cards'),
users: t('nav.users'),
finance: t('nav.finance'),
profile: t('nav.profile'),
create: t('nav.create'),
}
let currentPath = '/agent'
@@ -107,7 +109,7 @@ const breadcrumbs = computed(() => {
}
else if (!isNaN(Number(part))) {
crumbs.push({
title: '详情',
title: t('nav.detail'),
path: currentPath,
})
}
@@ -180,14 +182,14 @@ function handleLogout() {
<UiDropdownMenuGroup>
<UiDropdownMenuItem @click="router.push('/agent/profile')">
<User class="mr-2 h-4 w-4" />
个人中心
{{ t('nav.profile') }}
</UiDropdownMenuItem>
</UiDropdownMenuGroup>
<UiDropdownMenuSeparator />
<UiDropdownMenuItem class="text-red-500" @click="handleLogout">
<LogOut class="mr-2 h-4 w-4" />
退出登录
{{ t('nav.logout') }}
</UiDropdownMenuItem>
</UiDropdownMenuContent>
</UiDropdownMenu>
@@ -155,7 +155,7 @@ export function getColumns(actions: {
items.push(
h(DropdownMenuItem, { onClick: () => actions.onDownload(constant) }, () => [
h(Download, { class: 'mr-2 h-4 w-4' }),
'下载文件',
t('admin.cloudConstants.downloadFile'),
]),
)
}
@@ -87,7 +87,7 @@ export function getColumns(actions: {
decimal: t('admin.cloudVariables.types.decimal'),
string: t('admin.cloudVariables.types.string'),
binary: t('admin.cloudVariables.types.binary'),
stream: '记录',
stream: t('admin.cloudVariables.columns.stream'),
}
return h(Badge, { variant: 'outline' }, () => typeMap[type || 'string'] || type || 'string')
},
@@ -189,7 +189,7 @@ export function getColumns(actions: {
items.push(
h(DropdownMenuItem, { onClick: () => actions.onDownload(variable) }, () => [
h(Download, { class: 'mr-2 h-4 w-4' }),
'下载文件',
t('admin.cloudVariables.downloadFile'),
]),
)
}
@@ -1,6 +1,9 @@
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { DataTableProps } from '@/components/data-table/types'
import type { RiskRule } from '@/pages/admin/risk-control/data/schema'
@@ -12,6 +15,8 @@ import DataTableViewOptions from '@/components/data-table/view-options.vue'
import { getColumns } from '@/pages/admin/risk-control/components/columns'
import DataTableToolbar from '@/pages/admin/risk-control/components/data-table-toolbar.vue'
const { t } = useI18n()
const props = defineProps<Omit<DataTableProps<RiskRule>, 'columns'> & {
onToggleStatus: (row: RiskRule) => void
onEdit: (row: RiskRule) => void
@@ -40,13 +45,13 @@ const table = generateVueTable<RiskRule>({
})
const columnLabels: Record<string, string> = {
select: '选择',
type: '类型',
value: '封禁值',
reason: '原因',
status: '状态',
expires_at: '过期时间',
created_at: '创建时间',
select: t('common.select'),
type: t('common.type'),
value: t('admin.riskControl.columns.value'),
reason: t('common.reason'),
status: t('common.status'),
expires_at: t('common.expiresAt'),
created_at: t('common.createdAt'),
}
defineExpose({
@@ -60,13 +65,13 @@ defineExpose({
<div class="space-y-3">
<BulkActions :table="table" entity-name="rules">
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
批量启用
{{ t('common.batchEnable') }}
</UiButton>
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
批量禁用
{{ t('common.batchDisable') }}
</UiButton>
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
批量删除
{{ t('common.batchDelete') }}
</UiButton>
</BulkActions>
<div class="flex flex-wrap items-center justify-between gap-2">
@@ -2,6 +2,7 @@
import type { ColumnDef } from '@tanstack/vue-table'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { DataTableProps } from '@/components/data-table/types'
import type { Session } from '@/pages/admin/sessions/data/schema'
@@ -12,6 +13,8 @@ import DataTableViewOptions from '@/components/data-table/view-options.vue'
import { getColumns } from '@/pages/admin/sessions/components/columns'
import DataTableToolbar from '@/pages/admin/sessions/components/data-table-toolbar.vue'
const { t } = useI18n()
const props = defineProps<Omit<DataTableProps<Session>, 'columns'> & {
applications: { id: number, name: string }[]
appFilter?: string
@@ -35,13 +38,13 @@ const table = generateVueTable<Session>({
})
const columnLabels: Record<string, string> = {
instance_id: '实例标识',
device_identifier: '设备指纹',
device_name: '设备名称',
username: '所属用户',
app_name: '所属应用',
last_heartbeat: '最后心跳',
created_at: '创建时间',
instance_id: t('admin.sessions.columns.instanceId'),
device_identifier: t('admin.sessions.columns.deviceIdentifier'),
device_name: t('admin.sessions.columns.deviceName'),
username: t('admin.sessions.columns.username'),
app_name: t('admin.sessions.columns.appName'),
last_heartbeat: t('admin.sessions.columns.lastHeartbeat'),
created_at: t('common.createdAt'),
}
defineExpose({
+110 -107
View File
@@ -1,11 +1,14 @@
<script setup lang="ts">
import { Bell, Database, Loader2, Settings, Shield, ToggleLeft, Trash2, Upload } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
import { BasicPage } from '@/components/global-layout'
import api from '@/services/api'
const { t } = useI18n()
const loading = ref(true)
const saving = ref(false)
const uploadingLogo = ref(false)
@@ -110,7 +113,7 @@ async function loadSettings() {
}
}
catch (error) {
toast.error('加载设置失败')
toast.error(t('admin.systemSettings.loadFailed'))
}
finally {
loading.value = false
@@ -146,11 +149,11 @@ async function saveSettings() {
...notificationForm.value,
}
await api.put('/dev/system-settings', payload)
toast.success('保存成功')
toast.success(t('admin.systemSettings.saveSuccess'))
updateGlobalSettings()
}
catch (error) {
toast.error('保存失败')
toast.error(t('admin.systemSettings.saveFailed'))
}
finally {
saving.value = false
@@ -171,10 +174,10 @@ async function saveCleanupSettings() {
cleanupLoading.value = true
try {
await api.put('/dev/system-settings/cleanup', cleanupForm.value)
toast.success('清理设置保存成功')
toast.success(t('admin.systemSettings.cleanup.cleanupSaveSuccess'))
}
catch (error) {
toast.error('保存清理设置失败')
toast.error(t('admin.systemSettings.cleanup.cleanupSaveFailed'))
}
finally {
cleanupLoading.value = false
@@ -185,10 +188,10 @@ async function runManualCleanup() {
manualCleanupLoading.value = true
try {
await api.post('/dev/system-settings/cleanup/run')
toast.success('手动清理完成')
toast.success(t('admin.systemSettings.cleanup.manualCleanupSuccess'))
}
catch (error) {
toast.error('手动清理失败')
toast.error(t('admin.systemSettings.cleanup.manualCleanupFailed'))
}
finally {
manualCleanupLoading.value = false
@@ -213,10 +216,10 @@ async function handleLogoUpload(event: Event) {
const data = await api.postFormData<any>('/dev/system-settings/upload', formData)
basicForm.value.site_logo = data.url
toast.success('Logo上传成功')
toast.success(t('admin.systemSettings.upload.logoSuccess'))
}
catch (error: any) {
toast.error(error.message || '上传失败')
toast.error(error.message || t('admin.systemSettings.upload.uploadFailed'))
}
finally {
uploadingLogo.value = false
@@ -242,10 +245,10 @@ async function handleFaviconUpload(event: Event) {
const data = await api.postFormData<any>('/dev/system-settings/upload', formData)
basicForm.value.site_favicon = data.url
toast.success('图标上传成功')
toast.success(t('admin.systemSettings.upload.faviconSuccess'))
}
catch (error: any) {
toast.error(error.message || '上传失败')
toast.error(error.message || t('admin.systemSettings.upload.uploadFailed'))
}
finally {
uploadingFavicon.value = false
@@ -253,14 +256,14 @@ async function handleFaviconUpload(event: Event) {
}
}
const tabs = [
{ id: 'basic', label: '基本设置', icon: Settings },
{ id: 'security', label: '安全设置', icon: Shield },
{ id: 'backup', label: '备份设置', icon: Database },
{ id: 'cleanup', label: '数据清理', icon: Trash2 },
{ id: 'feature', label: '功能设置', icon: ToggleLeft },
{ id: 'notification', label: '通知设置', icon: Bell },
]
const tabs = computed(() => [
{ id: 'basic', label: t('admin.systemSettings.tabs.basic'), icon: Settings },
{ id: 'security', label: t('admin.systemSettings.tabs.security'), icon: Shield },
{ id: 'backup', label: t('admin.systemSettings.tabs.backup'), icon: Database },
{ id: 'cleanup', label: t('admin.systemSettings.tabs.cleanup'), icon: Trash2 },
{ id: 'feature', label: t('admin.systemSettings.tabs.feature'), icon: ToggleLeft },
{ id: 'notification', label: t('admin.systemSettings.tabs.notification'), icon: Bell },
])
onMounted(() => {
loadSettings()
@@ -268,7 +271,7 @@ onMounted(() => {
</script>
<template>
<BasicPage title="系统设置" description="配置系统各项设置">
<BasicPage :title="t('admin.systemSettings.title')" :description="t('admin.systemSettings.description')">
<div v-if="loading" class="flex items-center justify-center py-8">
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
@@ -291,26 +294,26 @@ onMounted(() => {
<UiCard v-show="activeTab === 'basic'">
<UiCardHeader>
<UiCardTitle>基本设置</UiCardTitle>
<UiCardDescription>配置网站基本信息</UiCardDescription>
<UiCardTitle>{{ t('admin.systemSettings.basic.title') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.systemSettings.basic.description') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="site_name">网站名称</UiLabel>
<UiInput id="site_name" v-model="basicForm.site_name" placeholder="请输入网站名称" />
<UiLabel for="site_name">{{ t('admin.systemSettings.basic.siteName') }}</UiLabel>
<UiInput id="site_name" v-model="basicForm.site_name" :placeholder="t('admin.systemSettings.basic.siteNamePlaceholder')" />
</div>
<div class="space-y-2">
<UiLabel>网站Logo</UiLabel>
<UiLabel>{{ t('admin.systemSettings.basic.siteLogo') }}</UiLabel>
<div class="flex items-start gap-4">
<div class="flex-1">
<div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" :disabled="uploadingLogo" @click="triggerLogoUpload">
<Loader2 v-if="uploadingLogo" class="mr-2 h-4 w-4 animate-spin" />
<Upload v-else class="mr-2 h-4 w-4" />
上传Logo
{{ t('admin.systemSettings.basic.uploadLogo') }}
</UiButton>
<span class="text-xs text-muted-foreground">支持 JPGPNGSVG 格式</span>
<span class="text-xs text-muted-foreground">{{ t('admin.systemSettings.basic.logoHint') }}</span>
</div>
<input
ref="logoInputRef"
@@ -332,7 +335,7 @@ onMounted(() => {
class="text-destructive hover:text-destructive"
@click="basicForm.site_logo = ''"
>
移除
{{ t('admin.systemSettings.basic.remove') }}
</UiButton>
</div>
</div>
@@ -340,16 +343,16 @@ onMounted(() => {
</div>
<div class="space-y-2">
<UiLabel>网站图标 (Favicon)</UiLabel>
<UiLabel>{{ t('admin.systemSettings.basic.favicon') }}</UiLabel>
<div class="flex items-start gap-4">
<div class="flex-1">
<div class="flex items-center gap-2">
<UiButton variant="outline" size="sm" :disabled="uploadingFavicon" @click="triggerFaviconUpload">
<Loader2 v-if="uploadingFavicon" class="mr-2 h-4 w-4 animate-spin" />
<Upload v-else class="mr-2 h-4 w-4" />
上传图标
{{ t('admin.systemSettings.basic.uploadFavicon') }}
</UiButton>
<span class="text-xs text-muted-foreground">推荐 32x32 64x64 像素的 ICO/PNG</span>
<span class="text-xs text-muted-foreground">{{ t('admin.systemSettings.basic.faviconHint') }}</span>
</div>
<input
ref="faviconInputRef"
@@ -371,7 +374,7 @@ onMounted(() => {
class="text-destructive hover:text-destructive"
@click="basicForm.site_favicon = ''"
>
移除
{{ t('admin.systemSettings.basic.remove') }}
</UiButton>
</div>
</div>
@@ -379,23 +382,23 @@ onMounted(() => {
</div>
<div class="space-y-2">
<UiLabel for="site_footer">页脚信息</UiLabel>
<UiInput id="site_footer" v-model="basicForm.site_footer" placeholder="请输入页脚信息" />
<UiLabel for="site_footer">{{ t('admin.systemSettings.basic.siteFooter') }}</UiLabel>
<UiInput id="site_footer" v-model="basicForm.site_footer" :placeholder="t('admin.systemSettings.basic.siteFooterPlaceholder')" />
</div>
</UiCardContent>
</UiCard>
<UiCard v-show="activeTab === 'security'">
<UiCardHeader>
<UiCardTitle>安全设置</UiCardTitle>
<UiCardDescription>配置系统安全相关选项</UiCardDescription>
<UiCardTitle>{{ t('admin.systemSettings.security.title') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.systemSettings.security.description') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel>登录验证码</UiLabel>
<UiLabel>{{ t('admin.systemSettings.security.enableCaptcha') }}</UiLabel>
<p class="text-sm text-muted-foreground">
启用后登录时需要输入验证码
{{ t('admin.systemSettings.security.enableCaptchaDesc') }}
</p>
</div>
<UiSwitch v-model="securityForm.enable_captcha" />
@@ -403,7 +406,7 @@ onMounted(() => {
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<UiLabel for="login_fail_lock_count">登录失败锁定次数</UiLabel>
<UiLabel for="login_fail_lock_count">{{ t('admin.systemSettings.security.loginFailLockCount') }}</UiLabel>
<UiInput
id="login_fail_lock_count"
v-model.number="securityForm.login_fail_lock_count"
@@ -412,12 +415,12 @@ onMounted(() => {
max="10"
/>
<p class="text-xs text-muted-foreground">
连续失败多少次后锁定账户
{{ t('admin.systemSettings.security.loginFailLockCountHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="login_fail_lock_minutes">锁定时长分钟</UiLabel>
<UiLabel for="login_fail_lock_minutes">{{ t('admin.systemSettings.security.lockMinutes') }}</UiLabel>
<UiInput
id="login_fail_lock_minutes"
v-model.number="securityForm.login_fail_lock_minutes"
@@ -426,14 +429,14 @@ onMounted(() => {
max="1440"
/>
<p class="text-xs text-muted-foreground">
账户锁定持续时间
{{ t('admin.systemSettings.security.lockMinutesHint') }}
</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<UiLabel for="password_min_length">密码最小长度</UiLabel>
<UiLabel for="password_min_length">{{ t('admin.systemSettings.security.passwordMinLength') }}</UiLabel>
<UiInput
id="password_min_length"
v-model.number="securityForm.password_min_length"
@@ -442,12 +445,12 @@ onMounted(() => {
max="32"
/>
<p class="text-xs text-muted-foreground">
用户密码最小字符数
{{ t('admin.systemSettings.security.passwordMinLengthHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="session_timeout">会话超时小时</UiLabel>
<UiLabel for="session_timeout">{{ t('admin.systemSettings.security.sessionTimeout') }}</UiLabel>
<UiInput
id="session_timeout"
v-model.number="securityForm.session_timeout"
@@ -456,7 +459,7 @@ onMounted(() => {
max="720"
/>
<p class="text-xs text-muted-foreground">
用户登录会话有效期
{{ t('admin.systemSettings.security.sessionTimeoutHint') }}
</p>
</div>
</div>
@@ -465,15 +468,15 @@ onMounted(() => {
<UiCard v-show="activeTab === 'backup'">
<UiCardHeader>
<UiCardTitle>备份设置</UiCardTitle>
<UiCardDescription>配置数据库自动备份</UiCardDescription>
<UiCardTitle>{{ t('admin.systemSettings.backup.title') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.systemSettings.backup.description') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel>启用自动备份</UiLabel>
<UiLabel>{{ t('admin.systemSettings.backup.enableBackup') }}</UiLabel>
<p class="text-sm text-muted-foreground">
定时自动备份数据库
{{ t('admin.systemSettings.backup.enableBackupDesc') }}
</p>
</div>
<UiSwitch v-model="backupForm.enable_backup" />
@@ -481,7 +484,7 @@ onMounted(() => {
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<UiLabel for="backup_interval">备份间隔小时</UiLabel>
<UiLabel for="backup_interval">{{ t('admin.systemSettings.backup.backupInterval') }}</UiLabel>
<UiInput
id="backup_interval"
v-model.number="backupForm.backup_interval"
@@ -490,12 +493,12 @@ onMounted(() => {
max="168"
/>
<p class="text-xs text-muted-foreground">
每隔多少小时备份一次
{{ t('admin.systemSettings.backup.backupIntervalHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="backup_retention">保留天数</UiLabel>
<UiLabel for="backup_retention">{{ t('admin.systemSettings.backup.backupRetention') }}</UiLabel>
<UiInput
id="backup_retention"
v-model.number="backupForm.backup_retention"
@@ -504,37 +507,37 @@ onMounted(() => {
max="90"
/>
<p class="text-xs text-muted-foreground">
备份文件保留多少天
{{ t('admin.systemSettings.backup.backupRetentionHint') }}
</p>
</div>
</div>
<div class="space-y-2">
<UiLabel for="backup_storage_type">存储位置</UiLabel>
<UiLabel for="backup_storage_type">{{ t('admin.systemSettings.backup.storageType') }}</UiLabel>
<UiSelect v-model="backupForm.backup_storage_type">
<UiSelectTrigger>
<UiSelectValue placeholder="选择存储位置" />
<UiSelectValue :placeholder="t('admin.systemSettings.backup.storageTypePlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="local">
本地存储
{{ t('admin.systemSettings.backup.local') }}
</UiSelectItem>
<UiSelectItem value="s3">
S3存储
{{ t('admin.systemSettings.backup.s3') }}
</UiSelectItem>
<UiSelectItem value="webdav">
WebDAV
{{ t('admin.systemSettings.backup.webdav') }}
</UiSelectItem>
<UiSelectItem value="ftp">
FTP
{{ t('admin.systemSettings.backup.ftp') }}
</UiSelectItem>
<UiSelectItem value="sftp">
SFTP
{{ t('admin.systemSettings.backup.sftp') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<p class="text-xs text-muted-foreground">
备份文件存储位置可在存储管理中配置
{{ t('admin.systemSettings.backup.storageTypeHint') }}
</p>
</div>
</UiCardContent>
@@ -542,15 +545,15 @@ onMounted(() => {
<UiCard v-show="activeTab === 'feature'">
<UiCardHeader>
<UiCardTitle>功能设置</UiCardTitle>
<UiCardDescription>配置系统功能开关</UiCardDescription>
<UiCardTitle>{{ t('admin.systemSettings.feature.title') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.systemSettings.feature.description') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel>工单系统</UiLabel>
<UiLabel>{{ t('admin.systemSettings.feature.ticketSystem') }}</UiLabel>
<p class="text-sm text-muted-foreground">
启用后用户可以提交工单
{{ t('admin.systemSettings.feature.ticketSystemDesc') }}
</p>
</div>
<UiSwitch v-model="featureForm.enable_ticket_system" />
@@ -558,34 +561,34 @@ onMounted(() => {
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel>多语言支持</UiLabel>
<UiLabel>{{ t('admin.systemSettings.feature.multiLang') }}</UiLabel>
<p class="text-sm text-muted-foreground">
启用后用户可以切换语言
{{ t('admin.systemSettings.feature.multiLangDesc') }}
</p>
</div>
<UiSwitch v-model="featureForm.enable_multi_lang" />
</div>
<div class="space-y-2">
<UiLabel for="default_theme">默认颜色模式</UiLabel>
<UiLabel for="default_theme">{{ t('admin.systemSettings.feature.defaultTheme') }}</UiLabel>
<UiSelect v-model="featureForm.default_theme">
<UiSelectTrigger>
<UiSelectValue placeholder="选择默认颜色模式" />
<UiSelectValue :placeholder="t('admin.systemSettings.feature.defaultThemePlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="system">
跟随系统
{{ t('admin.systemSettings.feature.followSystem') }}
</UiSelectItem>
<UiSelectItem value="light">
浅色模式
{{ t('admin.systemSettings.feature.lightMode') }}
</UiSelectItem>
<UiSelectItem value="dark">
深色模式
{{ t('admin.systemSettings.feature.darkMode') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<p class="text-xs text-muted-foreground">
新用户默认的颜色模式
{{ t('admin.systemSettings.feature.defaultThemeHint') }}
</p>
</div>
</UiCardContent>
@@ -593,22 +596,22 @@ onMounted(() => {
<UiCard v-show="activeTab === 'notification'">
<UiCardHeader>
<UiCardTitle>通知设置</UiCardTitle>
<UiCardDescription>配置系统通知选项</UiCardDescription>
<UiCardTitle>{{ t('admin.systemSettings.notification.title') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.systemSettings.notification.description') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel>启用邮件通知</UiLabel>
<UiLabel>{{ t('admin.systemSettings.notification.enableNotification') }}</UiLabel>
<p class="text-sm text-muted-foreground">
启用后系统将发送邮件通知
{{ t('admin.systemSettings.notification.enableNotificationDesc') }}
</p>
</div>
<UiSwitch v-model="notificationForm.enable_notification" />
</div>
<div class="space-y-2">
<UiLabel for="admin_notify_email">管理员通知邮箱</UiLabel>
<UiLabel for="admin_notify_email">{{ t('admin.systemSettings.notification.adminNotifyEmail') }}</UiLabel>
<UiInput
id="admin_notify_email"
v-model="notificationForm.admin_notify_email"
@@ -616,20 +619,20 @@ onMounted(() => {
placeholder="admin@example.com"
/>
<p class="text-xs text-muted-foreground">
接收系统通知的管理员邮箱
{{ t('admin.systemSettings.notification.adminNotifyEmailHint') }}
</p>
</div>
<div class="space-y-4">
<UiLabel>通知事件</UiLabel>
<UiLabel>{{ t('admin.systemSettings.notification.notifyEvents') }}</UiLabel>
<div class="space-y-3">
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<p class="text-sm font-medium">
异常登录通知
{{ t('admin.systemSettings.notification.loginNotify') }}
</p>
<p class="text-xs text-muted-foreground">
检测到异常登录时发送通知
{{ t('admin.systemSettings.notification.loginNotifyDesc') }}
</p>
</div>
<UiSwitch v-model="notificationForm.notify_on_login" />
@@ -638,10 +641,10 @@ onMounted(() => {
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<p class="text-sm font-medium">
充值通知
{{ t('admin.systemSettings.notification.rechargeNotify') }}
</p>
<p class="text-xs text-muted-foreground">
用户充值成功时发送通知
{{ t('admin.systemSettings.notification.rechargeNotifyDesc') }}
</p>
</div>
<UiSwitch v-model="notificationForm.notify_on_recharge" />
@@ -650,10 +653,10 @@ onMounted(() => {
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<p class="text-sm font-medium">
工单通知
{{ t('admin.systemSettings.notification.ticketNotify') }}
</p>
<p class="text-xs text-muted-foreground">
新工单提交时发送通知
{{ t('admin.systemSettings.notification.ticketNotifyDesc') }}
</p>
</div>
<UiSwitch v-model="notificationForm.notify_on_ticket" />
@@ -665,15 +668,15 @@ onMounted(() => {
<UiCard v-show="activeTab === 'cleanup'">
<UiCardHeader>
<UiCardTitle>数据清理</UiCardTitle>
<UiCardDescription>配置过期数据自动清理规则释放数据库空间</UiCardDescription>
<UiCardTitle>{{ t('admin.systemSettings.cleanup.title') }}</UiCardTitle>
<UiCardDescription>{{ t('admin.systemSettings.cleanup.description') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<UiLabel>启用自动清理</UiLabel>
<UiLabel>{{ t('admin.systemSettings.cleanup.enableAutoCleanup') }}</UiLabel>
<p class="text-sm text-muted-foreground">
定时自动清理过期数据
{{ t('admin.systemSettings.cleanup.enableAutoCleanupDesc') }}
</p>
</div>
<UiSwitch v-model="cleanupForm.enable_auto_cleanup" />
@@ -681,7 +684,7 @@ onMounted(() => {
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<UiLabel for="cleanup_interval_hours">清理间隔小时</UiLabel>
<UiLabel for="cleanup_interval_hours">{{ t('admin.systemSettings.cleanup.cleanupInterval') }}</UiLabel>
<UiInput
id="cleanup_interval_hours"
v-model.number="cleanupForm.cleanup_interval_hours"
@@ -690,12 +693,12 @@ onMounted(() => {
max="168"
/>
<p class="text-xs text-muted-foreground">
每隔多少小时执行一次清理
{{ t('admin.systemSettings.cleanup.cleanupIntervalHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="captcha_retention_days">验证码保留天数</UiLabel>
<UiLabel for="captcha_retention_days">{{ t('admin.systemSettings.cleanup.captchaRetention') }}</UiLabel>
<UiInput
id="captcha_retention_days"
v-model.number="cleanupForm.captcha_retention_days"
@@ -704,14 +707,14 @@ onMounted(() => {
max="30"
/>
<p class="text-xs text-muted-foreground">
图形验证码过期后保留天数
{{ t('admin.systemSettings.cleanup.captchaRetentionHint') }}
</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<UiLabel for="verify_code_retention_days">邮箱/短信验证码保留天数</UiLabel>
<UiLabel for="verify_code_retention_days">{{ t('admin.systemSettings.cleanup.verifyCodeRetention') }}</UiLabel>
<UiInput
id="verify_code_retention_days"
v-model.number="cleanupForm.verify_code_retention_days"
@@ -720,12 +723,12 @@ onMounted(() => {
max="90"
/>
<p class="text-xs text-muted-foreground">
已使用的邮箱/短信验证码保留天数
{{ t('admin.systemSettings.cleanup.verifyCodeRetentionHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="api_usage_retention_days">API调用日志保留天数</UiLabel>
<UiLabel for="api_usage_retention_days">{{ t('admin.systemSettings.cleanup.apiUsageRetention') }}</UiLabel>
<UiInput
id="api_usage_retention_days"
v-model.number="cleanupForm.api_usage_retention_days"
@@ -734,14 +737,14 @@ onMounted(() => {
max="365"
/>
<p class="text-xs text-muted-foreground">
API调用统计记录保留天数
{{ t('admin.systemSettings.cleanup.apiUsageRetentionHint') }}
</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<UiLabel for="webhook_log_retention_days">Webhook日志保留天数</UiLabel>
<UiLabel for="webhook_log_retention_days">{{ t('admin.systemSettings.cleanup.webhookLogRetention') }}</UiLabel>
<UiInput
id="webhook_log_retention_days"
v-model.number="cleanupForm.webhook_log_retention_days"
@@ -750,12 +753,12 @@ onMounted(() => {
max="365"
/>
<p class="text-xs text-muted-foreground">
Webhook发送日志保留天数
{{ t('admin.systemSettings.cleanup.webhookLogRetentionHint') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="device_session_retention_days">设备会话保留天数</UiLabel>
<UiLabel for="device_session_retention_days">{{ t('admin.systemSettings.cleanup.deviceSessionRetention') }}</UiLabel>
<UiInput
id="device_session_retention_days"
v-model.number="cleanupForm.device_session_retention_days"
@@ -764,7 +767,7 @@ onMounted(() => {
max="90"
/>
<p class="text-xs text-muted-foreground">
过期的设备会话记录保留天数
{{ t('admin.systemSettings.cleanup.deviceSessionRetentionHint') }}
</p>
</div>
</div>
@@ -772,12 +775,12 @@ onMounted(() => {
<div class="flex items-center gap-4 border-t pt-4">
<UiButton :disabled="cleanupLoading" @click="saveCleanupSettings">
<Loader2 v-if="cleanupLoading" class="mr-2 h-4 w-4 animate-spin" />
保存清理设置
{{ t('admin.systemSettings.cleanup.saveCleanupSettings') }}
</UiButton>
<UiButton variant="outline" :disabled="manualCleanupLoading" @click="runManualCleanup">
<Loader2 v-if="manualCleanupLoading" class="mr-2 h-4 w-4 animate-spin" />
<Trash2 v-else class="mr-2 h-4 w-4" />
立即执行清理
{{ t('admin.systemSettings.cleanup.runManualCleanup') }}
</UiButton>
</div>
</UiCardContent>
@@ -786,7 +789,7 @@ onMounted(() => {
<div class="flex justify-end">
<UiButton :disabled="saving" @click="saveSettings">
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
保存设置
{{ t('admin.systemSettings.saveSettings') }}
</UiButton>
</div>
</div>
@@ -60,7 +60,7 @@ export function getColumns(actions: {
header: () => t('admin.users.columns.status'),
cell: ({ row }) => {
const status = (row.getValue('online_status') as string) || 'offline'
return h(Badge, { variant: getOnlineStatusVariant(status as any) }, () => getOnlineStatusLabel(status as any))
return h(Badge, { variant: getOnlineStatusVariant(status as any) }, () => getOnlineStatusLabel(t, status as any))
},
},
{
+42 -36
View File
@@ -1,3 +1,5 @@
import type { Composer } from 'vue-i18n'
import { Ban, CheckCircle2, Clock } from 'lucide-vue-next'
import { h } from 'vue'
@@ -5,45 +7,49 @@ import type { FacetedFilterOption } from '@/components/data-table/types'
import type { OnlineStatus } from './schema'
export const userStatuses: FacetedFilterOption[] = [
{
label: '正常',
value: 'active',
icon: h(CheckCircle2),
},
{
label: '封禁',
value: 'banned',
icon: h(Ban),
},
]
export function getUserStatuses(t: Composer['t']): FacetedFilterOption[] {
return [
{
label: t('common.active'),
value: 'active',
icon: h(CheckCircle2),
},
{
label: t('common.banned'),
value: 'banned',
icon: h(Ban),
},
]
}
export const onlineStatuses: (FacetedFilterOption & { style: string })[] = [
{
label: '在线',
value: 'online',
icon: h(CheckCircle2),
style: 'bg-green-500/10 text-green-500',
},
{
label: '离线',
value: 'offline',
icon: h(Clock),
style: 'bg-gray-500/10 text-gray-500',
},
{
label: '封禁',
value: 'banned',
icon: h(Ban),
style: 'bg-red-500/10 text-red-500',
},
]
export function getOnlineStatuses(t: Composer['t']): (FacetedFilterOption & { style: string })[] {
return [
{
label: t('common.online'),
value: 'online',
icon: h(CheckCircle2),
style: 'bg-green-500/10 text-green-500',
},
{
label: t('common.offline'),
value: 'offline',
icon: h(Clock),
style: 'bg-gray-500/10 text-gray-500',
},
{
label: t('common.banned'),
value: 'banned',
icon: h(Ban),
style: 'bg-red-500/10 text-red-500',
},
]
}
export function getOnlineStatusLabel(status: OnlineStatus): string {
export function getOnlineStatusLabel(t: Composer['t'], status: OnlineStatus): string {
const labels: Record<OnlineStatus, string> = {
online: '在线',
offline: '离线',
banned: '封禁',
online: t('common.online'),
offline: t('common.offline'),
banned: t('common.banned'),
}
return labels[status] ?? status
}
+59 -56
View File
@@ -1,12 +1,15 @@
<script setup lang="ts">
import { Check, CheckCircle, Eye, FileArchive, FilePlus, GitBranch, Loader2, Upload, UploadCloud, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import { BasicPage } from '@/components/global-layout'
import api from '@/services/api'
const { t } = useI18n()
interface Application {
id: number
name: string
@@ -94,7 +97,7 @@ async function fetchVersion() {
}
catch (error) {
console.error('获取版本信息失败:', error)
toast.error('获取版本信息失败')
toast.error(t('admin.versions.createForm.fetchFailed'))
}
finally {
loading.value = false
@@ -116,7 +119,7 @@ function handleFileSelect(event: Event) {
if (target.files && target.files.length > 0) {
const file = target.files[0]
if (!file.name.toLowerCase().endsWith('.zip')) {
toast.error('只支持ZIP格式文件')
toast.error(t('admin.versions.createForm.zipOnly'))
return
}
formData.value.file = file
@@ -134,7 +137,7 @@ function removeFile() {
async function uploadZipFile() {
if (!formData.value.file) {
toast.error('请先选择ZIP文件')
toast.error(t('admin.versions.createForm.selectZipFirst'))
return
}
@@ -145,11 +148,11 @@ async function uploadZipFile() {
const response = await api.postFormData<UploadResponse>('/dev/versions/upload-zip', formDataObj)
uploadedData.value = response
toast.success('文件上传成功')
toast.success(t('admin.versions.createForm.uploadSuccess'))
}
catch (error: any) {
console.error('上传文件失败:', error)
toast.error(error.message || '上传失败')
toast.error(error.message || t('admin.versions.createForm.uploadFailed'))
}
finally {
uploading.value = false
@@ -158,11 +161,11 @@ async function uploadZipFile() {
async function handleSubmit() {
if (!formData.value.application_id) {
toast.error('请选择应用')
toast.error(t('admin.versions.createForm.selectAppRequired'))
return
}
if (!formData.value.version) {
toast.error('请输入版本号')
toast.error(t('admin.versions.createForm.versionRequired'))
return
}
@@ -186,12 +189,12 @@ async function handleSubmit() {
}
await api.put(`/dev/versions/${versionId.value}`, payload)
toast.success('版本更新成功')
toast.success(t('admin.versions.createForm.updateSuccess'))
router.push('/admin/versions')
}
catch (error: any) {
console.error('更新版本失败:', error)
toast.error(error.message || '更新失败')
toast.error(error.message || t('admin.versions.createForm.updateFailed'))
}
finally {
saving.value = false
@@ -206,11 +209,11 @@ onMounted(() => {
<template>
<BasicPage
title="编辑版本"
description="修改版本信息和更新策略"
:title="t('admin.versions.createForm.editTitle')"
:description="t('admin.versions.createForm.editDescription')"
:breadcrumbs="[
{ title: '版本管理', href: '/admin/versions' },
{ title: '编辑版本' },
{ title: t('admin.versions.title'), href: '/admin/versions' },
{ title: t('admin.versions.createForm.editTitle') },
]"
sticky
>
@@ -225,16 +228,16 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<GitBranch class="size-5" />
版本配置
{{ t('admin.versions.createForm.versionConfig') }}
</UiCardTitle>
<UiCardDescription>修改版本的基本信息和上传更新文件</UiCardDescription>
<UiCardDescription>{{ t('admin.versions.createForm.versionConfigDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>选择应用</UiLabel>
<UiLabel>{{ t('admin.versions.createForm.selectApp') }}</UiLabel>
<UiSelect v-model="formData.application_id" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue placeholder="选择应用" />
<UiSelectValue :placeholder="t('admin.versions.createForm.selectAppPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
@@ -250,42 +253,42 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="version">
版本号
{{ t('admin.versions.createForm.versionNumber') }}
</UiLabel>
<UiInput
id="version"
v-model="formData.version"
placeholder="例如: 1.0.0"
:placeholder="t('admin.versions.createForm.versionPlaceholder')"
:disabled="saving"
/>
</div>
<div class="space-y-2">
<UiLabel for="min_version">
最低版本
{{ t('admin.versions.createForm.minVersion') }}
</UiLabel>
<UiInput
id="min_version"
v-model="formData.min_version"
placeholder="例如: 0.9.0"
placeholder="0.9.0"
:disabled="saving"
/>
</div>
<div class="space-y-2">
<UiLabel for="description">
版本描述
{{ t('admin.versions.createForm.versionDesc') }}
</UiLabel>
<UiInput
id="description"
v-model="formData.description"
placeholder="简短描述此版本的更新内容"
:placeholder="t('admin.versions.createForm.versionDescPlaceholder')"
:disabled="saving"
/>
</div>
<div class="space-y-2">
<UiLabel>更新文件 (ZIP格式可选)</UiLabel>
<UiLabel>{{ t('admin.versions.createForm.updateFile') }}</UiLabel>
<div class="border-2 border-dashed rounded-lg p-4">
<input
ref="fileInputRef"
@@ -297,11 +300,11 @@ onMounted(() => {
<div v-if="!formData.file" class="text-center">
<UploadCloud class="size-10 mx-auto text-muted-foreground mb-2" />
<p class="text-sm text-muted-foreground mb-2">
拖拽ZIP文件到此处或点击上传
{{ t('admin.versions.createForm.dragOrClick') }}
</p>
<UiButton variant="outline" size="sm" :disabled="saving || uploading" @click="fileInputRef?.click()">
<FilePlus class="h-4 w-4 mr-2" />
选择文件
{{ t('admin.versions.createForm.selectFile') }}
</UiButton>
</div>
<div v-else class="space-y-3">
@@ -326,7 +329,7 @@ onMounted(() => {
>
<Loader2 v-if="uploading" class="h-4 w-4 mr-2 animate-spin" />
<Upload v-else class="h-4 w-4 mr-2" />
上传解析
{{ t('admin.versions.createForm.uploadAndParse') }}
</UiButton>
<UiButton variant="ghost" size="icon" :disabled="saving" @click="removeFile">
<X class="size-4" />
@@ -336,11 +339,11 @@ onMounted(() => {
<div v-if="uploadedData" class="rounded-lg bg-muted/50 p-3">
<div class="flex items-center gap-2 text-sm text-green-600 mb-2">
<CheckCircle class="size-4" />
<span>文件已上传并解析</span>
<span>{{ t('admin.versions.createForm.fileUploaded') }}</span>
</div>
<div class="text-xs text-muted-foreground space-y-1">
<p>文件哈希: {{ uploadedData.file_hash.substring(0, 16) }}...</p>
<p>包含 {{ uploadedData.files.length }} 个文件</p>
<p>{{ t('admin.versions.createForm.fileHash') }}: {{ uploadedData.file_hash.substring(0, 16) }}...</p>
<p>{{ t('admin.versions.createForm.containsFiles', { count: uploadedData.files.length }) }}</p>
</div>
</div>
</div>
@@ -348,22 +351,22 @@ onMounted(() => {
</div>
<div v-if="uploadedData && uploadedData.files.length > 0" class="space-y-2">
<UiLabel>文件列表</UiLabel>
<UiLabel>{{ t('admin.versions.createForm.fileList') }}</UiLabel>
<div class="border rounded-lg max-h-[300px] overflow-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50 sticky top-0">
<tr>
<th class="text-left p-2 font-medium">
文件名
{{ t('admin.versions.createForm.fileName') }}
</th>
<th class="text-left p-2 font-medium">
路径
{{ t('admin.versions.createForm.filePath') }}
</th>
<th class="text-right p-2 font-medium">
大小
{{ t('admin.versions.createForm.fileSize') }}
</th>
<th class="text-left p-2 font-medium">
类型
{{ t('admin.versions.createForm.fileType') }}
</th>
</tr>
</thead>
@@ -394,10 +397,10 @@ onMounted(() => {
</div>
<div v-if="executableFiles.length > 0" class="space-y-2">
<UiLabel>入口文件</UiLabel>
<UiLabel>{{ t('admin.versions.createForm.entryFile') }}</UiLabel>
<UiSelect v-model="formData.entry_file" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue placeholder="选择入口文件(可选)" />
<UiSelectValue :placeholder="t('admin.versions.createForm.entryFilePlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
@@ -414,10 +417,10 @@ onMounted(() => {
<div class="flex items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<UiLabel class="text-base">
强制更新
{{ t('admin.versions.createForm.forcedUpdate') }}
</UiLabel>
<p class="text-sm text-muted-foreground">
启用后用户必须更新到此版本才能继续使用
{{ t('admin.versions.createForm.forcedUpdateDesc') }}
</p>
</div>
<UiSwitch
@@ -430,10 +433,10 @@ onMounted(() => {
<div class="flex items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<UiLabel class="text-base">
自动更新
{{ t('admin.versions.createForm.autoUpdate') }}
</UiLabel>
<p class="text-sm text-muted-foreground">
启用后应用将自动下载并安装更新
{{ t('admin.versions.createForm.autoUpdateDesc') }}
</p>
</div>
<UiSwitch
@@ -445,13 +448,13 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="changelog">
更新日志
{{ t('admin.versions.createForm.changelog') }}
</UiLabel>
<textarea
id="changelog"
v-model="formData.changelog"
class="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="详细描述此版本的更新内容..."
:placeholder="t('admin.versions.createForm.changelogPlaceholder')"
:disabled="saving"
/>
</div>
@@ -464,44 +467,44 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Eye class="size-5" />
预览
{{ t('admin.versions.createForm.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">所属应用</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewApp') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">版本号</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewVersion') }}</span>
<span>{{ formData.version || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">最低版本</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewMinVersion') }}</span>
<span>{{ formData.min_version || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">版本描述</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewDesc') }}</span>
<span class="truncate max-w-[120px]">{{ formData.description || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">强制更新</span>
<span>{{ formData.update_strategy === 'forced' ? '是' : '否' }}</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewForcedUpdate') }}</span>
<span>{{ formData.update_strategy === 'forced' ? t('common.yes') : t('common.no') }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">自动更新</span>
<span>{{ formData.update_method === 'auto' ? '是' : '否' }}</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewAutoUpdate') }}</span>
<span>{{ formData.update_method === 'auto' ? t('common.yes') : t('common.no') }}</span>
</div>
</div>
<div class="border-t pt-4 mt-4">
<p class="text-sm text-muted-foreground mb-2">
更新日志预览
{{ t('admin.versions.createForm.changelogPreview') }}
</p>
<div class="rounded-lg bg-muted/50 p-3 min-h-[80px]">
<p class="text-sm whitespace-pre-wrap">
{{ formData.changelog || '暂无更新日志' }}
{{ formData.changelog || t('admin.versions.createForm.noChangelog') }}
</p>
</div>
</div>
@@ -519,14 +522,14 @@ onMounted(() => {
>
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
保存修改
{{ t('admin.versions.createForm.updateVersion') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
取消
{{ t('common.cancel') }}
</UiButton>
</div>
</UiCardContent>
+64 -61
View File
@@ -1,12 +1,15 @@
<script setup lang="ts">
import { CheckCircle, Eye, FileArchive, FilePlus, GitBranch, Loader2, Rocket, Upload, UploadCloud, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import { BasicPage } from '@/components/global-layout'
import api from '@/services/api'
const { t } = useI18n()
interface Application {
id: number
name: string
@@ -99,7 +102,7 @@ function handleFileSelect(event: Event) {
if (target.files && target.files.length > 0) {
const file = target.files[0]
if (!file.name.toLowerCase().endsWith('.zip')) {
toast.error('只支持ZIP格式文件')
toast.error(t('admin.versions.createForm.zipOnly'))
return
}
formData.value.file = file
@@ -117,7 +120,7 @@ function removeFile() {
async function uploadZipFile() {
if (!formData.value.file) {
toast.error('请先选择ZIP文件')
toast.error(t('admin.versions.createForm.selectZipFirst'))
return
}
@@ -128,11 +131,11 @@ async function uploadZipFile() {
const response = await api.postFormData<UploadResponse>('/dev/versions/upload-zip', formDataObj)
uploadedData.value = response
toast.success('文件上传成功')
toast.success(t('admin.versions.createForm.uploadSuccess'))
}
catch (error: any) {
console.error('上传文件失败:', error)
toast.error(error.message || '上传失败')
toast.error(error.message || t('admin.versions.createForm.uploadFailed'))
}
finally {
uploading.value = false
@@ -141,15 +144,15 @@ async function uploadZipFile() {
async function handleSubmit() {
if (!formData.value.application_id) {
toast.error('请选择应用')
toast.error(t('admin.versions.createForm.selectAppRequired'))
return
}
if (!formData.value.version) {
toast.error('请输入版本号')
toast.error(t('admin.versions.createForm.versionRequired'))
return
}
if (!uploadedData.value) {
toast.error('请先上传ZIP文件')
toast.error(t('admin.versions.createForm.uploadZipRequired'))
return
}
@@ -168,12 +171,12 @@ async function handleSubmit() {
file_size: uploadedData.value.file_size,
file_hash: uploadedData.value.file_hash,
})
toast.success('版本创建成功')
toast.success(t('admin.versions.createForm.createSuccess'))
router.push('/admin/versions')
}
catch (error: any) {
console.error('创建版本失败:', error)
toast.error(error.message || '创建失败')
toast.error(error.message || t('admin.versions.createForm.createFailed'))
}
finally {
saving.value = false
@@ -187,11 +190,11 @@ onMounted(() => {
<template>
<BasicPage
title="创建版本"
description="为应用创建新的版本,上传ZIP更新包并配置更新策略"
:title="t('admin.versions.createForm.title')"
:description="t('admin.versions.createForm.description')"
:breadcrumbs="[
{ title: '版本管理', href: '/admin/versions' },
{ title: '创建版本' },
{ title: t('admin.versions.title'), href: '/admin/versions' },
{ title: t('admin.versions.createForm.title') },
]"
sticky
>
@@ -202,16 +205,16 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<GitBranch class="size-5" />
版本配置
{{ t('admin.versions.createForm.versionConfig') }}
</UiCardTitle>
<UiCardDescription>填写版本的基本信息和上传更新文件</UiCardDescription>
<UiCardDescription>{{ t('admin.versions.createForm.versionConfigDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>选择应用 <span class="text-destructive">*</span></UiLabel>
<UiLabel>{{ t('admin.versions.createForm.selectApp') }} <span class="text-destructive">*</span></UiLabel>
<UiSelect v-model="formData.application_id" :disabled="saving || loading">
<UiSelectTrigger>
<UiSelectValue placeholder="选择应用" />
<UiSelectValue :placeholder="t('admin.versions.createForm.selectAppPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
@@ -227,42 +230,42 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="version">
版本号 <span class="text-destructive">*</span>
{{ t('admin.versions.createForm.versionNumber') }} <span class="text-destructive">*</span>
</UiLabel>
<UiInput
id="version"
v-model="formData.version"
placeholder="例如: 1.0.0"
:placeholder="t('admin.versions.createForm.versionPlaceholder')"
:disabled="saving"
/>
</div>
<div class="space-y-2">
<UiLabel for="min_version">
最低版本
{{ t('admin.versions.createForm.minVersion') }}
</UiLabel>
<UiInput
id="min_version"
v-model="formData.min_version"
placeholder="例如: 0.9.0 (低于此版本需要完整安装)"
:placeholder="t('admin.versions.createForm.minVersionPlaceholder')"
:disabled="saving"
/>
</div>
<div class="space-y-2">
<UiLabel for="description">
版本描述
{{ t('admin.versions.createForm.versionDesc') }}
</UiLabel>
<UiInput
id="description"
v-model="formData.description"
placeholder="简短描述此版本的更新内容"
:placeholder="t('admin.versions.createForm.versionDescPlaceholder')"
:disabled="saving"
/>
</div>
<div class="space-y-2">
<UiLabel>更新文件 (ZIP格式)</UiLabel>
<UiLabel>{{ t('admin.versions.createForm.updateFile') }}</UiLabel>
<div class="border-2 border-dashed rounded-lg p-4">
<input
ref="fileInputRef"
@@ -274,11 +277,11 @@ onMounted(() => {
<div v-if="!formData.file" class="text-center">
<UploadCloud class="size-10 mx-auto text-muted-foreground mb-2" />
<p class="text-sm text-muted-foreground mb-2">
拖拽ZIP文件到此处或点击上传
{{ t('admin.versions.createForm.dragOrClick') }}
</p>
<UiButton variant="outline" size="sm" :disabled="saving || uploading" @click="fileInputRef?.click()">
<FilePlus class="h-4 w-4 mr-2" />
选择文件
{{ t('admin.versions.createForm.selectFile') }}
</UiButton>
</div>
<div v-else class="space-y-3">
@@ -303,7 +306,7 @@ onMounted(() => {
>
<Loader2 v-if="uploading" class="h-4 w-4 mr-2 animate-spin" />
<Upload v-else class="h-4 w-4 mr-2" />
上传解析
{{ t('admin.versions.createForm.uploadAndParse') }}
</UiButton>
<UiButton variant="ghost" size="icon" :disabled="saving" @click="removeFile">
<X class="size-4" />
@@ -313,11 +316,11 @@ onMounted(() => {
<div v-if="uploadedData" class="rounded-lg bg-muted/50 p-3">
<div class="flex items-center gap-2 text-sm text-green-600 mb-2">
<CheckCircle class="size-4" />
<span>文件已上传并解析</span>
<span>{{ t('admin.versions.createForm.fileUploaded') }}</span>
</div>
<div class="text-xs text-muted-foreground space-y-1">
<p>文件哈希: {{ uploadedData.file_hash.substring(0, 16) }}...</p>
<p>包含 {{ uploadedData.files.length }} 个文件</p>
<p>{{ t('admin.versions.createForm.fileHash') }}: {{ uploadedData.file_hash.substring(0, 16) }}...</p>
<p>{{ t('admin.versions.createForm.containsFiles', { count: uploadedData.files.length }) }}</p>
</div>
</div>
</div>
@@ -325,22 +328,22 @@ onMounted(() => {
</div>
<div v-if="uploadedData && uploadedData.files.length > 0" class="space-y-2">
<UiLabel>文件列表</UiLabel>
<UiLabel>{{ t('admin.versions.createForm.fileList') }}</UiLabel>
<div class="border rounded-lg max-h-[300px] overflow-auto">
<table class="w-full text-sm">
<thead class="bg-muted/50 sticky top-0">
<tr>
<th class="text-left p-2 font-medium">
文件名
{{ t('admin.versions.createForm.fileName') }}
</th>
<th class="text-left p-2 font-medium">
路径
{{ t('admin.versions.createForm.filePath') }}
</th>
<th class="text-right p-2 font-medium">
大小
{{ t('admin.versions.createForm.fileSize') }}
</th>
<th class="text-left p-2 font-medium">
类型
{{ t('admin.versions.createForm.fileType') }}
</th>
</tr>
</thead>
@@ -371,13 +374,13 @@ onMounted(() => {
</div>
<div v-if="executableFiles.length > 0" class="space-y-2">
<UiLabel>入口文件</UiLabel>
<UiLabel>{{ t('admin.versions.createForm.entryFile') }}</UiLabel>
<p class="text-sm text-muted-foreground">
选择更新完成后要启动的主程序文件可选
{{ t('admin.versions.createForm.entryFileDesc') }}
</p>
<UiSelect v-model="formData.entry_file" :disabled="saving">
<UiSelectTrigger>
<UiSelectValue placeholder="选择入口文件(可选)" />
<UiSelectValue :placeholder="t('admin.versions.createForm.entryFilePlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
@@ -394,10 +397,10 @@ onMounted(() => {
<div class="flex items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<UiLabel class="text-base">
强制更新
{{ t('admin.versions.createForm.forcedUpdate') }}
</UiLabel>
<p class="text-sm text-muted-foreground">
启用后用户必须更新到此版本才能继续使用
{{ t('admin.versions.createForm.forcedUpdateDesc') }}
</p>
</div>
<UiSwitch
@@ -410,10 +413,10 @@ onMounted(() => {
<div class="flex items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<UiLabel class="text-base">
自动更新
{{ t('admin.versions.createForm.autoUpdate') }}
</UiLabel>
<p class="text-sm text-muted-foreground">
启用后应用将自动下载并安装更新
{{ t('admin.versions.createForm.autoUpdateDesc') }}
</p>
</div>
<UiSwitch
@@ -425,13 +428,13 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="changelog">
更新日志
{{ t('admin.versions.createForm.changelog') }}
</UiLabel>
<textarea
id="changelog"
v-model="formData.changelog"
class="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="详细描述此版本的更新内容、修复的问题、新增的功能等..."
:placeholder="t('admin.versions.createForm.changelogPlaceholder')"
:disabled="saving"
/>
</div>
@@ -444,56 +447,56 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Eye class="size-5" />
预览
{{ t('admin.versions.createForm.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">所属应用</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewApp') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">版本号</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewVersion') }}</span>
<span>{{ formData.version || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">最低版本</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewMinVersion') }}</span>
<span>{{ formData.min_version || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">版本描述</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewDesc') }}</span>
<span class="truncate max-w-[120px]">{{ formData.description || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">更新文件</span>
<span>{{ formData.file ? formData.file.name : '未上传' }}</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewFile') }}</span>
<span>{{ formData.file ? formData.file.name : t('admin.versions.createForm.notUploaded') }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">文件数量</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewFileCount') }}</span>
<span>{{ uploadedData ? uploadedData.files.length : '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">入口文件</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewEntryFile') }}</span>
<span class="truncate max-w-[120px]">{{ formData.entry_file || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">强制更新</span>
<span>{{ formData.update_strategy === 'forced' ? '是' : '否' }}</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewForcedUpdate') }}</span>
<span>{{ formData.update_strategy === 'forced' ? t('common.yes') : t('common.no') }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">自动更新</span>
<span>{{ formData.update_method === 'auto' ? '是' : '否' }}</span>
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewAutoUpdate') }}</span>
<span>{{ formData.update_method === 'auto' ? t('common.yes') : t('common.no') }}</span>
</div>
</div>
<div class="border-t pt-4 mt-4">
<p class="text-sm text-muted-foreground mb-2">
更新日志预览
{{ t('admin.versions.createForm.changelogPreview') }}
</p>
<div class="rounded-lg bg-muted/50 p-3 min-h-[80px]">
<p class="text-sm whitespace-pre-wrap">
{{ formData.changelog || '暂无更新日志' }}
{{ formData.changelog || t('admin.versions.createForm.noChangelog') }}
</p>
</div>
</div>
@@ -511,14 +514,14 @@ onMounted(() => {
>
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Rocket v-else class="mr-2 h-4 w-4" />
发布版本
{{ t('admin.versions.createForm.publishVersion') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
取消
{{ t('common.cancel') }}
</UiButton>
</div>
</UiCardContent>
+12 -10
View File
@@ -1,10 +1,12 @@
<script setup lang="ts">
import { AlertCircle, Box, Loader2 } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import api from '@/services/api'
const { t } = useI18n()
const route = useRoute()
const loading = ref(true)
@@ -19,7 +21,7 @@ onMounted(async () => {
cardTypes.value = data?.cardTypes || []
}
catch (error) {
console.error('获取应用详情失败:', error)
console.error('Load app detail failed:', error)
}
finally {
loading.value = false
@@ -43,21 +45,21 @@ onMounted(async () => {
{{ app.name }}
</h1>
<p class="text-muted-foreground">
{{ app.description || '暂无描述' }}
{{ app.description || t('agent.apps.detail.noDescription') }}
</p>
</div>
</div>
<UiCard>
<UiCardHeader>
<UiCardTitle>可用卡类</UiCardTitle>
<UiCardTitle>{{ t('agent.apps.detail.availableCardTypes') }}</UiCardTitle>
<UiCardDescription>
您可以为此应用生成以下类型的卡密
{{ t('agent.apps.detail.cardTypeDesc') }}
</UiCardDescription>
</UiCardHeader>
<UiCardContent>
<div v-if="cardTypes.length === 0" class="text-center py-8 text-muted-foreground">
暂无可用卡类
{{ t('agent.apps.detail.noCardTypes') }}
</div>
<div v-else class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<UiCard v-for="ct in cardTypes" :key="ct.id" class="bg-muted/50">
@@ -68,11 +70,11 @@ onMounted(async () => {
</UiCardHeader>
<UiCardContent>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">时长</span>
<span>{{ ct.duration_days }} </span>
<span class="text-muted-foreground">{{ t('agent.apps.detail.duration') }}</span>
<span>{{ ct.duration_days }} {{ t('agent.apps.detail.days') }}</span>
</div>
<div class="flex items-center justify-between text-sm mt-2">
<span class="text-muted-foreground">价格</span>
<span class="text-muted-foreground">{{ t('agent.apps.detail.price') }}</span>
<span class="font-medium">¥{{ ct.price }}</span>
</div>
<UiButton
@@ -81,7 +83,7 @@ onMounted(async () => {
as-child
>
<router-link :to="`/agent/cards/create?app_id=${app.id}&card_type_id=${ct.id}`">
生成卡密
{{ t('agent.apps.detail.generateCards') }}
</router-link>
</UiButton>
</UiCardContent>
@@ -93,7 +95,7 @@ onMounted(async () => {
<div v-else class="text-center py-12 text-muted-foreground">
<AlertCircle class="size-16 mx-auto mb-4 opacity-30" />
<p>应用不存在或无权访问</p>
<p>{{ t('agent.apps.detail.notFound') }}</p>
</div>
</div>
</template>
+11 -8
View File
@@ -1,9 +1,12 @@
<script setup lang="ts">
import { Box, Boxes, Loader2 } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import api from '@/services/api'
const { t } = useI18n()
interface App {
id: number
name: string
@@ -22,7 +25,7 @@ onMounted(async () => {
apps.value = data || []
}
catch (error) {
console.error('获取应用列表失败:', error)
console.error('Load apps failed:', error)
}
finally {
loading.value = false
@@ -30,7 +33,7 @@ onMounted(async () => {
})
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('zh-CN')
return new Date(dateStr).toLocaleDateString()
}
</script>
@@ -39,10 +42,10 @@ function formatDate(dateStr: string) {
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">
应用管理
{{ t('agent.apps.title') }}
</h1>
<p class="text-muted-foreground">
管理您授权的应用
{{ t('agent.apps.description') }}
</p>
</div>
</div>
@@ -55,7 +58,7 @@ function formatDate(dateStr: string) {
<div v-else-if="apps.length === 0" class="text-center py-12 text-muted-foreground">
<Boxes class="size-16 mx-auto mb-4 opacity-30" />
<p>暂无授权应用</p>
<p>{{ t('agent.apps.noApps') }}</p>
</div>
<div v-else class="divide-y">
@@ -73,14 +76,14 @@ function formatDate(dateStr: string) {
{{ app.name }}
</h3>
<p class="text-sm text-muted-foreground">
{{ app.description || '暂无描述' }}
{{ app.description || t('agent.apps.noDescription') }}
</p>
</div>
</div>
<div class="flex items-center gap-4">
<div class="text-right">
<p class="text-sm font-medium">
{{ app.card_types_count }} 种卡类
{{ app.card_types_count }} {{ t('agent.apps.cardTypesCount') }}
</p>
<p class="text-xs text-muted-foreground">
{{ formatDate(app.created_at) }}
@@ -88,7 +91,7 @@ function formatDate(dateStr: string) {
</div>
<UiButton variant="outline" size="sm" as-child>
<router-link :to="`/agent/apps/${app.id}`">
查看
{{ t('agent.apps.view') }}
</router-link>
</UiButton>
</div>
+25 -23
View File
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { Loader2 } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import api from '@/services/api'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
@@ -43,21 +45,21 @@ onMounted(async () => {
}
}
catch (error) {
console.error('获取数据失败:', error)
console.error('Load data failed:', error)
}
})
async function handleGenerate() {
if (!form.value.app_id) {
toast.error('请选择应用')
toast.error(t('agent.cards.create.selectApp'))
return
}
if (!form.value.card_type_id) {
toast.error('请选择卡类')
toast.error(t('agent.cards.create.selectCardType'))
return
}
if (form.value.quantity < 1 || form.value.quantity > 100) {
toast.error('生成数量需要在 1-100 之间')
toast.error(t('agent.cards.create.quantityRange'))
return
}
@@ -69,17 +71,17 @@ async function handleGenerate() {
quantity: form.value.quantity,
})
toast.success(`成功生成 ${data.codes.length} 张卡密`)
toast.success(t('agent.cards.create.generateSuccess', { count: data.codes.length }))
const codesText = data.codes.join('\n')
await navigator.clipboard.writeText(codesText)
toast.success('卡密已复制到剪贴板')
toast.success(t('agent.cards.create.copiedToClipboard'))
router.push('/agent/cards')
}
catch (error: any) {
console.error('生成卡密失败:', error)
toast.error(error.message || '生成失败')
console.error('Generate cards failed:', error)
toast.error(error.message || t('agent.cards.create.generateFailed'))
}
finally {
loading.value = false
@@ -91,26 +93,26 @@ async function handleGenerate() {
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold">
生成卡密
{{ t('agent.cards.create.title') }}
</h1>
<p class="text-muted-foreground">
为授权应用生成卡密
{{ t('agent.cards.create.description') }}
</p>
</div>
<UiCard class="max-w-xl">
<UiCardHeader>
<UiCardTitle>生成设置</UiCardTitle>
<UiCardTitle>{{ t('agent.cards.create.generateSettings') }}</UiCardTitle>
<UiCardDescription>
选择应用和卡类设置生成数量
{{ t('agent.cards.create.settingsDesc') }}
</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>选择应用</UiLabel>
<UiLabel>{{ t('agent.cards.create.selectAppLabel') }}</UiLabel>
<UiSelect v-model="form.app_id">
<UiSelectTrigger>
<UiSelectValue placeholder="请选择应用" />
<UiSelectValue :placeholder="t('agent.cards.create.selectAppPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in apps" :key="app.id" :value="String(app.id)">
@@ -121,36 +123,36 @@ async function handleGenerate() {
</div>
<div v-if="form.app_id" class="space-y-2">
<UiLabel>选择卡类</UiLabel>
<UiLabel>{{ t('agent.cards.create.selectCardTypeLabel') }}</UiLabel>
<UiSelect v-model="form.card_type_id">
<UiSelectTrigger>
<UiSelectValue placeholder="请选择卡类" />
<UiSelectValue :placeholder="t('agent.cards.create.selectCardTypePlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="ct in availableCardTypes" :key="ct.id" :value="String(ct.id)">
{{ ct.name }} - {{ ct.duration_days }} - ¥{{ ct.price }}
{{ ct.name }} - {{ ct.duration_days }}{{ t('agent.apps.detail.days') }} - ¥{{ ct.price }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div class="space-y-2">
<UiLabel>生成数量</UiLabel>
<UiLabel>{{ t('agent.cards.create.quantity') }}</UiLabel>
<UiInput
v-model.number="form.quantity"
type="number"
:min="1"
:max="100"
placeholder="请输入生成数量"
:placeholder="t('agent.cards.create.quantityPlaceholder')"
/>
<p class="text-xs text-muted-foreground">
单次最多生成 100 张卡密
{{ t('agent.cards.create.quantityHint') }}
</p>
</div>
<div v-if="totalPrice > 0" class="p-4 rounded-lg bg-muted/50">
<div class="flex items-center justify-between">
<span class="text-muted-foreground">预计费用</span>
<span class="text-muted-foreground">{{ t('agent.cards.create.estimatedCost') }}</span>
<span class="text-xl font-bold">¥{{ totalPrice.toFixed(2) }}</span>
</div>
</div>
@@ -158,7 +160,7 @@ async function handleGenerate() {
<div class="flex gap-3">
<UiButton variant="outline" as-child>
<router-link to="/agent/cards">
取消
{{ t('agent.cards.create.cancel') }}
</router-link>
</UiButton>
<UiButton
@@ -166,7 +168,7 @@ async function handleGenerate() {
@click="handleGenerate"
>
<Loader2 v-if="loading" class="mr-2 h-4 w-4 animate-spin" />
{{ loading ? '生成中...' : '生成卡密' }}
{{ loading ? t('agent.cards.create.generating') : t('agent.cards.create.generateBtn') }}
</UiButton>
</div>
</UiCardContent>
+20 -17
View File
@@ -1,9 +1,12 @@
<script setup lang="ts">
import { Key, Loader2, Plus } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import api from '@/services/api'
const { t } = useI18n()
interface Card {
id: number
code: string
@@ -23,7 +26,7 @@ onMounted(async () => {
cards.value = data || []
}
catch (error) {
console.error('获取卡密列表失败:', error)
console.error('Load cards failed:', error)
}
finally {
loading.value = false
@@ -33,15 +36,15 @@ onMounted(async () => {
function formatDate(dateStr: string | null) {
if (!dateStr)
return '-'
return new Date(dateStr).toLocaleDateString('zh-CN')
return new Date(dateStr).toLocaleDateString()
}
function getStatusBadge(status: string) {
const map: Record<string, { label: string, class: string }> = {
unused: { label: '未使用', class: 'bg-green-500/10 text-green-500' },
used: { label: '已使用', class: 'bg-blue-500/10 text-blue-500' },
expired: { label: '已过期', class: 'bg-red-500/10 text-red-500' },
disabled: { label: '已禁用', class: 'bg-gray-500/10 text-gray-500' },
unused: { label: t('agent.cards.unused'), class: 'bg-green-500/10 text-green-500' },
used: { label: t('agent.cards.used'), class: 'bg-blue-500/10 text-blue-500' },
expired: { label: t('agent.cards.expired'), class: 'bg-red-500/10 text-red-500' },
disabled: { label: t('agent.cards.disabled'), class: 'bg-gray-500/10 text-gray-500' },
}
return map[status] || { label: status, class: '' }
}
@@ -52,16 +55,16 @@ function getStatusBadge(status: string) {
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold">
卡密管理
{{ t('agent.cards.title') }}
</h1>
<p class="text-muted-foreground">
管理您生成的卡密
{{ t('agent.cards.description') }}
</p>
</div>
<UiButton as-child>
<router-link to="/agent/cards/create">
<Plus class="mr-2 h-4 w-4" />
生成卡密
{{ t('agent.cards.generateCards') }}
</router-link>
</UiButton>
</div>
@@ -74,10 +77,10 @@ function getStatusBadge(status: string) {
<div v-else-if="cards.length === 0" class="text-center py-12 text-muted-foreground">
<Key class="size-16 mx-auto mb-4 opacity-30" />
<p>暂无卡密记录</p>
<p>{{ t('agent.cards.noCards') }}</p>
<UiButton class="mt-4" as-child>
<router-link to="/agent/cards/create">
生成卡密
{{ t('agent.cards.generateCards') }}
</router-link>
</UiButton>
</div>
@@ -87,22 +90,22 @@ function getStatusBadge(status: string) {
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-3 text-left text-sm font-medium">
卡密
{{ t('agent.cards.cardCode') }}
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
应用
{{ t('agent.cards.app') }}
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
卡类
{{ t('agent.cards.cardType') }}
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
状态
{{ t('agent.cards.status') }}
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
创建时间
{{ t('agent.cards.createTime') }}
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
使用时间
{{ t('agent.cards.useTime') }}
</th>
</tr>
</thead>
+14 -11
View File
@@ -1,9 +1,12 @@
<script setup lang="ts">
import { ArrowDownLeft, ArrowUpRight, Loader2, Receipt, RotateCcw } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import api from '@/services/api'
const { t } = useI18n()
interface Transaction {
id: number
type: string
@@ -24,7 +27,7 @@ onMounted(async () => {
transactions.value = data?.transactions || []
}
catch (error) {
console.error('获取财务数据失败:', error)
console.error('Load finance data failed:', error)
}
finally {
loading.value = false
@@ -32,14 +35,14 @@ onMounted(async () => {
})
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleString('zh-CN')
return new Date(dateStr).toLocaleString()
}
function getTypeLabel(type: string) {
const map: Record<string, string> = {
recharge: '充值',
consume: '消费',
refund: '退款',
recharge: t('agent.finance.recharge'),
consume: t('agent.finance.consume'),
refund: t('agent.finance.refund'),
}
return map[type] || type
}
@@ -58,10 +61,10 @@ function getTypeClass(type: string) {
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold">
财务管理
{{ t('agent.finance.title') }}
</h1>
<p class="text-muted-foreground">
查看您的账户余额和交易记录
{{ t('agent.finance.description') }}
</p>
</div>
@@ -69,7 +72,7 @@ function getTypeClass(type: string) {
<UiCard>
<UiCardHeader class="pb-2">
<UiCardTitle class="text-sm font-medium">
账户余额
{{ t('agent.finance.accountBalance') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent>
@@ -82,7 +85,7 @@ function getTypeClass(type: string) {
<UiCard>
<UiCardHeader>
<UiCardTitle>交易记录</UiCardTitle>
<UiCardTitle>{{ t('agent.finance.transactionRecords') }}</UiCardTitle>
</UiCardHeader>
<UiCardContent class="p-0">
<div v-if="loading" class="flex items-center justify-center py-12">
@@ -91,7 +94,7 @@ function getTypeClass(type: string) {
<div v-else-if="transactions.length === 0" class="text-center py-12 text-muted-foreground">
<Receipt class="size-16 mx-auto mb-4 opacity-30" />
<p>暂无交易记录</p>
<p>{{ t('agent.finance.noTransactions') }}</p>
</div>
<div v-else class="divide-y">
@@ -125,7 +128,7 @@ function getTypeClass(type: string) {
{{ tx.type === 'recharge' || tx.type === 'refund' ? '+' : '-' }}¥{{ tx.amount.toFixed(2) }}
</p>
<p class="text-xs text-muted-foreground">
余额: ¥{{ tx.balance.toFixed(2) }}
{{ t('agent.finance.balance') }}: ¥{{ tx.balance.toFixed(2) }}
</p>
</div>
</div>
+22 -19
View File
@@ -1,9 +1,12 @@
<script setup lang="ts">
import { Boxes, CreditCard, DollarSign, Key, Plus, Users } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import api from '@/services/api'
const { t } = useI18n()
const loading = ref(true)
const stats = ref({
totalApps: 0,
@@ -38,7 +41,7 @@ onMounted(async () => {
}
}
catch (error) {
console.error('获取统计数据失败:', error)
console.error('Load stats failed:', error)
}
finally {
loading.value = false
@@ -50,10 +53,10 @@ onMounted(async () => {
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold">
控制台
{{ t('agent.dashboard.title') }}
</h1>
<p class="text-muted-foreground">
欢迎回来查看您的业务概况
{{ t('agent.dashboard.welcomeBack') }}
</p>
</div>
@@ -61,7 +64,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2">
<UiCardTitle class="text-sm font-medium">
授权应用
{{ t('agent.dashboard.authorizedApps') }}
</UiCardTitle>
<Boxes class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -70,7 +73,7 @@ onMounted(async () => {
{{ stats.totalApps }}
</div>
<p class="text-xs text-muted-foreground">
已授权的应用数量
{{ t('agent.dashboard.authorizedAppsCount') }}
</p>
</UiCardContent>
</UiCard>
@@ -78,7 +81,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2">
<UiCardTitle class="text-sm font-medium">
卡密总数
{{ t('agent.dashboard.totalCards') }}
</UiCardTitle>
<Key class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -87,7 +90,7 @@ onMounted(async () => {
{{ stats.totalCards.toLocaleString() }}
</div>
<p class="text-xs text-muted-foreground">
今日生成: {{ stats.todayCards }}
{{ t('agent.dashboard.todayGenerated') }}: {{ stats.todayCards }}
</p>
</UiCardContent>
</UiCard>
@@ -95,7 +98,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2">
<UiCardTitle class="text-sm font-medium">
用户总数
{{ t('agent.dashboard.totalUsers') }}
</UiCardTitle>
<Users class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -104,7 +107,7 @@ onMounted(async () => {
{{ stats.totalUsers.toLocaleString() }}
</div>
<p class="text-xs text-muted-foreground">
累计注册用户
{{ t('agent.dashboard.totalUsersDesc') }}
</p>
</UiCardContent>
</UiCard>
@@ -112,7 +115,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader class="flex flex-row items-center justify-between pb-2">
<UiCardTitle class="text-sm font-medium">
累计收入
{{ t('agent.dashboard.totalRevenue') }}
</UiCardTitle>
<DollarSign class="size-4 text-muted-foreground" />
</UiCardHeader>
@@ -121,7 +124,7 @@ onMounted(async () => {
¥{{ stats.totalRevenue.toFixed(2) }}
</div>
<p class="text-xs text-muted-foreground">
今日: ¥{{ stats.todayRevenue.toFixed(2) }}
{{ t('agent.dashboard.todayRevenue') }}: ¥{{ stats.todayRevenue.toFixed(2) }}
</p>
</UiCardContent>
</UiCard>
@@ -130,31 +133,31 @@ onMounted(async () => {
<div class="grid gap-6 lg:grid-cols-2">
<UiCard>
<UiCardHeader>
<UiCardTitle>快捷操作</UiCardTitle>
<UiCardTitle>{{ t('agent.dashboard.quickActions') }}</UiCardTitle>
</UiCardHeader>
<UiCardContent class="grid gap-4 sm:grid-cols-2">
<router-link to="/agent/cards/create">
<UiButton variant="outline" class="w-full h-20 flex-col gap-2">
<Plus class="size-5" />
<span>生成卡密</span>
<span>{{ t('agent.dashboard.generateCards') }}</span>
</UiButton>
</router-link>
<router-link to="/agent/apps">
<UiButton variant="outline" class="w-full h-20 flex-col gap-2">
<Boxes class="size-5" />
<span>应用管理</span>
<span>{{ t('agent.dashboard.appManagement') }}</span>
</UiButton>
</router-link>
<router-link to="/agent/users">
<UiButton variant="outline" class="w-full h-20 flex-col gap-2">
<Users class="size-5" />
<span>用户管理</span>
<span>{{ t('agent.dashboard.userManagement') }}</span>
</UiButton>
</router-link>
<router-link to="/agent/finance">
<UiButton variant="outline" class="w-full h-20 flex-col gap-2">
<CreditCard class="size-5" />
<span>财务管理</span>
<span>{{ t('agent.dashboard.financeManagement') }}</span>
</UiButton>
</router-link>
</UiCardContent>
@@ -162,7 +165,7 @@ onMounted(async () => {
<UiCard>
<UiCardHeader>
<UiCardTitle>最近生成卡密</UiCardTitle>
<UiCardTitle>{{ t('agent.dashboard.recentCards') }}</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<div v-if="recentCards.length > 0" class="space-y-4">
@@ -176,13 +179,13 @@ onMounted(async () => {
</p>
</div>
<UiBadge :variant="card.status === 'unused' ? 'default' : 'secondary'">
{{ card.status === 'unused' ? '未使用' : '已使用' }}
{{ card.status === 'unused' ? t('agent.dashboard.unused') : t('agent.dashboard.used') }}
</UiBadge>
</div>
</div>
<div v-else class="text-center py-8 text-muted-foreground">
<Key class="size-12 mx-auto mb-2 opacity-30" />
<p>暂无卡密记录</p>
<p>{{ t('agent.dashboard.noCards') }}</p>
</div>
</UiCardContent>
</UiCard>
+14 -11
View File
@@ -1,9 +1,12 @@
<script setup lang="ts">
import { Loader2, Users } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import api from '@/services/api'
const { t } = useI18n()
interface User {
id: number
username: string
@@ -22,7 +25,7 @@ onMounted(async () => {
users.value = data || []
}
catch (error) {
console.error('获取用户列表失败:', error)
console.error('Load users failed:', error)
}
finally {
loading.value = false
@@ -32,7 +35,7 @@ onMounted(async () => {
function formatDate(dateStr: string | null) {
if (!dateStr)
return '-'
return new Date(dateStr).toLocaleDateString('zh-CN')
return new Date(dateStr).toLocaleDateString()
}
</script>
@@ -40,10 +43,10 @@ function formatDate(dateStr: string | null) {
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold">
用户管理
{{ t('agent.users.title') }}
</h1>
<p class="text-muted-foreground">
管理您应用下的用户
{{ t('agent.users.description') }}
</p>
</div>
@@ -55,7 +58,7 @@ function formatDate(dateStr: string | null) {
<div v-else-if="users.length === 0" class="text-center py-12 text-muted-foreground">
<Users class="size-16 mx-auto mb-4 opacity-30" />
<p>暂无用户记录</p>
<p>{{ t('agent.users.noUsers') }}</p>
</div>
<div v-else class="overflow-x-auto">
@@ -63,19 +66,19 @@ function formatDate(dateStr: string | null) {
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-3 text-left text-sm font-medium">
用户名
{{ t('agent.users.username') }}
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
邮箱
{{ t('agent.users.email') }}
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
状态
{{ t('agent.users.status') }}
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
注册时间
{{ t('agent.users.registerTime') }}
</th>
<th class="px-4 py-3 text-left text-sm font-medium">
最后登录
{{ t('agent.users.lastLogin') }}
</th>
</tr>
</thead>
@@ -92,7 +95,7 @@ function formatDate(dateStr: string | null) {
:class="user.status === 'active' ? 'bg-green-500/10 text-green-500' : ''"
variant="outline"
>
{{ user.status === 'active' ? '正常' : user.status }}
{{ user.status === 'active' ? t('agent.users.active') : user.status }}
</UiBadge>
</td>
<td class="px-4 py-3 text-sm text-muted-foreground">
+25 -23
View File
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { ChartLine, Loader2, Shield, ShieldCheck, Users, Zap } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import api from '@/services/api'
const { t } = useI18n()
const router = useRouter()
const loading = ref(false)
@@ -21,7 +23,7 @@ const isValid = computed(() => {
async function handleLogin() {
if (!isValid.value) {
toast.error('请填写用户名和密码')
toast.error(t('auth.login.fillRequired'))
return
}
@@ -36,7 +38,7 @@ async function handleLogin() {
localStorage.setItem('token', data.token)
localStorage.setItem('user', JSON.stringify(data.user))
toast.success('登录成功')
toast.success(t('auth.login.loginSuccess'))
if (data.user.role === 'agent' || data.user.parent_agent_id) {
router.push('/agent')
@@ -47,8 +49,8 @@ async function handleLogin() {
}
}
catch (error: any) {
console.error('登录失败:', error)
toast.error(error.message || '登录失败,请检查用户名和密码')
console.error('Login failed:', error)
toast.error(error.message || t('auth.login.loginFailed'))
}
finally {
loading.value = false
@@ -68,30 +70,30 @@ async function handleLogin() {
<div class="size-12 rounded-xl bg-primary flex items-center justify-center">
<ShieldCheck class="size-7 text-primary-foreground" />
</div>
<span class="text-2xl font-bold">管理平台</span>
<span class="text-2xl font-bold">{{ t('auth.login.platformName') }}</span>
</div>
<h1 class="text-4xl font-bold mb-4">
软件授权管理平台
{{ t('auth.login.heroTitle') }}
</h1>
<p class="text-lg text-muted-foreground mb-8">
专业的软件验证与授权管理解决方案为您的软件提供全方位的保护
{{ t('auth.login.heroDesc') }}
</p>
<div class="grid grid-cols-2 gap-4">
<div class="flex items-center gap-3 p-4 rounded-lg bg-background/50 backdrop-blur">
<Zap class="size-5 text-primary" />
<span class="text-sm">快速集成</span>
<span class="text-sm">{{ t('auth.login.quickIntegration') }}</span>
</div>
<div class="flex items-center gap-3 p-4 rounded-lg bg-background/50 backdrop-blur">
<Shield class="size-5 text-primary" />
<span class="text-sm">安全可靠</span>
<span class="text-sm">{{ t('auth.login.secureReliable') }}</span>
</div>
<div class="flex items-center gap-3 p-4 rounded-lg bg-background/50 backdrop-blur">
<ChartLine class="size-5 text-primary" />
<span class="text-sm">数据分析</span>
<span class="text-sm">{{ t('auth.login.dataAnalysis') }}</span>
</div>
<div class="flex items-center gap-3 p-4 rounded-lg bg-background/50 backdrop-blur">
<Users class="size-5 text-primary" />
<span class="text-sm">用户管理</span>
<span class="text-sm">{{ t('auth.login.userManagement') }}</span>
</div>
</div>
</div>
@@ -104,38 +106,38 @@ async function handleLogin() {
<div class="size-10 rounded-xl bg-primary flex items-center justify-center">
<ShieldCheck class="size-6 text-primary-foreground" />
</div>
<span class="text-xl font-bold">管理平台</span>
<span class="text-xl font-bold">{{ t('auth.login.platformName') }}</span>
</div>
</div>
<div class="mb-8">
<h2 class="text-2xl font-bold mb-2">
欢迎回来
{{ t('auth.login.welcomeBack') }}
</h2>
<p class="text-muted-foreground">
请输入您的账号信息登录系统
{{ t('auth.login.loginDesc') }}
</p>
</div>
<form class="space-y-6" @submit.prevent="handleLogin">
<div class="space-y-2">
<UiLabel for="username">用户名</UiLabel>
<UiLabel for="username">{{ t('auth.login.username') }}</UiLabel>
<UiInput
id="username"
v-model="form.username"
type="text"
name="username"
autocomplete="username"
placeholder="请输入用户名"
:placeholder="t('auth.login.usernamePlaceholder')"
:disabled="loading"
/>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<UiLabel for="password">密码</UiLabel>
<UiLabel for="password">{{ t('auth.login.password') }}</UiLabel>
<router-link to="/forgot-password" class="text-sm text-primary hover:underline">
忘记密码
{{ t('auth.login.forgotPassword') }}
</router-link>
</div>
<UiInput
@@ -144,7 +146,7 @@ async function handleLogin() {
type="password"
name="password"
autocomplete="current-password"
placeholder="请输入密码"
:placeholder="t('auth.login.passwordPlaceholder')"
:disabled="loading"
/>
</div>
@@ -156,7 +158,7 @@ async function handleLogin() {
v-model:checked="form.remember"
/>
<UiLabel for="remember" class="text-sm font-normal cursor-pointer">
记住我
{{ t('auth.login.rememberMe') }}
</UiLabel>
</div>
</div>
@@ -167,14 +169,14 @@ async function handleLogin() {
:disabled="loading || !isValid"
>
<Loader2 v-if="loading" class="mr-2 h-4 w-4 animate-spin" />
{{ loading ? '登录中...' : '登录' }}
{{ loading ? t('auth.login.loggingIn') : t('auth.login.loginBtn') }}
</UiButton>
</form>
<div class="mt-6 text-center text-sm text-muted-foreground">
还没有账号
{{ t('auth.login.noAccount') }}
<router-link to="/register" class="text-primary hover:underline">
立即注册
{{ t('auth.login.registerNow') }}
</router-link>
</div>
</div>
+35 -33
View File
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { Check, Loader2, ShieldCheck } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import api from '@/services/api'
const { t } = useI18n()
const router = useRouter()
const loading = ref(false)
@@ -27,27 +29,27 @@ const isValid = computed(() => {
async function handleRegister() {
if (!form.value.username) {
toast.error('请输入用户名')
toast.error(t('auth.register.usernameRequired'))
return
}
if (!form.value.email) {
toast.error('请输入邮箱')
toast.error(t('auth.register.emailRequired'))
return
}
if (!form.value.password) {
toast.error('请输入密码')
toast.error(t('auth.register.passwordRequired'))
return
}
if (form.value.password.length < 6) {
toast.error('密码长度至少6位')
toast.error(t('auth.register.passwordMinLength'))
return
}
if (form.value.password !== form.value.confirmPassword) {
toast.error('两次输入的密码不一致')
toast.error(t('auth.register.passwordMismatch'))
return
}
if (!form.value.agree) {
toast.error('请阅读并同意服务条款')
toast.error(t('auth.register.agreeRequired'))
return
}
@@ -59,12 +61,12 @@ async function handleRegister() {
password: form.value.password,
})
toast.success('注册成功,请登录')
toast.success(t('auth.register.registerSuccess'))
router.push('/login')
}
catch (error: any) {
console.error('注册失败:', error)
toast.error(error.message || '注册失败')
console.error('Registration failed:', error)
toast.error(error.message || t('auth.register.registerFailed'))
}
finally {
loading.value = false
@@ -84,32 +86,32 @@ async function handleRegister() {
<div class="size-12 rounded-xl bg-primary flex items-center justify-center">
<ShieldCheck class="size-7 text-primary-foreground" />
</div>
<span class="text-2xl font-bold">管理平台</span>
<span class="text-2xl font-bold">{{ t('auth.register.platformName') }}</span>
</div>
<h1 class="text-4xl font-bold mb-4">
开始使用
{{ t('auth.register.heroTitle') }}
</h1>
<p class="text-lg text-muted-foreground mb-8">
创建账号立即体验专业的软件授权管理服务
{{ t('auth.register.heroDesc') }}
</p>
<div class="space-y-4">
<div class="flex items-center gap-3">
<div class="size-8 rounded-full bg-primary/20 flex items-center justify-center">
<Check class="size-4 text-primary" />
</div>
<span>免费创建应用快速集成 SDK</span>
<span>{{ t('auth.register.feature1') }}</span>
</div>
<div class="flex items-center gap-3">
<div class="size-8 rounded-full bg-primary/20 flex items-center justify-center">
<Check class="size-4 text-primary" />
</div>
<span>完善的用户管理和数据分析</span>
<span>{{ t('auth.register.feature2') }}</span>
</div>
<div class="flex items-center gap-3">
<div class="size-8 rounded-full bg-primary/20 flex items-center justify-center">
<Check class="size-4 text-primary" />
</div>
<span>灵活的卡密授权方案</span>
<span>{{ t('auth.register.feature3') }}</span>
</div>
</div>
</div>
@@ -122,60 +124,60 @@ async function handleRegister() {
<div class="size-10 rounded-xl bg-primary flex items-center justify-center">
<ShieldCheck class="size-6 text-primary-foreground" />
</div>
<span class="text-xl font-bold">管理平台</span>
<span class="text-xl font-bold">{{ t('auth.register.platformName') }}</span>
</div>
</div>
<div class="mb-8">
<h2 class="text-2xl font-bold mb-2">
创建账号
{{ t('auth.register.createAccount') }}
</h2>
<p class="text-muted-foreground">
填写以下信息完成注册
{{ t('auth.register.registerDesc') }}
</p>
</div>
<form class="space-y-5" @submit.prevent="handleRegister">
<div class="space-y-2">
<UiLabel for="username">用户名</UiLabel>
<UiLabel for="username">{{ t('auth.register.username') }}</UiLabel>
<UiInput
id="username"
v-model="form.username"
type="text"
placeholder="请输入用户名"
:placeholder="t('auth.register.usernamePlaceholder')"
:disabled="loading"
/>
</div>
<div class="space-y-2">
<UiLabel for="email">邮箱</UiLabel>
<UiLabel for="email">{{ t('auth.register.email') }}</UiLabel>
<UiInput
id="email"
v-model="form.email"
type="email"
placeholder="请输入邮箱"
:placeholder="t('auth.register.emailPlaceholder')"
:disabled="loading"
/>
</div>
<div class="space-y-2">
<UiLabel for="password">密码</UiLabel>
<UiLabel for="password">{{ t('auth.register.password') }}</UiLabel>
<UiInput
id="password"
v-model="form.password"
type="password"
placeholder="请输入密码(至少6位)"
:placeholder="t('auth.register.passwordPlaceholder')"
:disabled="loading"
/>
</div>
<div class="space-y-2">
<UiLabel for="confirmPassword">确认密码</UiLabel>
<UiLabel for="confirmPassword">{{ t('auth.register.confirmPassword') }}</UiLabel>
<UiInput
id="confirmPassword"
v-model="form.confirmPassword"
type="password"
placeholder="请再次输入密码"
:placeholder="t('auth.register.confirmPasswordPlaceholder')"
:disabled="loading"
/>
</div>
@@ -186,10 +188,10 @@ async function handleRegister() {
v-model:checked="form.agree"
/>
<UiLabel for="agree" class="text-sm font-normal cursor-pointer leading-tight">
我已阅读并同意
<a href="#" class="text-primary hover:underline">服务条款</a>
<a href="#" class="text-primary hover:underline">隐私政策</a>
{{ t('auth.register.agreeTerms') }}
<a href="#" class="text-primary hover:underline">{{ t('auth.register.termsOfService') }}</a>
{{ t('auth.register.and') }}
<a href="#" class="text-primary hover:underline">{{ t('auth.register.privacyPolicy') }}</a>
</UiLabel>
</div>
@@ -199,14 +201,14 @@ async function handleRegister() {
:disabled="loading || !isValid"
>
<Loader2 v-if="loading" class="mr-2 h-4 w-4 animate-spin" />
{{ loading ? '注册中...' : '注册' }}
{{ loading ? t('auth.register.registering') : t('auth.register.registerBtn') }}
</UiButton>
</form>
<div class="mt-6 text-center text-sm text-muted-foreground">
已有账号
{{ t('auth.register.hasAccount') }}
<router-link to="/login" class="text-primary hover:underline">
立即登录
{{ t('auth.register.loginNow') }}
</router-link>
</div>
</div>
+46 -44
View File
@@ -1,11 +1,13 @@
<script setup lang="ts">
import { Check, ChevronRight, Database, Loader2, RefreshCw, Server, ShieldCheck, User } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
import { resetInstallCheck } from '@/router/guard/index'
const { t } = useI18n()
const router = useRouter()
const loading = ref(false)
@@ -32,10 +34,10 @@ const form = ref({
redis_db: 0,
})
const dbTypes = [
{ value: 'sqlite', label: 'SQLite', desc: '轻量级,无需额外服务,适合小型部署' },
{ value: 'mysql', label: 'MySQL', desc: '高性能,适合生产环境' },
]
const dbTypes = computed(() => [
{ value: 'sqlite', label: t('install.dbTypeSqlite'), desc: t('install.dbTypeSqliteDesc') },
{ value: 'mysql', label: t('install.dbTypeMysql'), desc: t('install.dbTypeMysqlDesc') },
])
async function checkInstallStatus() {
checkingStatus.value = true
@@ -49,7 +51,7 @@ async function checkInstallStatus() {
}
}
catch (error) {
console.error('检查安装状态失败:', error)
console.error('Check install status failed:', error)
}
finally {
checkingStatus.value = false
@@ -76,12 +78,12 @@ async function testDatabase() {
})
const result = await res.json()
if (!res.ok || result.code !== 200) {
throw new Error(result.message || '数据库连接失败')
throw new Error(result.message || t('install.dbConnectFailed'))
}
toast.success(result.data?.message || '数据库连接成功')
toast.success(result.data?.message || t('install.dbConnectSuccess'))
}
catch (error: any) {
toast.error(error.message || '数据库连接失败')
toast.error(error.message || t('install.dbConnectFailed'))
}
finally {
testing.value = false
@@ -145,13 +147,13 @@ async function handleInstall() {
})
const result = await res.json()
if (!res.ok || result.code !== 200) {
throw new Error(result.message || '安装失败')
throw new Error(result.message || t('install.installFailed'))
}
toast.success(result.data?.message || '安装成功')
toast.success(result.data?.message || t('install.installSuccess'))
step.value = 4
}
catch (error: any) {
toast.error(error.message || '安装失败')
toast.error(error.message || t('install.installFailed'))
}
finally {
loading.value = false
@@ -180,7 +182,7 @@ onMounted(() => {
<div class="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-primary/5 via-background to-background">
<div v-if="checkingStatus" class="flex items-center gap-2">
<Loader2 class="size-5 animate-spin" />
<span>检查安装状态...</span>
<span>{{ t('install.checkingStatus') }}</span>
</div>
<div v-else class="w-full max-w-2xl">
@@ -191,10 +193,10 @@ onMounted(() => {
</div>
</div>
<h1 class="text-2xl font-bold">
系统安装向导
{{ t('install.wizardTitle') }}
</h1>
<p class="text-muted-foreground mt-2">
欢迎使用软件授权管理平台请完成以下配置
{{ t('install.wizardDesc') }}
</p>
</div>
@@ -224,12 +226,12 @@ onMounted(() => {
<div v-if="step === 1">
<div class="flex items-center gap-2 mb-6">
<Database class="size-5 text-primary" />
<h2 class="text-lg font-semibold">数据库配置</h2>
<h2 class="text-lg font-semibold">{{ t('install.dbConfig') }}</h2>
</div>
<div class="space-y-4">
<div class="space-y-3">
<UiLabel>数据库类型</UiLabel>
<UiLabel>{{ t('install.dbType') }}</UiLabel>
<div class="grid grid-cols-2 gap-3">
<button
v-for="db in dbTypes"
@@ -248,31 +250,31 @@ onMounted(() => {
<template v-if="form.db_type === 'mysql'">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel for="db_host">主机地址</UiLabel>
<UiLabel for="db_host">{{ t('install.dbHost') }}</UiLabel>
<UiInput id="db_host" v-model="form.db_host" placeholder="localhost" />
</div>
<div class="space-y-2">
<UiLabel for="db_port">端口</UiLabel>
<UiLabel for="db_port">{{ t('install.dbPort') }}</UiLabel>
<UiInput id="db_port" v-model="form.db_port" placeholder="3306" />
</div>
</div>
<div class="space-y-2">
<UiLabel for="db_name">数据库名</UiLabel>
<UiLabel for="db_name">{{ t('install.dbName') }}</UiLabel>
<UiInput id="db_name" v-model="form.db_name" placeholder="verification_platform" />
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel for="db_username">用户名</UiLabel>
<UiLabel for="db_username">{{ t('install.dbUsername') }}</UiLabel>
<UiInput id="db_username" v-model="form.db_username" placeholder="root" />
</div>
<div class="space-y-2">
<UiLabel for="db_password">密码</UiLabel>
<UiLabel for="db_password">{{ t('install.dbPassword') }}</UiLabel>
<UiInput id="db_password" v-model="form.db_password" type="password" placeholder="••••••••" />
</div>
</div>
<UiButton variant="outline" :disabled="testing" @click="testDatabase">
<Loader2 v-if="testing" class="mr-2 size-4 animate-spin" />
测试连接
{{ t('install.testConnection') }}
</UiButton>
</template>
</div>
@@ -281,17 +283,17 @@ onMounted(() => {
<div v-else-if="step === 2">
<div class="flex items-center gap-2 mb-6">
<ShieldCheck class="size-5 text-primary" />
<h2 class="text-lg font-semibold">安全配置</h2>
<h2 class="text-lg font-semibold">{{ t('install.securityConfig') }}</h2>
</div>
<div class="space-y-4">
<div class="space-y-2">
<UiLabel for="jwt_secret">JWT 密钥</UiLabel>
<UiLabel for="jwt_secret">{{ t('install.jwtSecret') }}</UiLabel>
<div class="flex gap-2">
<UiInput
id="jwt_secret"
v-model="form.jwt_secret"
placeholder="留空则自动生成"
:placeholder="t('install.jwtSecretPlaceholder')"
class="flex-1"
/>
<UiButton variant="outline" @click="generateJwtSecret">
@@ -299,33 +301,33 @@ onMounted(() => {
</UiButton>
</div>
<p class="text-sm text-muted-foreground">
JWT 密钥用于签名认证令牌请妥善保管
{{ t('install.jwtSecretHint') }}
</p>
</div>
<div class="p-4 rounded-lg bg-muted/50">
<div class="flex items-center gap-2 mb-2">
<Server class="size-4 text-muted-foreground" />
<span class="font-medium">Redis 缓存可选</span>
<span class="font-medium">{{ t('install.redisCache') }}</span>
</div>
<div class="flex items-center gap-2 mb-3">
<UiCheckbox id="use_redis" v-model:checked="form.use_redis" />
<UiLabel for="use_redis" class="text-sm font-normal cursor-pointer">启用 Redis</UiLabel>
<UiLabel for="use_redis" class="text-sm font-normal cursor-pointer">{{ t('install.enableRedis') }}</UiLabel>
</div>
<template v-if="form.use_redis">
<div class="grid grid-cols-2 gap-3">
<div class="space-y-2">
<UiLabel for="redis_host">主机</UiLabel>
<UiLabel for="redis_host">{{ t('install.redisHost') }}</UiLabel>
<UiInput id="redis_host" v-model="form.redis_host" placeholder="localhost" />
</div>
<div class="space-y-2">
<UiLabel for="redis_port">端口</UiLabel>
<UiLabel for="redis_port">{{ t('install.redisPort') }}</UiLabel>
<UiInput id="redis_port" v-model="form.redis_port" placeholder="6379" />
</div>
</div>
<div class="mt-3 space-y-2">
<UiLabel for="redis_password">密码</UiLabel>
<UiInput id="redis_password" v-model="form.redis_password" type="password" placeholder="可选" />
<UiLabel for="redis_password">{{ t('install.redisPassword') }}</UiLabel>
<UiInput id="redis_password" v-model="form.redis_password" type="password" :placeholder="t('install.redisPasswordPlaceholder')" />
</div>
</template>
</div>
@@ -335,23 +337,23 @@ onMounted(() => {
<div v-else-if="step === 3">
<div class="flex items-center gap-2 mb-6">
<User class="size-5 text-primary" />
<h2 class="text-lg font-semibold">管理员账号</h2>
<h2 class="text-lg font-semibold">{{ t('install.adminAccount') }}</h2>
</div>
<div class="space-y-4">
<div class="space-y-2">
<UiLabel for="admin_user">用户名</UiLabel>
<UiLabel for="admin_user">{{ t('install.adminUser') }}</UiLabel>
<UiInput id="admin_user" v-model="form.admin_user" placeholder="admin" />
</div>
<div class="space-y-2">
<UiLabel for="admin_pass">密码</UiLabel>
<UiInput id="admin_pass" v-model="form.admin_pass" type="password" placeholder="至少6位" />
<UiLabel for="admin_pass">{{ t('install.adminPass') }}</UiLabel>
<UiInput id="admin_pass" v-model="form.admin_pass" type="password" :placeholder="t('install.adminPassPlaceholder')" />
<p v-if="form.admin_pass && form.admin_pass.length < 6" class="text-sm text-destructive">
密码长度至少6位
{{ t('install.adminPassMinLength') }}
</p>
</div>
<div class="space-y-2">
<UiLabel for="admin_email">邮箱可选</UiLabel>
<UiLabel for="admin_email">{{ t('install.adminEmail') }}</UiLabel>
<UiInput id="admin_email" v-model="form.admin_email" type="email" placeholder="admin@example.com" />
</div>
</div>
@@ -362,12 +364,12 @@ onMounted(() => {
<div class="size-16 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center mx-auto mb-4">
<Check class="size-8 text-green-600 dark:text-green-400" />
</div>
<h2 class="text-xl font-semibold mb-2">安装完成</h2>
<h2 class="text-xl font-semibold mb-2">{{ t('install.installComplete') }}</h2>
<p class="text-muted-foreground mb-6">
系统已成功安装您现在可以使用管理员账号登录
{{ t('install.installCompleteDesc') }}
</p>
<UiButton @click="goLogin">
前往登录
{{ t('install.goToLogin') }}
<ChevronRight class="ml-2 size-4" />
</UiButton>
</div>
@@ -380,7 +382,7 @@ onMounted(() => {
variant="outline"
@click="step--"
>
上一步
{{ t('install.prevStep') }}
</UiButton>
<div v-else />
@@ -389,7 +391,7 @@ onMounted(() => {
:disabled="!canProceed"
@click="goNext"
>
下一步
{{ t('install.nextStep') }}
<ChevronRight class="ml-2 size-4" />
</UiButton>
<UiButton
@@ -398,7 +400,7 @@ onMounted(() => {
@click="handleInstall"
>
<Loader2 v-if="loading" class="mr-2 size-4 animate-spin" />
{{ loading ? '安装中...' : '开始安装' }}
{{ loading ? t('install.installing') : t('install.startInstall') }}
</UiButton>
</UiCardFooter>
</UiCard>
+70 -67
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
import { Activity, ArrowDownLeft, ArrowRight, ArrowUpRight, Calendar, Camera, Copy, Crown, Eye, EyeOff, HardDrive, Key, Loader2, Mail, Receipt, RefreshCw, RotateCcw, Save, Shield, User, Zap } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
import api from '@/services/api'
const { t } = useI18n()
interface UserProfile {
id: number
username: string
@@ -105,8 +108,8 @@ async function fetchProfile() {
}
}
catch (error) {
console.error('获取用户信息失败:', error)
toast.error('获取用户信息失败')
console.error('Fetch profile failed:', error)
toast.error(t('profile.loadUserFailed'))
}
finally {
loading.value = false
@@ -115,7 +118,7 @@ async function fetchProfile() {
async function handleUpdateProfile() {
if (!isProfileFormValid.value) {
toast.error('请填写完整信息')
toast.error(t('profile.fillComplete'))
return
}
@@ -126,7 +129,7 @@ async function handleUpdateProfile() {
email: profileForm.value.email,
phone: profileForm.value.phone,
})
toast.success('个人信息更新成功')
toast.success(t('profile.profileUpdateSuccess'))
if (user.value) {
user.value.username = profileForm.value.username
@@ -136,8 +139,8 @@ async function handleUpdateProfile() {
}
}
catch (error: any) {
console.error('更新个人信息失败:', error)
toast.error(error.message || '更新失败')
console.error('Update profile failed:', error)
toast.error(error.message || t('profile.profileUpdateFailed'))
}
finally {
saving.value = false
@@ -146,19 +149,19 @@ async function handleUpdateProfile() {
async function handleChangePassword() {
if (!passwordForm.value.currentPassword) {
toast.error('请输入当前密码')
toast.error(t('profile.currentPasswordRequired'))
return
}
if (!passwordForm.value.newPassword) {
toast.error('请输入新密码')
toast.error(t('profile.newPasswordRequired'))
return
}
if (passwordForm.value.newPassword !== passwordForm.value.confirmPassword) {
toast.error('两次输入的密码不一致')
toast.error(t('profile.passwordMismatch'))
return
}
if (passwordForm.value.newPassword.length < 6) {
toast.error('密码长度至少6位')
toast.error(t('profile.passwordMinLength'))
return
}
@@ -168,7 +171,7 @@ async function handleChangePassword() {
current_password: passwordForm.value.currentPassword,
new_password: passwordForm.value.newPassword,
})
toast.success('密码修改成功')
toast.success(t('profile.passwordChangeSuccess'))
showPasswordDialog.value = false
passwordForm.value = {
currentPassword: '',
@@ -177,8 +180,8 @@ async function handleChangePassword() {
}
}
catch (error: any) {
console.error('修改密码失败:', error)
toast.error(error.message || '修改密码失败')
console.error('Change password failed:', error)
toast.error(error.message || t('profile.passwordChangeFailed'))
}
finally {
changingPassword.value = false
@@ -196,12 +199,12 @@ async function handleAvatarChange(event: Event) {
return
if (!file.type.startsWith('image/')) {
toast.error('请选择图片文件')
toast.error(t('profile.selectImageFile'))
return
}
if (file.size > 2 * 1024 * 1024) {
toast.error('图片大小不能超过2MB')
toast.error(t('profile.imageSizeExceeded'))
return
}
@@ -224,12 +227,12 @@ async function handleAvatarChange(event: Event) {
user.value.avatar = data.avatar
localStorage.setItem('user', JSON.stringify(user.value))
}
toast.success('头像更新成功')
toast.success(t('profile.avatarUpdateSuccess'))
}
}
catch (error: any) {
console.error('上传头像失败:', error)
toast.error(error.message || '上传头像失败')
console.error('Upload avatar failed:', error)
toast.error(error.message || t('profile.avatarUpdateFailed'))
}
finally {
uploadingAvatar.value = false
@@ -273,26 +276,26 @@ function formatNumber(num: number) {
function getRoleName(role: string) {
const roles: Record<string, string> = {
admin: '管理员',
agent: '代理商',
admin: t('profile.roleAdmin'),
agent: t('profile.roleAgent'),
}
return roles[role] || role
}
function getStatusName(status: string) {
const statuses: Record<string, string> = {
active: '正常',
inactive: '未激活',
banned: '已封禁',
active: t('profile.statusActive'),
inactive: t('profile.statusInactive'),
banned: t('profile.statusBanned'),
}
return statuses[status] || status
}
function getTransactionType(type: string) {
const types: Record<string, string> = {
recharge: '充值',
consume: '消费',
refund: '退款',
recharge: t('profile.txRecharge'),
consume: t('profile.txConsume'),
refund: t('profile.txRefund'),
}
return types[type] || type
}
@@ -312,12 +315,12 @@ async function handleRegenerateApiToken() {
const data = await api.post<{ api_token: string }>('/dev/profile/api-token')
if (data?.api_token && user.value) {
user.value.api_token = data.api_token
toast.success('API Token 已重新生成')
toast.success(t('profile.tokenRegenerated'))
}
}
catch (error: any) {
console.error('重新生成API Token失败:', error)
toast.error(error.message || '重新生成失败')
console.error('Regenerate API token failed:', error)
toast.error(error.message || t('profile.tokenRegenerateFailed'))
}
finally {
regeneratingToken.value = false
@@ -327,7 +330,7 @@ async function handleRegenerateApiToken() {
function copyApiToken() {
if (user.value?.api_token) {
navigator.clipboard.writeText(user.value.api_token)
toast.success('API Token 已复制到剪贴板')
toast.success(t('profile.tokenCopied'))
}
}
@@ -380,7 +383,7 @@ onMounted(() => {
<div class="absolute -bottom-12 right-8 flex items-center gap-2">
<UiButton variant="outline" size="sm" @click="showPasswordDialog = true">
<Key class="mr-2 h-4 w-4" />
修改密码
{{ t('profile.changePassword') }}
</UiButton>
</div>
<input
@@ -412,7 +415,7 @@ onMounted(() => {
</span>
<span class="flex items-center gap-1">
<Calendar class="size-4" />
加入于 {{ formatDateShort(user?.created_at || '') }}
{{ t('profile.joinedAt') }} {{ formatDateShort(user?.created_at || '') }}
</span>
</div>
</div>
@@ -423,45 +426,45 @@ onMounted(() => {
<div class="bg-muted/30 px-6 py-4 border-b">
<h2 class="font-semibold flex items-center gap-2">
<User class="size-5 text-primary" />
基本信息
{{ t('profile.basicInfo') }}
</h2>
</div>
<UiCardContent class="p-6">
<div class="grid gap-5 sm:grid-cols-2">
<div class="space-y-2">
<UiLabel for="username" class="text-muted-foreground">
用户名
{{ t('profile.username') }}
</UiLabel>
<UiInput
id="username"
v-model="profileForm.username"
placeholder="请输入用户名"
:placeholder="t('profile.usernamePlaceholder')"
class="bg-background"
/>
</div>
<div class="space-y-2">
<UiLabel for="email" class="text-muted-foreground">
邮箱
{{ t('profile.email') }}
</UiLabel>
<UiInput
id="email"
v-model="profileForm.email"
type="email"
placeholder="请输入邮箱"
:placeholder="t('profile.emailPlaceholder')"
class="bg-background"
/>
</div>
<div class="space-y-2 sm:col-span-2">
<UiLabel for="phone" class="text-muted-foreground">
手机号
{{ t('profile.phone') }}
</UiLabel>
<UiInput
id="phone"
v-model="profileForm.phone"
type="tel"
placeholder="请输入手机号"
:placeholder="t('profile.phonePlaceholder')"
class="bg-background"
/>
</div>
@@ -474,7 +477,7 @@ onMounted(() => {
>
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
<Save v-else class="mr-2 h-4 w-4" />
保存修改
{{ t('profile.saveChanges') }}
</UiButton>
</div>
</UiCardContent>
@@ -490,7 +493,7 @@ onMounted(() => {
<UiCardContent class="p-6">
<div class="space-y-4">
<p class="text-sm text-muted-foreground">
API Token 用于调用开放 API 接口请妥善保管不要泄露给他人
{{ t('profile.apiTokenDesc') }}
</p>
<div class="flex items-center gap-2">
<code class="flex-1 p-3 bg-muted rounded-lg text-sm font-mono break-all border">
@@ -509,7 +512,7 @@ onMounted(() => {
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
{{ showApiToken ? '隐藏' : '显示' }}
{{ showApiToken ? t('profile.hide') : t('profile.show') }}
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
@@ -526,7 +529,7 @@ onMounted(() => {
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
复制
{{ t('profile.copy') }}
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
@@ -539,7 +542,7 @@ onMounted(() => {
>
<Loader2 v-if="regeneratingToken" class="mr-2 h-4 w-4 animate-spin" />
<RefreshCw v-else class="mr-2 h-4 w-4" />
{{ user?.api_token ? '重新生成' : '生成 Token' }}
{{ user?.api_token ? t('profile.regenerateToken') : t('profile.generateToken') }}
</UiButton>
</div>
</div>
@@ -550,11 +553,11 @@ onMounted(() => {
<div class="bg-muted/30 px-6 py-4 border-b flex items-center justify-between">
<h2 class="font-semibold flex items-center gap-2">
<Receipt class="size-5 text-primary" />
最近交易
{{ t('profile.recentTransactions') }}
</h2>
<UiButton variant="ghost" size="sm" as-child>
<router-link to="/admin/finance" class="text-primary">
查看全部
{{ t('profile.viewAll') }}
<ArrowRight class="ml-1 h-4 w-4" />
</router-link>
</UiButton>
@@ -604,7 +607,7 @@ onMounted(() => {
</div>
<div v-else class="text-center py-12 text-muted-foreground">
<Receipt class="size-16 mx-auto mb-3 opacity-30" />
<p>暂无交易记录</p>
<p>{{ t('profile.noTransactions') }}</p>
</div>
</UiCardContent>
</UiCard>
@@ -616,10 +619,10 @@ onMounted(() => {
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-muted-foreground">
当前套餐
{{ t('profile.currentPlan') }}
</p>
<p class="text-2xl font-bold mt-1">
{{ subscription?.plan || '基础版' }}
{{ subscription?.plan || t('profile.basicPlan') }}
</p>
</div>
<div class="size-12 rounded-xl bg-primary/10 flex items-center justify-center">
@@ -627,11 +630,11 @@ onMounted(() => {
</div>
</div>
<p class="text-sm text-muted-foreground mt-2">
有效期至 {{ formatDateShort(subscription?.expire_date || '') }}
{{ t('profile.expiresAt') }} {{ formatDateShort(subscription?.expire_date || '') }}
</p>
<UiButton class="w-full mt-4" as-child>
<router-link to="/billing">
升级套餐
{{ t('profile.upgradePlan') }}
</router-link>
</UiButton>
</div>
@@ -640,7 +643,7 @@ onMounted(() => {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground flex items-center gap-1">
<Zap class="size-4" />
API 调用
{{ t('profile.apiCalls') }}
</span>
<span class="font-medium">{{ formatNumber(subscription?.api_used || 0) }} / {{ formatNumber(subscription?.api_quota || 10000) }}</span>
</div>
@@ -657,7 +660,7 @@ onMounted(() => {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground flex items-center gap-1">
<HardDrive class="size-4" />
存储空间
{{ t('profile.storage') }}
</span>
<span class="font-medium">{{ formatBytes(subscription?.storage_used || 0) }} / {{ formatBytes(subscription?.storage_quota || 100 * 1024 * 1024) }}</span>
</div>
@@ -676,23 +679,23 @@ onMounted(() => {
<div class="bg-muted/30 px-6 py-4 border-b">
<h2 class="font-semibold flex items-center gap-2">
<Activity class="size-5 text-primary" />
账户统计
{{ t('profile.accountStats') }}
</h2>
</div>
<UiCardContent class="p-6">
<div class="space-y-4">
<div class="flex items-center justify-between py-2">
<span class="text-muted-foreground text-sm">上次登录</span>
<span class="text-muted-foreground text-sm">{{ t('profile.lastLogin') }}</span>
<span class="text-sm font-medium">{{ formatDate(user?.last_login_at || '') }}</span>
</div>
<div class="flex items-center justify-between py-2 border-t">
<span class="text-muted-foreground text-sm">账户状态</span>
<span class="text-muted-foreground text-sm">{{ t('profile.accountStatus') }}</span>
<UiBadge :class="user?.status === 'active' ? 'bg-green-500/10 text-green-500' : ''" variant="outline">
{{ getStatusName(user?.status || '') }}
</UiBadge>
</div>
<div class="flex items-center justify-between py-2 border-t">
<span class="text-muted-foreground text-sm">账户类型</span>
<span class="text-muted-foreground text-sm">{{ t('profile.accountType') }}</span>
<UiBadge variant="outline">
{{ getRoleName(user?.role || '') }}
</UiBadge>
@@ -708,56 +711,56 @@ onMounted(() => {
<UiDialog v-model:open="showPasswordDialog">
<UiDialogContent class="sm:max-w-md">
<UiDialogHeader>
<UiDialogTitle>修改密码</UiDialogTitle>
<UiDialogTitle>{{ t('profile.changePassword') }}</UiDialogTitle>
<UiDialogDescription>
请输入当前密码和新密码密码长度至少6位
{{ t('profile.changePasswordDesc') }}
</UiDialogDescription>
</UiDialogHeader>
<div class="space-y-4 py-4">
<div class="space-y-2">
<UiLabel for="currentPassword">
当前密码
{{ t('profile.currentPassword') }}
</UiLabel>
<UiInput
id="currentPassword"
v-model="passwordForm.currentPassword"
type="password"
placeholder="请输入当前密码"
:placeholder="t('profile.currentPasswordPlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel for="newPassword">
新密码
{{ t('profile.newPassword') }}
</UiLabel>
<UiInput
id="newPassword"
v-model="passwordForm.newPassword"
type="password"
placeholder="请输入新密码"
:placeholder="t('profile.newPasswordPlaceholder')"
/>
</div>
<div class="space-y-2">
<UiLabel for="confirmPassword">
确认密码
{{ t('profile.confirmPassword') }}
</UiLabel>
<UiInput
id="confirmPassword"
v-model="passwordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
:placeholder="t('profile.confirmPasswordPlaceholder')"
/>
</div>
</div>
<UiDialogFooter>
<UiButton variant="outline" @click="showPasswordDialog = false">
取消
{{ t('profile.cancel') }}
</UiButton>
<UiButton
:disabled="changingPassword || !isPasswordFormValid"
@click="handleChangePassword"
>
<Loader2 v-if="changingPassword" class="mr-2 h-4 w-4 animate-spin" />
确认修改
{{ t('profile.confirmChange') }}
</UiButton>
</UiDialogFooter>
</UiDialogContent>
+556 -7
View File
@@ -35,7 +35,49 @@
"next": "Next",
"yes": "Yes",
"no": "No",
"optional": "Optional"
"optional": "Optional",
"select": "Select",
"type": "Type",
"reason": "Reason",
"status": "Status",
"expiresAt": "Expires At",
"createdAt": "Created At",
"batchEnable": "Batch Enable",
"batchDisable": "Batch Disable",
"batchDelete": "Batch Delete",
"save": "Save",
"search": "Search",
"upload": "Upload",
"download": "Download",
"copy": "Copy",
"show": "Show",
"hide": "Hide",
"username": "Username",
"email": "Email",
"password": "Password",
"phone": "Phone",
"submit": "Submit",
"close": "Close",
"detail": "Detail",
"viewAll": "View All",
"noData": "No Data",
"active": "Active",
"inactive": "Inactive",
"banned": "Banned",
"online": "Online",
"offline": "Offline",
"enabled": "Enabled",
"disabled": "Disabled",
"unused": "Unused",
"used": "Used",
"expired": "Expired",
"recharge": "Recharge",
"consume": "Consume",
"refund": "Refund",
"admin": "Admin",
"agent": "Agent",
"basicPlan": "Basic Plan",
"normal": "Normal"
},
"premium": {
"premium": "premium",
@@ -1386,6 +1428,71 @@
"methods": {
"manual": "Manual",
"auto": "Auto"
},
"createForm": {
"title": "Create Version",
"description": "Create a new version for the application, upload ZIP update package and configure update strategy",
"editTitle": "Edit Version",
"editDescription": "Modify version information and update strategy",
"versionConfig": "Version Configuration",
"versionConfigDesc": "Fill in basic version information and upload update file",
"selectApp": "Select Application",
"selectAppPlaceholder": "Select application",
"versionNumber": "Version Number",
"versionPlaceholder": "e.g.: 1.0.0",
"minVersion": "Minimum Version",
"minVersionPlaceholder": "e.g.: 0.9.0 (full installation required below this version)",
"versionDesc": "Version Description",
"versionDescPlaceholder": "Briefly describe the updates in this version",
"updateFile": "Update File (ZIP format)",
"dragOrClick": "Drag ZIP file here or click to upload",
"selectFile": "Select File",
"uploadAndParse": "Upload & Parse",
"fileUploaded": "File uploaded and parsed",
"fileHash": "File Hash",
"containsFiles": "Contains {count} files",
"fileList": "File List",
"fileName": "File Name",
"filePath": "Path",
"fileSize": "Size",
"fileType": "Type",
"entryFile": "Entry File",
"entryFileDesc": "Select the main program file to launch after update (optional)",
"entryFilePlaceholder": "Select entry file (optional)",
"forcedUpdate": "Force Update",
"forcedUpdateDesc": "When enabled, users must update to this version to continue using",
"autoUpdate": "Auto Update",
"autoUpdateDesc": "When enabled, the application will automatically download and install updates",
"changelog": "Changelog",
"changelogPlaceholder": "Describe the updates, bug fixes, new features in detail...",
"preview": "Preview",
"previewApp": "Application",
"previewVersion": "Version",
"previewMinVersion": "Min Version",
"previewDesc": "Description",
"previewFile": "Update File",
"previewFileCount": "File Count",
"previewEntryFile": "Entry File",
"previewForcedUpdate": "Force Update",
"previewAutoUpdate": "Auto Update",
"changelogPreview": "Changelog Preview",
"noChangelog": "No changelog",
"notUploaded": "Not uploaded",
"publishVersion": "Publish Version",
"updateVersion": "Save Changes",
"zipOnly": "Only ZIP format files are supported",
"selectZipFirst": "Please select a ZIP file first",
"uploadSuccess": "File uploaded successfully",
"uploadFailed": "Upload failed",
"selectAppRequired": "Please select an application",
"versionRequired": "Please enter version number",
"uploadZipRequired": "Please upload a ZIP file first",
"createSuccess": "Version created successfully",
"createFailed": "Failed to create version",
"updateSuccess": "Version updated successfully",
"updateFailed": "Failed to update version",
"fetchFailed": "Failed to fetch version information",
"fetchAppsFailed": "Failed to fetch application list"
}
},
"cardTypes": {
@@ -1798,6 +1905,7 @@
"description": "Description",
"application": "Application",
"status": "Status",
"stream": "Record",
"createdAt": "Created At"
},
"types": {
@@ -1831,7 +1939,8 @@
"appRequired": "Please select application",
"status": "Status"
},
"createDescription": "Add a new cloud constant"
"createDescription": "Add a new cloud constant",
"downloadFile": "Download File"
},
"cloudVariables": {
"title": "Cloud Variables",
@@ -1887,6 +1996,7 @@
"description": "Description",
"application": "Application",
"status": "Status",
"stream": "Record",
"createdAt": "Created At"
},
"types": {
@@ -1937,6 +2047,7 @@
"scope": "Scope"
},
"createDescription": "Add a new cloud variable",
"downloadFile": "Download File",
"records": {
"title": "Variable Records",
"description": "View records for variable {key}",
@@ -2041,6 +2152,25 @@
"updateFailed": "Failed to update",
"fetchFailed": "Failed to fetch cloud function"
},
"riskControl": {
"title": "Risk Control",
"description": "Manage risk control rules and ban strategies",
"columns": {
"value": "Banned Value"
}
},
"sessions": {
"title": "Sessions",
"description": "Manage online sessions and instances",
"columns": {
"instanceId": "Instance ID",
"deviceIdentifier": "Device Fingerprint",
"deviceName": "Device Name",
"username": "Username",
"appName": "Application",
"lastHeartbeat": "Last Heartbeat"
}
},
"extension": {
"title": "Extension Configuration",
"description": "Manage Webhooks and Open API keys",
@@ -2212,6 +2342,408 @@
"appVariablesDesc": "Read/write application cloud variables"
}
}
},
"systemSettings": {
"title": "System Settings",
"description": "Configure system settings",
"loadFailed": "Failed to load settings",
"saveSuccess": "Settings saved successfully",
"saveFailed": "Failed to save settings",
"saveSettings": "Save Settings",
"tabs": {
"basic": "Basic",
"security": "Security",
"backup": "Backup",
"cleanup": "Data Cleanup",
"feature": "Features",
"notification": "Notifications"
},
"basic": {
"title": "Basic Settings",
"description": "Configure website basic information",
"siteName": "Site Name",
"siteNamePlaceholder": "Enter site name",
"siteLogo": "Site Logo",
"uploadLogo": "Upload Logo",
"logoHint": "Supports JPG, PNG, SVG formats",
"remove": "Remove",
"favicon": "Site Favicon",
"uploadFavicon": "Upload Favicon",
"faviconHint": "Recommended 32x32 or 64x64 pixels ICO/PNG",
"siteFooter": "Footer Text",
"siteFooterPlaceholder": "Enter footer text"
},
"security": {
"title": "Security Settings",
"description": "Configure system security options",
"enableCaptcha": "Login Captcha",
"enableCaptchaDesc": "Require captcha verification on login",
"loginFailLockCount": "Login Failure Lock Count",
"loginFailLockCountHint": "Number of consecutive failures before account lockout",
"lockMinutes": "Lock Duration (minutes)",
"lockMinutesHint": "Duration of account lockout",
"passwordMinLength": "Minimum Password Length",
"passwordMinLengthHint": "Minimum character count for user passwords",
"sessionTimeout": "Session Timeout (hours)",
"sessionTimeoutHint": "User login session validity period"
},
"backup": {
"title": "Backup Settings",
"description": "Configure automatic database backup",
"enableBackup": "Enable Auto Backup",
"enableBackupDesc": "Automatically backup database on schedule",
"backupInterval": "Backup Interval (hours)",
"backupIntervalHint": "Hours between each backup",
"backupRetention": "Retention Days",
"backupRetentionHint": "Number of days to keep backup files",
"storageType": "Storage Location",
"storageTypePlaceholder": "Select storage location",
"storageTypeHint": "Backup file storage location, configurable in storage management",
"local": "Local Storage",
"s3": "S3 Storage",
"webdav": "WebDAV",
"ftp": "FTP",
"sftp": "SFTP"
},
"feature": {
"title": "Feature Settings",
"description": "Configure system feature toggles",
"ticketSystem": "Ticket System",
"ticketSystemDesc": "Allow users to submit support tickets",
"multiLang": "Multi-language Support",
"multiLangDesc": "Allow users to switch languages",
"defaultTheme": "Default Color Mode",
"defaultThemePlaceholder": "Select default color mode",
"defaultThemeHint": "Default color mode for new users",
"followSystem": "Follow System",
"lightMode": "Light Mode",
"darkMode": "Dark Mode"
},
"notification": {
"title": "Notification Settings",
"description": "Configure system notification options",
"enableNotification": "Enable Email Notifications",
"enableNotificationDesc": "Send email notifications when enabled",
"adminNotifyEmail": "Admin Notification Email",
"adminNotifyEmailHint": "Admin email for receiving system notifications",
"notifyEvents": "Notification Events",
"loginNotify": "Abnormal Login Notification",
"loginNotifyDesc": "Send notification when abnormal login detected",
"rechargeNotify": "Recharge Notification",
"rechargeNotifyDesc": "Send notification when user recharge succeeds",
"ticketNotify": "Ticket Notification",
"ticketNotifyDesc": "Send notification when new ticket submitted"
},
"cleanup": {
"title": "Data Cleanup",
"description": "Configure automatic cleanup rules for expired data to free database space",
"enableAutoCleanup": "Enable Auto Cleanup",
"enableAutoCleanupDesc": "Automatically clean up expired data on schedule",
"cleanupInterval": "Cleanup Interval (hours)",
"cleanupIntervalHint": "Hours between each cleanup execution",
"captchaRetention": "Captcha Retention Days",
"captchaRetentionHint": "Days to retain expired captchas",
"verifyCodeRetention": "Email/SMS Code Retention Days",
"verifyCodeRetentionHint": "Days to retain used email/SMS verification codes",
"apiUsageRetention": "API Usage Log Retention Days",
"apiUsageRetentionHint": "Days to retain API usage statistics",
"webhookLogRetention": "Webhook Log Retention Days",
"webhookLogRetentionHint": "Days to retain webhook delivery logs",
"deviceSessionRetention": "Device Session Retention Days",
"deviceSessionRetentionHint": "Days to retain expired device session records",
"saveCleanupSettings": "Save Cleanup Settings",
"runManualCleanup": "Run Cleanup Now",
"cleanupSaveSuccess": "Cleanup settings saved successfully",
"cleanupSaveFailed": "Failed to save cleanup settings",
"manualCleanupSuccess": "Manual cleanup completed",
"manualCleanupFailed": "Manual cleanup failed"
},
"upload": {
"logoSuccess": "Logo uploaded successfully",
"faviconSuccess": "Favicon uploaded successfully",
"uploadFailed": "Upload failed"
}
}
},
"auth": {
"login": {
"platformName": "Admin Panel",
"heroTitle": "Software License Management Platform",
"heroDesc": "Professional software verification and license management solution, providing comprehensive protection for your software",
"quickIntegration": "Quick Integration",
"secureReliable": "Secure & Reliable",
"dataAnalysis": "Data Analysis",
"userManagement": "User Management",
"welcomeBack": "Welcome Back",
"loginDesc": "Enter your credentials to sign in",
"username": "Username",
"usernamePlaceholder": "Enter username",
"password": "Password",
"passwordPlaceholder": "Enter password",
"forgotPassword": "Forgot password?",
"rememberMe": "Remember me",
"loginBtn": "Sign In",
"loggingIn": "Signing in...",
"noAccount": "Don't have an account?",
"registerNow": "Sign up",
"fillRequired": "Please enter username and password",
"loginSuccess": "Login successful",
"loginFailed": "Login failed, please check your username and password"
},
"register": {
"platformName": "Admin Panel",
"heroTitle": "Get Started",
"heroDesc": "Create an account and experience professional software license management",
"feature1": "Create applications for free, quick SDK integration",
"feature2": "Comprehensive user management and data analysis",
"feature3": "Flexible card license solutions",
"createAccount": "Create Account",
"registerDesc": "Fill in the information below to register",
"username": "Username",
"usernamePlaceholder": "Enter username",
"email": "Email",
"emailPlaceholder": "Enter email",
"password": "Password",
"passwordPlaceholder": "Enter password (at least 6 characters)",
"confirmPassword": "Confirm Password",
"confirmPasswordPlaceholder": "Enter password again",
"agreeTerms": "I have read and agree to the",
"termsOfService": "Terms of Service",
"and": "and",
"privacyPolicy": "Privacy Policy",
"registerBtn": "Sign Up",
"registering": "Registering...",
"hasAccount": "Already have an account?",
"loginNow": "Sign in",
"usernameRequired": "Please enter username",
"emailRequired": "Please enter email",
"passwordRequired": "Please enter password",
"passwordMinLength": "Password must be at least 6 characters",
"passwordMismatch": "Passwords do not match",
"agreeRequired": "Please agree to the terms of service",
"registerSuccess": "Registration successful, please login",
"registerFailed": "Registration failed"
}
},
"install": {
"checkingStatus": "Checking installation status...",
"wizardTitle": "System Installation Wizard",
"wizardDesc": "Welcome to the Software License Management Platform, please complete the following configuration",
"checkFailed": "Failed to check installation status",
"dbConfig": "Database Configuration",
"dbType": "Database Type",
"dbTypeSqlite": "SQLite",
"dbTypeSqliteDesc": "Lightweight, no additional service required, suitable for small deployments",
"dbTypeMysql": "MySQL",
"dbTypeMysqlDesc": "High performance, suitable for production environments",
"dbHost": "Host Address",
"dbPort": "Port",
"dbName": "Database Name",
"dbUsername": "Username",
"dbPassword": "Password",
"testConnection": "Test Connection",
"dbConnectFailed": "Database connection failed",
"dbConnectSuccess": "Database connection successful",
"securityConfig": "Security Configuration",
"jwtSecret": "JWT Secret",
"jwtSecretPlaceholder": "Leave empty to auto-generate",
"jwtSecretHint": "JWT secret is used to sign authentication tokens, please keep it safe",
"redisCache": "Redis Cache (Optional)",
"enableRedis": "Enable Redis",
"redisHost": "Host",
"redisPort": "Port",
"redisPassword": "Password",
"redisPasswordPlaceholder": "Optional",
"adminAccount": "Admin Account",
"adminUser": "Username",
"adminPass": "Password",
"adminPassPlaceholder": "At least 6 characters",
"adminPassMinLength": "Password must be at least 6 characters",
"adminEmail": "Email (Optional)",
"installComplete": "Installation Complete",
"installCompleteDesc": "The system has been successfully installed, you can now login with your admin account",
"goToLogin": "Go to Login",
"prevStep": "Previous",
"nextStep": "Next",
"installing": "Installing...",
"startInstall": "Start Installation",
"installFailed": "Installation failed",
"installSuccess": "Installation successful"
},
"profile": {
"changePassword": "Change Password",
"changePasswordDesc": "Enter your current password and new password, minimum 6 characters",
"currentPassword": "Current Password",
"currentPasswordPlaceholder": "Enter current password",
"newPassword": "New Password",
"newPasswordPlaceholder": "Enter new password",
"confirmPassword": "Confirm Password",
"confirmPasswordPlaceholder": "Enter new password again",
"cancel": "Cancel",
"confirmChange": "Confirm Change",
"basicInfo": "Basic Information",
"username": "Username",
"usernamePlaceholder": "Enter username",
"email": "Email",
"emailPlaceholder": "Enter email",
"phone": "Phone",
"phonePlaceholder": "Enter phone number",
"saveChanges": "Save Changes",
"joinedAt": "Joined at",
"apiToken": "API Token",
"apiTokenDesc": "API Token is used to call open API endpoints, please keep it safe and do not share it.",
"show": "Show",
"hide": "Hide",
"copy": "Copy",
"regenerateToken": "Regenerate",
"generateToken": "Generate Token",
"tokenRegenerated": "API Token has been regenerated",
"tokenRegenerateFailed": "Failed to regenerate",
"tokenCopied": "API Token copied to clipboard",
"recentTransactions": "Recent Transactions",
"viewAll": "View All",
"noTransactions": "No transactions yet",
"currentPlan": "Current Plan",
"basicPlan": "Basic",
"expiresAt": "Expires at",
"upgradePlan": "Upgrade Plan",
"apiCalls": "API Calls",
"storage": "Storage",
"accountStats": "Account Statistics",
"lastLogin": "Last Login",
"accountStatus": "Account Status",
"accountType": "Account Type",
"loadUserFailed": "Failed to load user information",
"fillComplete": "Please fill in all required fields",
"profileUpdateSuccess": "Profile updated successfully",
"profileUpdateFailed": "Update failed",
"currentPasswordRequired": "Please enter current password",
"newPasswordRequired": "Please enter new password",
"passwordMismatch": "Passwords do not match",
"passwordMinLength": "Password must be at least 6 characters",
"passwordChangeSuccess": "Password changed successfully",
"passwordChangeFailed": "Failed to change password",
"selectImageFile": "Please select an image file",
"imageSizeExceeded": "Image size cannot exceed 2MB",
"avatarUpdateSuccess": "Avatar updated successfully",
"avatarUpdateFailed": "Failed to upload avatar",
"roleAdmin": "Admin",
"roleAgent": "Agent",
"statusActive": "Active",
"statusInactive": "Inactive",
"statusBanned": "Banned",
"txRecharge": "Recharge",
"txConsume": "Consume",
"txRefund": "Refund"
},
"agent": {
"dashboard": {
"title": "Dashboard",
"welcomeBack": "Welcome back, here's your business overview",
"loadStatsFailed": "Failed to load statistics",
"authorizedApps": "Authorized Apps",
"authorizedAppsCount": "Number of authorized applications",
"totalCards": "Total Cards",
"todayGenerated": "Today",
"totalUsers": "Total Users",
"totalUsersDesc": "Cumulative registered users",
"totalRevenue": "Total Revenue",
"todayRevenue": "Today",
"quickActions": "Quick Actions",
"generateCards": "Generate Cards",
"appManagement": "App Management",
"userManagement": "User Management",
"financeManagement": "Finance Management",
"recentCards": "Recently Generated Cards",
"noCards": "No card records yet",
"unused": "Unused",
"used": "Used"
},
"finance": {
"title": "Finance Management",
"description": "View your account balance and transaction records",
"loadFailed": "Failed to load financial data",
"accountBalance": "Account Balance",
"transactionRecords": "Transaction Records",
"noTransactions": "No transactions yet",
"balance": "Balance",
"recharge": "Recharge",
"consume": "Consume",
"refund": "Refund"
},
"users": {
"title": "User Management",
"description": "Manage users under your applications",
"loadFailed": "Failed to load user list",
"noUsers": "No user records yet",
"username": "Username",
"email": "Email",
"status": "Status",
"registerTime": "Register Time",
"lastLogin": "Last Login",
"active": "Active"
},
"apps": {
"title": "App Management",
"description": "Manage your authorized applications",
"loadFailed": "Failed to load app list",
"noApps": "No authorized apps yet",
"noDescription": "No description",
"cardTypesCount": "card types",
"view": "View",
"detail": {
"loadFailed": "Failed to load app details",
"noDescription": "No description",
"availableCardTypes": "Available Card Types",
"cardTypeDesc": "You can generate the following card types for this application",
"noCardTypes": "No available card types",
"duration": "Duration",
"days": "days",
"price": "Price",
"generateCards": "Generate Cards",
"notFound": "Application not found or access denied"
}
},
"cards": {
"title": "Card Management",
"description": "Manage your generated cards",
"loadFailed": "Failed to load card list",
"generateCards": "Generate Cards",
"noCards": "No card records yet",
"cardCode": "Card Code",
"app": "Application",
"cardType": "Card Type",
"status": "Status",
"createTime": "Create Time",
"useTime": "Use Time",
"unused": "Unused",
"used": "Used",
"expired": "Expired",
"disabled": "Disabled",
"create": {
"title": "Generate Cards",
"description": "Generate cards for authorized applications",
"loadFailed": "Failed to load data",
"selectApp": "Please select an application",
"selectCardType": "Please select a card type",
"quantityRange": "Quantity must be between 1 and 100",
"generateSuccess": "Successfully generated {count} cards",
"copiedToClipboard": "Cards copied to clipboard",
"generateFailed": "Generation failed",
"generateSettings": "Generation Settings",
"settingsDesc": "Select application and card type, set generation quantity",
"selectAppLabel": "Select Application",
"selectAppPlaceholder": "Please select an application",
"selectCardTypeLabel": "Select Card Type",
"selectCardTypePlaceholder": "Please select a card type",
"quantity": "Quantity",
"quantityPlaceholder": "Enter quantity",
"quantityHint": "Maximum 100 cards per generation",
"estimatedCost": "Estimated Cost",
"cancel": "Cancel",
"generating": "Generating...",
"generateBtn": "Generate Cards"
}
}
},
"pricing": {
@@ -2294,6 +2826,7 @@
"advancedFeatures": "Advanced",
"contentManagement": "Content",
"systemManagement": "System",
"financeGroup": "Finance",
"console": "Console",
"applications": "Applications",
"cardTypes": "Card Types",
@@ -2303,7 +2836,8 @@
"users": "Users",
"devices": "Devices",
"sessions": "Sessions",
"agentApps": "Agent Apps",
"agents": "Agents",
"agentApps": "Authorizations",
"finance": "Finance",
"logs": "Logs",
"tickets": "Tickets",
@@ -2317,17 +2851,32 @@
"docCategories": "Doc Categories",
"docList": "Documents",
"packages": "Packages",
"systemSettings": "System Settings",
"paymentChannels": "Payment Channels",
"emailSettings": "Email Settings",
"smsSettings": "SMS Settings",
"storageConfigs": "Storage",
"basicSettings": "Basic Settings",
"emailSettings": "Email Service",
"smsSettings": "SMS Service",
"paymentSettings": "Payment Settings",
"security": "Security Settings",
"profile": "Profile",
"adminDashboard": "Admin Dashboard",
"agentDashboard": "Agent Dashboard",
"logout": "Logout",
"login": "Login",
"register": "Register",
"navigation": "Navigation"
"navigation": "Navigation",
"create": "Create",
"edit": "Edit",
"detail": "Detail",
"recharge": "Recharge",
"request": "Request Auth",
"invite": "Invite Auth",
"records": "Records",
"basicPlan": "Basic Plan",
"normal": "Normal",
"storage": "Storage",
"admin": "Admin",
"agent": "Agent"
},
"footer": {
"brand": "Weishouquan",
+557 -8
View File
@@ -35,7 +35,49 @@
"next": "下一页",
"yes": "是",
"no": "否",
"optional": "可选"
"optional": "可选",
"select": "选择",
"type": "类型",
"reason": "原因",
"status": "状态",
"expiresAt": "过期时间",
"createdAt": "创建时间",
"batchEnable": "批量启用",
"batchDisable": "批量禁用",
"batchDelete": "批量删除",
"save": "保存",
"search": "搜索",
"upload": "上传",
"download": "下载",
"copy": "复制",
"show": "显示",
"hide": "隐藏",
"username": "用户名",
"email": "邮箱",
"password": "密码",
"phone": "手机号",
"submit": "提交",
"close": "关闭",
"detail": "详情",
"viewAll": "查看全部",
"noData": "暂无数据",
"active": "正常",
"inactive": "未激活",
"banned": "已封禁",
"online": "在线",
"offline": "离线",
"enabled": "已启用",
"disabled": "已禁用",
"unused": "未使用",
"used": "已使用",
"expired": "已过期",
"recharge": "充值",
"consume": "消费",
"refund": "退款",
"admin": "管理员",
"agent": "代理商",
"basicPlan": "基础版",
"normal": "正常"
},
"premium": {
"premium": "会员计划",
@@ -1353,6 +1395,71 @@
"methods": {
"manual": "手动更新",
"auto": "自动更新"
},
"createForm": {
"title": "创建版本",
"description": "为应用创建新的版本,上传ZIP更新包并配置更新策略",
"editTitle": "编辑版本",
"editDescription": "修改版本信息和更新策略",
"versionConfig": "版本配置",
"versionConfigDesc": "填写版本的基本信息和上传更新文件",
"selectApp": "选择应用",
"selectAppPlaceholder": "选择应用",
"versionNumber": "版本号",
"versionPlaceholder": "例如: 1.0.0",
"minVersion": "最低版本",
"minVersionPlaceholder": "例如: 0.9.0 (低于此版本需要完整安装)",
"versionDesc": "版本描述",
"versionDescPlaceholder": "简短描述此版本的更新内容",
"updateFile": "更新文件 (ZIP格式)",
"dragOrClick": "拖拽ZIP文件到此处或点击上传",
"selectFile": "选择文件",
"uploadAndParse": "上传解析",
"fileUploaded": "文件已上传并解析",
"fileHash": "文件哈希",
"containsFiles": "包含 {count} 个文件",
"fileList": "文件列表",
"fileName": "文件名",
"filePath": "路径",
"fileSize": "大小",
"fileType": "类型",
"entryFile": "入口文件",
"entryFileDesc": "选择更新完成后要启动的主程序文件(可选)",
"entryFilePlaceholder": "选择入口文件(可选)",
"forcedUpdate": "强制更新",
"forcedUpdateDesc": "启用后用户必须更新到此版本才能继续使用",
"autoUpdate": "自动更新",
"autoUpdateDesc": "启用后应用将自动下载并安装更新",
"changelog": "更新日志",
"changelogPlaceholder": "详细描述此版本的更新内容、修复的问题、新增的功能等...",
"preview": "预览",
"previewApp": "所属应用",
"previewVersion": "版本号",
"previewMinVersion": "最低版本",
"previewDesc": "版本描述",
"previewFile": "更新文件",
"previewFileCount": "文件数量",
"previewEntryFile": "入口文件",
"previewForcedUpdate": "强制更新",
"previewAutoUpdate": "自动更新",
"changelogPreview": "更新日志预览",
"noChangelog": "暂无更新日志",
"notUploaded": "未上传",
"publishVersion": "发布版本",
"updateVersion": "保存修改",
"zipOnly": "只支持ZIP格式文件",
"selectZipFirst": "请先选择ZIP文件",
"uploadSuccess": "文件上传成功",
"uploadFailed": "上传失败",
"selectAppRequired": "请选择应用",
"versionRequired": "请输入版本号",
"uploadZipRequired": "请先上传ZIP文件",
"createSuccess": "版本创建成功",
"createFailed": "创建失败",
"updateSuccess": "版本更新成功",
"updateFailed": "更新失败",
"fetchFailed": "获取版本信息失败",
"fetchAppsFailed": "获取应用列表失败"
}
},
"cardTypes": {
@@ -1788,6 +1895,7 @@
"description": "描述",
"application": "应用",
"status": "状态",
"stream": "记录",
"createdAt": "创建时间"
},
"types": {
@@ -1821,7 +1929,8 @@
"appRequired": "请选择应用",
"status": "启用状态"
},
"createDescription": "添加新的云端常量"
"createDescription": "添加新的云端常量",
"downloadFile": "下载文件"
},
"cloudVariables": {
"title": "云端变量",
@@ -1876,6 +1985,7 @@
"description": "描述",
"application": "应用",
"status": "状态",
"stream": "记录",
"createdAt": "创建时间"
},
"types": {
@@ -1926,6 +2036,7 @@
"scope": "作用域"
},
"createDescription": "添加新的云端变量",
"downloadFile": "下载文件",
"records": {
"title": "变量记录",
"description": "查看变量 {key} 的记录数据",
@@ -2030,6 +2141,25 @@
"updateFailed": "更新失败",
"fetchFailed": "获取云端函数失败"
},
"riskControl": {
"title": "风控管理",
"description": "管理风控规则和封禁策略",
"columns": {
"value": "封禁值"
}
},
"sessions": {
"title": "在线实例",
"description": "管理在线会话和实例",
"columns": {
"instanceId": "实例标识",
"deviceIdentifier": "设备指纹",
"deviceName": "设备名称",
"username": "所属用户",
"appName": "所属应用",
"lastHeartbeat": "最后心跳"
}
},
"extension": {
"title": "扩展配置",
"description": "管理Webhook和开放API密钥",
@@ -2201,6 +2331,408 @@
"appVariablesDesc": "读写应用云端变量"
}
}
},
"systemSettings": {
"title": "系统设置",
"description": "配置系统各项设置",
"loadFailed": "加载设置失败",
"saveSuccess": "保存成功",
"saveFailed": "保存失败",
"saveSettings": "保存设置",
"tabs": {
"basic": "基本设置",
"security": "安全设置",
"backup": "备份设置",
"cleanup": "数据清理",
"feature": "功能设置",
"notification": "通知设置"
},
"basic": {
"title": "基本设置",
"description": "配置网站基本信息",
"siteName": "网站名称",
"siteNamePlaceholder": "请输入网站名称",
"siteLogo": "网站Logo",
"uploadLogo": "上传Logo",
"logoHint": "支持 JPG、PNG、SVG 格式",
"remove": "移除",
"favicon": "网站图标 (Favicon)",
"uploadFavicon": "上传图标",
"faviconHint": "推荐 32x32 或 64x64 像素的 ICO/PNG",
"siteFooter": "页脚信息",
"siteFooterPlaceholder": "请输入页脚信息"
},
"security": {
"title": "安全设置",
"description": "配置系统安全相关选项",
"enableCaptcha": "登录验证码",
"enableCaptchaDesc": "启用后登录时需要输入验证码",
"loginFailLockCount": "登录失败锁定次数",
"loginFailLockCountHint": "连续失败多少次后锁定账户",
"lockMinutes": "锁定时长(分钟)",
"lockMinutesHint": "账户锁定持续时间",
"passwordMinLength": "密码最小长度",
"passwordMinLengthHint": "用户密码最小字符数",
"sessionTimeout": "会话超时(小时)",
"sessionTimeoutHint": "用户登录会话有效期"
},
"backup": {
"title": "备份设置",
"description": "配置数据库自动备份",
"enableBackup": "启用自动备份",
"enableBackupDesc": "定时自动备份数据库",
"backupInterval": "备份间隔(小时)",
"backupIntervalHint": "每隔多少小时备份一次",
"backupRetention": "保留天数",
"backupRetentionHint": "备份文件保留多少天",
"storageType": "存储位置",
"storageTypePlaceholder": "选择存储位置",
"storageTypeHint": "备份文件存储位置,可在存储管理中配置",
"local": "本地存储",
"s3": "S3存储",
"webdav": "WebDAV",
"ftp": "FTP",
"sftp": "SFTP"
},
"feature": {
"title": "功能设置",
"description": "配置系统功能开关",
"ticketSystem": "工单系统",
"ticketSystemDesc": "启用后用户可以提交工单",
"multiLang": "多语言支持",
"multiLangDesc": "启用后用户可以切换语言",
"defaultTheme": "默认颜色模式",
"defaultThemePlaceholder": "选择默认颜色模式",
"defaultThemeHint": "新用户默认的颜色模式",
"followSystem": "跟随系统",
"lightMode": "浅色模式",
"darkMode": "深色模式"
},
"notification": {
"title": "通知设置",
"description": "配置系统通知选项",
"enableNotification": "启用邮件通知",
"enableNotificationDesc": "启用后系统将发送邮件通知",
"adminNotifyEmail": "管理员通知邮箱",
"adminNotifyEmailHint": "接收系统通知的管理员邮箱",
"notifyEvents": "通知事件",
"loginNotify": "异常登录通知",
"loginNotifyDesc": "检测到异常登录时发送通知",
"rechargeNotify": "充值通知",
"rechargeNotifyDesc": "用户充值成功时发送通知",
"ticketNotify": "工单通知",
"ticketNotifyDesc": "新工单提交时发送通知"
},
"cleanup": {
"title": "数据清理",
"description": "配置过期数据自动清理规则,释放数据库空间",
"enableAutoCleanup": "启用自动清理",
"enableAutoCleanupDesc": "定时自动清理过期数据",
"cleanupInterval": "清理间隔(小时)",
"cleanupIntervalHint": "每隔多少小时执行一次清理",
"captchaRetention": "验证码保留天数",
"captchaRetentionHint": "图形验证码过期后保留天数",
"verifyCodeRetention": "邮箱/短信验证码保留天数",
"verifyCodeRetentionHint": "已使用的邮箱/短信验证码保留天数",
"apiUsageRetention": "API调用日志保留天数",
"apiUsageRetentionHint": "API调用统计记录保留天数",
"webhookLogRetention": "Webhook日志保留天数",
"webhookLogRetentionHint": "Webhook发送日志保留天数",
"deviceSessionRetention": "设备会话保留天数",
"deviceSessionRetentionHint": "过期的设备会话记录保留天数",
"saveCleanupSettings": "保存清理设置",
"runManualCleanup": "立即执行清理",
"cleanupSaveSuccess": "清理设置保存成功",
"cleanupSaveFailed": "保存清理设置失败",
"manualCleanupSuccess": "手动清理完成",
"manualCleanupFailed": "手动清理失败"
},
"upload": {
"logoSuccess": "Logo上传成功",
"faviconSuccess": "图标上传成功",
"uploadFailed": "上传失败"
}
}
},
"auth": {
"login": {
"platformName": "管理平台",
"heroTitle": "软件授权管理平台",
"heroDesc": "专业的软件验证与授权管理解决方案,为您的软件提供全方位的保护",
"quickIntegration": "快速集成",
"secureReliable": "安全可靠",
"dataAnalysis": "数据分析",
"userManagement": "用户管理",
"welcomeBack": "欢迎回来",
"loginDesc": "请输入您的账号信息登录系统",
"username": "用户名",
"usernamePlaceholder": "请输入用户名",
"password": "密码",
"passwordPlaceholder": "请输入密码",
"forgotPassword": "忘记密码?",
"rememberMe": "记住我",
"loginBtn": "登录",
"loggingIn": "登录中...",
"noAccount": "还没有账号?",
"registerNow": "立即注册",
"fillRequired": "请填写用户名和密码",
"loginSuccess": "登录成功",
"loginFailed": "登录失败,请检查用户名和密码"
},
"register": {
"platformName": "管理平台",
"heroTitle": "开始使用",
"heroDesc": "创建账号,立即体验专业的软件授权管理服务",
"feature1": "免费创建应用,快速集成 SDK",
"feature2": "完善的用户管理和数据分析",
"feature3": "灵活的卡密授权方案",
"createAccount": "创建账号",
"registerDesc": "填写以下信息完成注册",
"username": "用户名",
"usernamePlaceholder": "请输入用户名",
"email": "邮箱",
"emailPlaceholder": "请输入邮箱",
"password": "密码",
"passwordPlaceholder": "请输入密码(至少6位)",
"confirmPassword": "确认密码",
"confirmPasswordPlaceholder": "请再次输入密码",
"agreeTerms": "我已阅读并同意",
"termsOfService": "服务条款",
"and": "和",
"privacyPolicy": "隐私政策",
"registerBtn": "注册",
"registering": "注册中...",
"hasAccount": "已有账号?",
"loginNow": "立即登录",
"usernameRequired": "请输入用户名",
"emailRequired": "请输入邮箱",
"passwordRequired": "请输入密码",
"passwordMinLength": "密码长度至少6位",
"passwordMismatch": "两次输入的密码不一致",
"agreeRequired": "请阅读并同意服务条款",
"registerSuccess": "注册成功,请登录",
"registerFailed": "注册失败"
}
},
"install": {
"checkingStatus": "检查安装状态...",
"wizardTitle": "系统安装向导",
"wizardDesc": "欢迎使用软件授权管理平台,请完成以下配置",
"checkFailed": "检查安装状态失败",
"dbConfig": "数据库配置",
"dbType": "数据库类型",
"dbTypeSqlite": "SQLite",
"dbTypeSqliteDesc": "轻量级,无需额外服务,适合小型部署",
"dbTypeMysql": "MySQL",
"dbTypeMysqlDesc": "高性能,适合生产环境",
"dbHost": "主机地址",
"dbPort": "端口",
"dbName": "数据库名",
"dbUsername": "用户名",
"dbPassword": "密码",
"testConnection": "测试连接",
"dbConnectFailed": "数据库连接失败",
"dbConnectSuccess": "数据库连接成功",
"securityConfig": "安全配置",
"jwtSecret": "JWT 密钥",
"jwtSecretPlaceholder": "留空则自动生成",
"jwtSecretHint": "JWT 密钥用于签名认证令牌,请妥善保管",
"redisCache": "Redis 缓存(可选)",
"enableRedis": "启用 Redis",
"redisHost": "主机",
"redisPort": "端口",
"redisPassword": "密码",
"redisPasswordPlaceholder": "可选",
"adminAccount": "管理员账号",
"adminUser": "用户名",
"adminPass": "密码",
"adminPassPlaceholder": "至少6位",
"adminPassMinLength": "密码长度至少6位",
"adminEmail": "邮箱(可选)",
"installComplete": "安装完成",
"installCompleteDesc": "系统已成功安装,您现在可以使用管理员账号登录",
"goToLogin": "前往登录",
"prevStep": "上一步",
"nextStep": "下一步",
"installing": "安装中...",
"startInstall": "开始安装",
"installFailed": "安装失败",
"installSuccess": "安装成功"
},
"profile": {
"changePassword": "修改密码",
"changePasswordDesc": "请输入当前密码和新密码,密码长度至少6位",
"currentPassword": "当前密码",
"currentPasswordPlaceholder": "请输入当前密码",
"newPassword": "新密码",
"newPasswordPlaceholder": "请输入新密码",
"confirmPassword": "确认密码",
"confirmPasswordPlaceholder": "请再次输入新密码",
"cancel": "取消",
"confirmChange": "确认修改",
"basicInfo": "基本信息",
"username": "用户名",
"usernamePlaceholder": "请输入用户名",
"email": "邮箱",
"emailPlaceholder": "请输入邮箱",
"phone": "手机号",
"phonePlaceholder": "请输入手机号",
"saveChanges": "保存修改",
"joinedAt": "加入于",
"apiToken": "API Token",
"apiTokenDesc": "API Token 用于调用开放 API 接口,请妥善保管,不要泄露给他人。",
"show": "显示",
"hide": "隐藏",
"copy": "复制",
"regenerateToken": "重新生成",
"generateToken": "生成 Token",
"tokenRegenerated": "API Token 已重新生成",
"tokenRegenerateFailed": "重新生成失败",
"tokenCopied": "API Token 已复制到剪贴板",
"recentTransactions": "最近交易",
"viewAll": "查看全部",
"noTransactions": "暂无交易记录",
"currentPlan": "当前套餐",
"basicPlan": "基础版",
"expiresAt": "有效期至",
"upgradePlan": "升级套餐",
"apiCalls": "API 调用",
"storage": "存储空间",
"accountStats": "账户统计",
"lastLogin": "上次登录",
"accountStatus": "账户状态",
"accountType": "账户类型",
"loadUserFailed": "获取用户信息失败",
"fillComplete": "请填写完整信息",
"profileUpdateSuccess": "个人信息更新成功",
"profileUpdateFailed": "更新失败",
"currentPasswordRequired": "请输入当前密码",
"newPasswordRequired": "请输入新密码",
"passwordMismatch": "两次输入的密码不一致",
"passwordMinLength": "密码长度至少6位",
"passwordChangeSuccess": "密码修改成功",
"passwordChangeFailed": "修改密码失败",
"selectImageFile": "请选择图片文件",
"imageSizeExceeded": "图片大小不能超过2MB",
"avatarUpdateSuccess": "头像更新成功",
"avatarUpdateFailed": "上传头像失败",
"roleAdmin": "管理员",
"roleAgent": "代理商",
"statusActive": "正常",
"statusInactive": "未激活",
"statusBanned": "已封禁",
"txRecharge": "充值",
"txConsume": "消费",
"txRefund": "退款"
},
"agent": {
"dashboard": {
"title": "控制台",
"welcomeBack": "欢迎回来,查看您的业务概况",
"loadStatsFailed": "获取统计数据失败",
"authorizedApps": "授权应用",
"authorizedAppsCount": "已授权的应用数量",
"totalCards": "卡密总数",
"todayGenerated": "今日生成",
"totalUsers": "用户总数",
"totalUsersDesc": "累计注册用户",
"totalRevenue": "累计收入",
"todayRevenue": "今日",
"quickActions": "快捷操作",
"generateCards": "生成卡密",
"appManagement": "应用管理",
"userManagement": "用户管理",
"financeManagement": "财务管理",
"recentCards": "最近生成卡密",
"noCards": "暂无卡密记录",
"unused": "未使用",
"used": "已使用"
},
"finance": {
"title": "财务管理",
"description": "查看您的账户余额和交易记录",
"loadFailed": "获取财务数据失败",
"accountBalance": "账户余额",
"transactionRecords": "交易记录",
"noTransactions": "暂无交易记录",
"balance": "余额",
"recharge": "充值",
"consume": "消费",
"refund": "退款"
},
"users": {
"title": "用户管理",
"description": "管理您应用下的用户",
"loadFailed": "获取用户列表失败",
"noUsers": "暂无用户记录",
"username": "用户名",
"email": "邮箱",
"status": "状态",
"registerTime": "注册时间",
"lastLogin": "最后登录",
"active": "正常"
},
"apps": {
"title": "应用管理",
"description": "管理您授权的应用",
"loadFailed": "获取应用列表失败",
"noApps": "暂无授权应用",
"noDescription": "暂无描述",
"cardTypesCount": "种卡类",
"view": "查看",
"detail": {
"loadFailed": "获取应用详情失败",
"noDescription": "暂无描述",
"availableCardTypes": "可用卡类",
"cardTypeDesc": "您可以为此应用生成以下类型的卡密",
"noCardTypes": "暂无可用卡类",
"duration": "时长",
"days": "天",
"price": "价格",
"generateCards": "生成卡密",
"notFound": "应用不存在或无权访问"
}
},
"cards": {
"title": "卡密管理",
"description": "管理您生成的卡密",
"loadFailed": "获取卡密列表失败",
"generateCards": "生成卡密",
"noCards": "暂无卡密记录",
"cardCode": "卡密",
"app": "应用",
"cardType": "卡类",
"status": "状态",
"createTime": "创建时间",
"useTime": "使用时间",
"unused": "未使用",
"used": "已使用",
"expired": "已过期",
"disabled": "已禁用",
"create": {
"title": "生成卡密",
"description": "为授权应用生成卡密",
"loadFailed": "获取数据失败",
"selectApp": "请选择应用",
"selectCardType": "请选择卡类",
"quantityRange": "生成数量需要在 1-100 之间",
"generateSuccess": "成功生成 {count} 张卡密",
"copiedToClipboard": "卡密已复制到剪贴板",
"generateFailed": "生成失败",
"generateSettings": "生成设置",
"settingsDesc": "选择应用和卡类,设置生成数量",
"selectAppLabel": "选择应用",
"selectAppPlaceholder": "请选择应用",
"selectCardTypeLabel": "选择卡类",
"selectCardTypePlaceholder": "请选择卡类",
"quantity": "生成数量",
"quantityPlaceholder": "请输入生成数量",
"quantityHint": "单次最多生成 100 张卡密",
"estimatedCost": "预计费用",
"cancel": "取消",
"generating": "生成中...",
"generateBtn": "生成卡密"
}
}
},
"pricing": {
@@ -2283,6 +2815,7 @@
"advancedFeatures": "高级功能",
"contentManagement": "内容管理",
"systemManagement": "系统管理",
"financeGroup": "财务",
"console": "控制台",
"applications": "应用管理",
"cardTypes": "卡类管理",
@@ -2292,7 +2825,8 @@
"users": "用户管理",
"devices": "设备管理",
"sessions": "在线实例",
"agentApps": "代理授权",
"agents": "代理管理",
"agentApps": "授权管理",
"finance": "财务管理",
"logs": "日志记录",
"tickets": "工单系统",
@@ -2306,17 +2840,32 @@
"docCategories": "文档分类",
"docList": "文档列表",
"packages": "套餐管理",
"systemSettings": "系统设置",
"paymentChannels": "支付渠道",
"emailSettings": "邮箱配置",
"smsSettings": "短信配置",
"storageConfigs": "存储管理",
"basicSettings": "基本设置",
"emailSettings": "邮件服务",
"smsSettings": "短信服务",
"paymentSettings": "支付配置",
"security": "安全设置",
"profile": "个人中心",
"adminDashboard": "管理后台",
"agentDashboard": "代理后台",
"agentDashboard": "代理后台",
"logout": "退出登录",
"login": "登录",
"register": "注册",
"navigation": "导航"
"navigation": "导航",
"create": "创建",
"edit": "编辑",
"detail": "详情",
"recharge": "充值",
"request": "申请授权",
"invite": "邀请授权",
"records": "记录",
"basicPlan": "基础版",
"normal": "正常",
"storage": "存储",
"admin": "管理员",
"agent": "代理商"
},
"footer": {
"brand": "微授权",