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:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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">支持 JPG、PNG、SVG 格式</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))
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "微授权",
|
||||
|
||||
Reference in New Issue
Block a user