Files
verify/frontend/src/pages/install/index.vue
T
admin c816f4d85c 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
2026-05-07 23:06:58 +08:00

410 lines
14 KiB
Vue

<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)
const testing = ref(false)
const step = ref(1)
const installStatus = ref<any>(null)
const checkingStatus = ref(true)
const form = ref({
db_type: 'sqlite',
db_host: 'localhost',
db_port: '3306',
db_name: 'verification_platform',
db_username: 'root',
db_password: '',
jwt_secret: '',
admin_user: 'admin',
admin_pass: '',
admin_email: '',
use_redis: false,
redis_host: 'localhost',
redis_port: '6379',
redis_password: '',
redis_db: 0,
})
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
try {
const res = await fetch('/api/install/status')
const data = await res.json()
installStatus.value = data?.data
if (data?.data?.installed) {
resetInstallCheck()
router.push('/login')
}
}
catch (error) {
console.error('Check install status failed:', error)
}
finally {
checkingStatus.value = false
}
}
async function testDatabase() {
testing.value = true
try {
const payload: any = {
db_type: form.value.db_type,
}
if (form.value.db_type === 'mysql') {
payload.host = form.value.db_host
payload.port = form.value.db_port
payload.name = form.value.db_name
payload.username = form.value.db_username
payload.password = form.value.db_password
}
const res = await fetch('/api/install/test-db', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
const result = await res.json()
if (!res.ok || result.code !== 200) {
throw new Error(result.message || t('install.dbConnectFailed'))
}
toast.success(result.data?.message || t('install.dbConnectSuccess'))
}
catch (error: any) {
toast.error(error.message || t('install.dbConnectFailed'))
}
finally {
testing.value = false
}
}
function generateJwtSecret() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < 32; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
form.value.jwt_secret = result
}
const canProceed = computed(() => {
if (step.value === 1) {
if (form.value.db_type === 'sqlite') return true
return form.value.db_host && form.value.db_port && form.value.db_name && form.value.db_username
}
if (step.value === 2) {
return true
}
if (step.value === 3) {
return form.value.admin_user && form.value.admin_pass && form.value.admin_pass.length >= 6
}
return true
})
async function handleInstall() {
loading.value = true
try {
const payload: any = {
db_type: form.value.db_type,
jwt_secret: form.value.jwt_secret,
admin_user: form.value.admin_user,
admin_pass: form.value.admin_pass,
admin_email: form.value.admin_email,
use_redis: form.value.use_redis,
}
if (form.value.db_type === 'mysql') {
payload.db_host = form.value.db_host
payload.db_port = form.value.db_port
payload.db_name = form.value.db_name
payload.db_username = form.value.db_username
payload.db_password = form.value.db_password
}
if (form.value.use_redis) {
payload.redis_host = form.value.redis_host
payload.redis_port = form.value.redis_port
payload.redis_password = form.value.redis_password
payload.redis_db = form.value.redis_db
}
const res = await fetch('/api/install/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
const result = await res.json()
if (!res.ok || result.code !== 200) {
throw new Error(result.message || t('install.installFailed'))
}
toast.success(result.data?.message || t('install.installSuccess'))
step.value = 4
}
catch (error: any) {
toast.error(error.message || t('install.installFailed'))
}
finally {
loading.value = false
}
}
function goNext() {
if (step.value < 3) {
step.value++
}
}
function goLogin() {
localStorage.removeItem('token')
localStorage.removeItem('user')
resetInstallCheck()
router.push('/login')
}
onMounted(() => {
checkInstallStatus()
})
</script>
<template>
<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>{{ t('install.checkingStatus') }}</span>
</div>
<div v-else class="w-full max-w-2xl">
<div class="text-center mb-8">
<div class="flex items-center justify-center gap-3 mb-4">
<div class="size-12 rounded-xl bg-primary flex items-center justify-center">
<ShieldCheck class="size-7 text-primary-foreground" />
</div>
</div>
<h1 class="text-2xl font-bold">
{{ t('install.wizardTitle') }}
</h1>
<p class="text-muted-foreground mt-2">
{{ t('install.wizardDesc') }}
</p>
</div>
<div class="flex items-center justify-center gap-2 mb-8">
<div
v-for="s in 4"
:key="s"
class="flex items-center"
>
<div
class="size-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors"
:class="step >= s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'"
>
<Check v-if="step > s" class="size-4" />
<span v-else>{{ s }}</span>
</div>
<div
v-if="s < 4"
class="w-12 h-0.5 mx-1"
:class="step > s ? 'bg-primary' : 'bg-muted'"
/>
</div>
</div>
<UiCard>
<UiCardContent class="p-6">
<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">{{ t('install.dbConfig') }}</h2>
</div>
<div class="space-y-4">
<div class="space-y-3">
<UiLabel>{{ t('install.dbType') }}</UiLabel>
<div class="grid grid-cols-2 gap-3">
<button
v-for="db in dbTypes"
:key="db.value"
type="button"
class="p-4 rounded-lg border-2 text-left transition-all"
:class="form.db_type === db.value ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50'"
@click="form.db_type = db.value"
>
<div class="font-medium">{{ db.label }}</div>
<div class="text-sm text-muted-foreground">{{ db.desc }}</div>
</button>
</div>
</div>
<template v-if="form.db_type === 'mysql'">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<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">{{ 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">{{ 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">{{ 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">{{ 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>
</div>
<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">{{ t('install.securityConfig') }}</h2>
</div>
<div class="space-y-4">
<div class="space-y-2">
<UiLabel for="jwt_secret">{{ t('install.jwtSecret') }}</UiLabel>
<div class="flex gap-2">
<UiInput
id="jwt_secret"
v-model="form.jwt_secret"
:placeholder="t('install.jwtSecretPlaceholder')"
class="flex-1"
/>
<UiButton variant="outline" @click="generateJwtSecret">
<RefreshCw class="size-4" />
</UiButton>
</div>
<p class="text-sm text-muted-foreground">
{{ 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">{{ 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">{{ 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">{{ 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">{{ 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">{{ t('install.redisPassword') }}</UiLabel>
<UiInput id="redis_password" v-model="form.redis_password" type="password" :placeholder="t('install.redisPasswordPlaceholder')" />
</div>
</template>
</div>
</div>
</div>
<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">{{ t('install.adminAccount') }}</h2>
</div>
<div class="space-y-4">
<div class="space-y-2">
<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">{{ 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">
{{ t('install.adminPassMinLength') }}
</p>
</div>
<div class="space-y-2">
<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>
</div>
<div v-else-if="step === 4">
<div class="text-center py-8">
<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">{{ 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>
</div>
</UiCardContent>
<UiCardFooter v-if="step < 4" class="flex justify-between border-t p-6">
<UiButton
v-if="step > 1"
variant="outline"
@click="step--"
>
{{ t('install.prevStep') }}
</UiButton>
<div v-else />
<UiButton
v-if="step < 3"
:disabled="!canProceed"
@click="goNext"
>
{{ t('install.nextStep') }}
<ChevronRight class="ml-2 size-4" />
</UiButton>
<UiButton
v-else
:disabled="!canProceed || loading"
@click="handleInstall"
>
<Loader2 v-if="loading" class="mr-2 size-4 animate-spin" />
{{ loading ? t('install.installing') : t('install.startInstall') }}
</UiButton>
</UiCardFooter>
</UiCard>
</div>
</div>
</template>