style(frontend): 统一 Views 模块代码风格
- 移除语句末尾分号,规范代码格式 - 优化组件结构和类型定义 - 改进视图文档和示例 - 提升代码一致性
This commit is contained in:
@@ -1,12 +1,30 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-dark-900 dark:to-dark-800 flex items-center justify-center p-4">
|
||||
<div
|
||||
class="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 p-4 dark:from-dark-900 dark:to-dark-800"
|
||||
>
|
||||
<div class="w-full max-w-2xl">
|
||||
<!-- Logo & Title -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary-500 to-primary-600 shadow-lg mb-4">
|
||||
<svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<div class="mb-8 text-center">
|
||||
<div
|
||||
class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-500 to-primary-600 shadow-lg"
|
||||
>
|
||||
<svg
|
||||
class="h-8 w-8 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Sub2API Setup</h1>
|
||||
@@ -20,63 +38,110 @@
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
:class="[
|
||||
'w-10 h-10 rounded-full flex items-center justify-center font-semibold text-sm transition-all',
|
||||
'flex h-10 w-10 items-center justify-center rounded-full text-sm font-semibold transition-all',
|
||||
currentStep > index
|
||||
? 'bg-primary-500 text-white'
|
||||
: currentStep === index
|
||||
? 'bg-primary-500 text-white ring-4 ring-primary-100 dark:ring-primary-900'
|
||||
: 'bg-gray-200 dark:bg-dark-700 text-gray-500 dark:text-dark-400'
|
||||
? 'bg-primary-500 text-white ring-4 ring-primary-100 dark:ring-primary-900'
|
||||
: 'bg-gray-200 text-gray-500 dark:bg-dark-700 dark:text-dark-400'
|
||||
]"
|
||||
>
|
||||
<svg v-if="currentStep > index" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
v-if="currentStep > index"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
<span v-else>{{ index + 1 }}</span>
|
||||
</div>
|
||||
<span class="ml-2 text-sm font-medium" :class="currentStep >= index ? 'text-gray-900 dark:text-white' : 'text-gray-400 dark:text-dark-500'">
|
||||
<span
|
||||
class="ml-2 text-sm font-medium"
|
||||
:class="
|
||||
currentStep >= index
|
||||
? 'text-gray-900 dark:text-white'
|
||||
: 'text-gray-400 dark:text-dark-500'
|
||||
"
|
||||
>
|
||||
{{ step.title }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="index < steps.length - 1" class="w-12 h-0.5 mx-3" :class="currentStep > index ? 'bg-primary-500' : 'bg-gray-200 dark:bg-dark-700'"></div>
|
||||
<div
|
||||
v-if="index < steps.length - 1"
|
||||
class="mx-3 h-0.5 w-12"
|
||||
:class="currentStep > index ? 'bg-primary-500' : 'bg-gray-200 dark:bg-dark-700'"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step Content -->
|
||||
<div class="bg-white dark:bg-dark-800 rounded-2xl shadow-xl p-8">
|
||||
<div class="rounded-2xl bg-white p-8 shadow-xl dark:bg-dark-800">
|
||||
<!-- Step 1: Database -->
|
||||
<div v-if="currentStep === 0" class="space-y-6">
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Database Configuration</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-dark-400 mt-1">Connect to your PostgreSQL database</p>
|
||||
<div class="mb-6 text-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Database Configuration
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
||||
Connect to your PostgreSQL database
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="input-label">Host</label>
|
||||
<input v-model="formData.database.host" type="text" class="input" placeholder="localhost" />
|
||||
<input
|
||||
v-model="formData.database.host"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="localhost"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">Port</label>
|
||||
<input v-model.number="formData.database.port" type="number" class="input" placeholder="5432" />
|
||||
<input
|
||||
v-model.number="formData.database.port"
|
||||
type="number"
|
||||
class="input"
|
||||
placeholder="5432"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="input-label">Username</label>
|
||||
<input v-model="formData.database.user" type="text" class="input" placeholder="postgres" />
|
||||
<input
|
||||
v-model="formData.database.user"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="postgres"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">Password</label>
|
||||
<input v-model="formData.database.password" type="password" class="input" placeholder="Password" />
|
||||
<input
|
||||
v-model="formData.database.password"
|
||||
type="password"
|
||||
class="input"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="input-label">Database Name</label>
|
||||
<input v-model="formData.database.dbname" type="text" class="input" placeholder="sub2api" />
|
||||
<input
|
||||
v-model="formData.database.dbname"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="sub2api"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">SSL Mode</label>
|
||||
@@ -94,43 +159,90 @@
|
||||
:disabled="testingDb"
|
||||
class="btn btn-secondary w-full"
|
||||
>
|
||||
<svg v-if="testingDb" class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg
|
||||
v-if="testingDb"
|
||||
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg v-else-if="dbConnected" class="w-5 h-5 mr-2 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
v-else-if="dbConnected"
|
||||
class="mr-2 h-5 w-5 text-green-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
{{ testingDb ? 'Testing...' : dbConnected ? 'Connection Successful' : 'Test Connection' }}
|
||||
{{
|
||||
testingDb ? 'Testing...' : dbConnected ? 'Connection Successful' : 'Test Connection'
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Redis -->
|
||||
<div v-if="currentStep === 1" class="space-y-6">
|
||||
<div class="text-center mb-6">
|
||||
<div class="mb-6 text-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Redis Configuration</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-dark-400 mt-1">Connect to your Redis server</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
||||
Connect to your Redis server
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="input-label">Host</label>
|
||||
<input v-model="formData.redis.host" type="text" class="input" placeholder="localhost" />
|
||||
<input
|
||||
v-model="formData.redis.host"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="localhost"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">Port</label>
|
||||
<input v-model.number="formData.redis.port" type="number" class="input" placeholder="6379" />
|
||||
<input
|
||||
v-model.number="formData.redis.port"
|
||||
type="number"
|
||||
class="input"
|
||||
placeholder="6379"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="input-label">Password (optional)</label>
|
||||
<input v-model="formData.redis.password" type="password" class="input" placeholder="Password" />
|
||||
<input
|
||||
v-model="formData.redis.password"
|
||||
type="password"
|
||||
class="input"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">Database</label>
|
||||
<input v-model.number="formData.redis.db" type="number" class="input" placeholder="0" />
|
||||
<input
|
||||
v-model.number="formData.redis.db"
|
||||
type="number"
|
||||
class="input"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -139,38 +251,87 @@
|
||||
:disabled="testingRedis"
|
||||
class="btn btn-secondary w-full"
|
||||
>
|
||||
<svg v-if="testingRedis" class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg
|
||||
v-if="testingRedis"
|
||||
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg v-else-if="redisConnected" class="w-5 h-5 mr-2 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
v-else-if="redisConnected"
|
||||
class="mr-2 h-5 w-5 text-green-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
{{ testingRedis ? 'Testing...' : redisConnected ? 'Connection Successful' : 'Test Connection' }}
|
||||
{{
|
||||
testingRedis
|
||||
? 'Testing...'
|
||||
: redisConnected
|
||||
? 'Connection Successful'
|
||||
: 'Test Connection'
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Admin -->
|
||||
<div v-if="currentStep === 2" class="space-y-6">
|
||||
<div class="text-center mb-6">
|
||||
<div class="mb-6 text-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Admin Account</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-dark-400 mt-1">Create your administrator account</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
||||
Create your administrator account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">Email</label>
|
||||
<input v-model="formData.admin.email" type="email" class="input" placeholder="admin@example.com" />
|
||||
<input
|
||||
v-model="formData.admin.email"
|
||||
type="email"
|
||||
class="input"
|
||||
placeholder="admin@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">Password</label>
|
||||
<input v-model="formData.admin.password" type="password" class="input" placeholder="Min 6 characters" />
|
||||
<input
|
||||
v-model="formData.admin.password"
|
||||
type="password"
|
||||
class="input"
|
||||
placeholder="Min 6 characters"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">Confirm Password</label>
|
||||
<input v-model="confirmPassword" type="password" class="input" placeholder="Confirm password" />
|
||||
<p v-if="confirmPassword && formData.admin.password !== confirmPassword" class="input-error-text">
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
class="input"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<p
|
||||
v-if="confirmPassword && formData.admin.password !== confirmPassword"
|
||||
class="input-error-text"
|
||||
>
|
||||
Passwords do not match
|
||||
</p>
|
||||
</div>
|
||||
@@ -178,53 +339,110 @@
|
||||
|
||||
<!-- Step 4: Complete -->
|
||||
<div v-if="currentStep === 3" class="space-y-6">
|
||||
<div class="text-center mb-6">
|
||||
<div class="mb-6 text-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Ready to Install</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-dark-400 mt-1">Review your configuration and complete setup</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-dark-400">
|
||||
Review your configuration and complete setup
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-gray-50 dark:bg-dark-700 rounded-xl">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-dark-400 mb-2">Database</h3>
|
||||
<p class="text-gray-900 dark:text-white">{{ formData.database.user }}@{{ formData.database.host }}:{{ formData.database.port }}/{{ formData.database.dbname }}</p>
|
||||
<div class="rounded-xl bg-gray-50 p-4 dark:bg-dark-700">
|
||||
<h3 class="mb-2 text-sm font-medium text-gray-500 dark:text-dark-400">Database</h3>
|
||||
<p class="text-gray-900 dark:text-white">
|
||||
{{ formData.database.user }}@{{ formData.database.host }}:{{
|
||||
formData.database.port
|
||||
}}/{{ formData.database.dbname }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-dark-700 rounded-xl">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-dark-400 mb-2">Redis</h3>
|
||||
<p class="text-gray-900 dark:text-white">{{ formData.redis.host }}:{{ formData.redis.port }}</p>
|
||||
<div class="rounded-xl bg-gray-50 p-4 dark:bg-dark-700">
|
||||
<h3 class="mb-2 text-sm font-medium text-gray-500 dark:text-dark-400">Redis</h3>
|
||||
<p class="text-gray-900 dark:text-white">
|
||||
{{ formData.redis.host }}:{{ formData.redis.port }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-dark-700 rounded-xl">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-dark-400 mb-2">Admin Email</h3>
|
||||
<div class="rounded-xl bg-gray-50 p-4 dark:bg-dark-700">
|
||||
<h3 class="mb-2 text-sm font-medium text-gray-500 dark:text-dark-400">Admin Email</h3>
|
||||
<p class="text-gray-900 dark:text-white">{{ formData.admin.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div v-if="errorMessage" class="mt-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/50 rounded-xl">
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="mt-6 rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800/50 dark:bg-red-900/20"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-red-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||
<svg
|
||||
class="h-5 w-5 flex-shrink-0 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-sm text-red-700 dark:text-red-400">{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div v-if="installSuccess" class="mt-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800/50 rounded-xl">
|
||||
<div
|
||||
v-if="installSuccess"
|
||||
class="mt-6 rounded-xl border border-green-200 bg-green-50 p-4 dark:border-green-800/50 dark:bg-green-900/20"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<svg v-if="!serviceReady" class="animate-spin w-5 h-5 text-green-500 flex-shrink-0" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg
|
||||
v-if="!serviceReady"
|
||||
class="h-5 w-5 flex-shrink-0 animate-spin text-green-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg v-else class="w-5 h-5 text-green-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
v-else
|
||||
class="h-5 w-5 flex-shrink-0 text-green-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-green-700 dark:text-green-400">Installation completed!</p>
|
||||
<p class="text-sm text-green-600 dark:text-green-500 mt-1">
|
||||
{{ serviceReady ? 'Redirecting to login page...' : 'Service is restarting, please wait...' }}
|
||||
<p class="text-sm font-medium text-green-700 dark:text-green-400">
|
||||
Installation completed!
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-green-600 dark:text-green-500">
|
||||
{{
|
||||
serviceReady
|
||||
? 'Redirecting to login page...'
|
||||
: 'Service is restarting, please wait...'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,8 +455,18 @@
|
||||
@click="currentStep--"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
<svg
|
||||
class="mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 19.5L8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
Previous
|
||||
</button>
|
||||
@@ -251,7 +479,13 @@
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Next
|
||||
<svg class="w-4 h-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
class="ml-2 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -262,9 +496,25 @@
|
||||
:disabled="installing"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<svg v-if="installing" class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg
|
||||
v-if="installing"
|
||||
class="-ml-1 mr-2 h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ installing ? 'Installing...' : 'Complete Installation' }}
|
||||
</button>
|
||||
@@ -275,38 +525,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { testDatabase, testRedis, install, type InstallRequest } from '@/api/setup';
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { testDatabase, testRedis, install, type InstallRequest } from '@/api/setup'
|
||||
|
||||
const steps = [
|
||||
{ id: 'database', title: 'Database' },
|
||||
{ id: 'redis', title: 'Redis' },
|
||||
{ id: 'admin', title: 'Admin' },
|
||||
{ id: 'complete', title: 'Complete' },
|
||||
];
|
||||
{ id: 'complete', title: 'Complete' }
|
||||
]
|
||||
|
||||
const currentStep = ref(0);
|
||||
const errorMessage = ref('');
|
||||
const installSuccess = ref(false);
|
||||
const currentStep = ref(0)
|
||||
const errorMessage = ref('')
|
||||
const installSuccess = ref(false)
|
||||
|
||||
// Connection test states
|
||||
const testingDb = ref(false);
|
||||
const testingRedis = ref(false);
|
||||
const dbConnected = ref(false);
|
||||
const redisConnected = ref(false);
|
||||
const installing = ref(false);
|
||||
const confirmPassword = ref('');
|
||||
const serviceReady = ref(false);
|
||||
const testingDb = ref(false)
|
||||
const testingRedis = ref(false)
|
||||
const dbConnected = ref(false)
|
||||
const redisConnected = ref(false)
|
||||
const installing = ref(false)
|
||||
const confirmPassword = ref('')
|
||||
const serviceReady = ref(false)
|
||||
|
||||
// Get current server port from browser location (set by install.sh)
|
||||
const getCurrentPort = (): number => {
|
||||
const port = window.location.port;
|
||||
const port = window.location.port
|
||||
if (port) {
|
||||
return parseInt(port, 10);
|
||||
return parseInt(port, 10)
|
||||
}
|
||||
// Default port based on protocol
|
||||
return window.location.protocol === 'https:' ? 443 : 80;
|
||||
};
|
||||
return window.location.protocol === 'https:' ? 443 : 80
|
||||
}
|
||||
|
||||
const formData = reactive<InstallRequest>({
|
||||
database: {
|
||||
@@ -315,105 +565,105 @@ const formData = reactive<InstallRequest>({
|
||||
user: 'postgres',
|
||||
password: '',
|
||||
dbname: 'sub2api',
|
||||
sslmode: 'disable',
|
||||
sslmode: 'disable'
|
||||
},
|
||||
redis: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
password: '',
|
||||
db: 0,
|
||||
db: 0
|
||||
},
|
||||
admin: {
|
||||
email: '',
|
||||
password: '',
|
||||
password: ''
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: getCurrentPort(), // Use current port from browser
|
||||
mode: 'release',
|
||||
},
|
||||
});
|
||||
port: getCurrentPort(), // Use current port from browser
|
||||
mode: 'release'
|
||||
}
|
||||
})
|
||||
|
||||
const canProceed = computed(() => {
|
||||
switch (currentStep.value) {
|
||||
case 0:
|
||||
return dbConnected.value;
|
||||
return dbConnected.value
|
||||
case 1:
|
||||
return redisConnected.value;
|
||||
return redisConnected.value
|
||||
case 2:
|
||||
return (
|
||||
formData.admin.email &&
|
||||
formData.admin.password.length >= 6 &&
|
||||
formData.admin.password === confirmPassword.value
|
||||
);
|
||||
)
|
||||
default:
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
async function testDatabaseConnection() {
|
||||
testingDb.value = true;
|
||||
errorMessage.value = '';
|
||||
dbConnected.value = false;
|
||||
testingDb.value = true
|
||||
errorMessage.value = ''
|
||||
dbConnected.value = false
|
||||
|
||||
try {
|
||||
await testDatabase(formData.database);
|
||||
dbConnected.value = true;
|
||||
await testDatabase(formData.database)
|
||||
dbConnected.value = true
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { data?: { detail?: string } }; message?: string };
|
||||
errorMessage.value = err.response?.data?.detail || err.message || 'Connection failed';
|
||||
const err = error as { response?: { data?: { detail?: string } }; message?: string }
|
||||
errorMessage.value = err.response?.data?.detail || err.message || 'Connection failed'
|
||||
} finally {
|
||||
testingDb.value = false;
|
||||
testingDb.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function testRedisConnection() {
|
||||
testingRedis.value = true;
|
||||
errorMessage.value = '';
|
||||
redisConnected.value = false;
|
||||
testingRedis.value = true
|
||||
errorMessage.value = ''
|
||||
redisConnected.value = false
|
||||
|
||||
try {
|
||||
await testRedis(formData.redis);
|
||||
redisConnected.value = true;
|
||||
await testRedis(formData.redis)
|
||||
redisConnected.value = true
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { data?: { detail?: string } }; message?: string };
|
||||
errorMessage.value = err.response?.data?.detail || err.message || 'Connection failed';
|
||||
const err = error as { response?: { data?: { detail?: string } }; message?: string }
|
||||
errorMessage.value = err.response?.data?.detail || err.message || 'Connection failed'
|
||||
} finally {
|
||||
testingRedis.value = false;
|
||||
testingRedis.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function nextStep() {
|
||||
if (canProceed.value) {
|
||||
errorMessage.value = '';
|
||||
currentStep.value++;
|
||||
errorMessage.value = ''
|
||||
currentStep.value++
|
||||
}
|
||||
}
|
||||
|
||||
async function performInstall() {
|
||||
installing.value = true;
|
||||
errorMessage.value = '';
|
||||
installing.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await install(formData);
|
||||
installSuccess.value = true;
|
||||
await install(formData)
|
||||
installSuccess.value = true
|
||||
// Start polling for service restart
|
||||
waitForServiceRestart();
|
||||
waitForServiceRestart()
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { data?: { detail?: string } }; message?: string };
|
||||
errorMessage.value = err.response?.data?.detail || err.message || 'Installation failed';
|
||||
const err = error as { response?: { data?: { detail?: string } }; message?: string }
|
||||
errorMessage.value = err.response?.data?.detail || err.message || 'Installation failed'
|
||||
} finally {
|
||||
installing.value = false;
|
||||
installing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for service to restart and become available
|
||||
async function waitForServiceRestart() {
|
||||
const maxAttempts = 30; // 30 attempts, ~30 seconds max
|
||||
const interval = 1000; // 1 second between attempts
|
||||
const maxAttempts = 30 // 30 attempts, ~30 seconds max
|
||||
const interval = 1000 // 1 second between attempts
|
||||
|
||||
// Wait a moment for the service to start restarting
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
try {
|
||||
@@ -421,25 +671,25 @@ async function waitForServiceRestart() {
|
||||
const response = await fetch('/health', {
|
||||
method: 'GET',
|
||||
cache: 'no-store'
|
||||
});
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Service is up, check if setup is no longer needed
|
||||
const statusResponse = await fetch('/setup/status', {
|
||||
method: 'GET',
|
||||
cache: 'no-store'
|
||||
});
|
||||
})
|
||||
|
||||
if (statusResponse.ok) {
|
||||
const data = await statusResponse.json();
|
||||
const data = await statusResponse.json()
|
||||
// If needs_setup is false, service has restarted in normal mode
|
||||
if (data.data && !data.data.needs_setup) {
|
||||
serviceReady.value = true;
|
||||
serviceReady.value = true
|
||||
// Redirect to login page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 1500);
|
||||
return;
|
||||
window.location.href = '/login'
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,11 +697,12 @@ async function waitForServiceRestart() {
|
||||
// Service not ready yet, continue polling
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
|
||||
// If we reach here, service didn't restart in time
|
||||
// Show a message to refresh manually
|
||||
errorMessage.value = 'Service restart is taking longer than expected. Please refresh the page manually.';
|
||||
errorMessage.value =
|
||||
'Service restart is taking longer than expected. Please refresh the page manually.'
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user