feat(frontend): unify ui with slate control center
add shared page and auth shells to standardize the main application layout and authentication entry experience refresh dashboard, settings, login, setup, notifications, proxies, and audit logs with a consistent Element Plus based control-center presentation modernize workspace side panels and status monitor while keeping the three-column layout, and fix the terminal hover cursor to show a text caret
This commit is contained in:
@@ -1,98 +1,14 @@
|
||||
<template>
|
||||
<!-- Page Container with Subtle Dot Background -->
|
||||
<div class="flex items-center justify-center min-h-screen bg-background p-4 bg-[radial-gradient(theme(colors.border)_1px,transparent_1px)] bg-[size:16px_16px]">
|
||||
<!-- Setup Card -->
|
||||
<div class="flex w-full max-w-4xl rounded-xl shadow-2xl overflow-hidden bg-background border border-border/20">
|
||||
<!-- Left Panel (Brand) - Hidden on small screens -->
|
||||
<div class="hidden md:flex w-2/5 bg-gradient-to-br from-primary to-primary-dark flex-col items-center justify-center p-10 text-white relative">
|
||||
<!-- Subtle pattern or overlay could go here -->
|
||||
<div class="z-10 text-center">
|
||||
<img src="../assets/logo.png" alt="Project Logo" class="h-20 w-auto mb-5 mx-auto">
|
||||
<h1 class="text-3xl font-bold mb-2">{{ $t('projectName') }}</h1>
|
||||
<p class="text-base opacity-80">{{ $t('setup.description') }}</p> <!-- Moved description here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel (Setup Form) -->
|
||||
<div class="w-full md:w-3/5 flex flex-col justify-center p-8 sm:p-12">
|
||||
<!-- Mobile Logo & Title (optional) -->
|
||||
<div class="flex flex-col items-center mb-6 md:hidden">
|
||||
<img src="../assets/logo.png" alt="Project Logo" class="h-16 w-auto mb-3">
|
||||
<h2 class="text-xl font-semibold text-foreground">{{ $t('setup.title') }}</h2>
|
||||
<p class="text-sm text-text-secondary mt-1">{{ $t('setup.description') }}</p>
|
||||
</div>
|
||||
<!-- Desktop Title (Subtle) -->
|
||||
<h2 class="text-2xl font-semibold mb-6 text-center text-foreground hidden md:block">{{ $t('setup.title') }}</h2>
|
||||
|
||||
<form @submit.prevent="handleSetup" class="space-y-5"> <!-- Reduced space slightly -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('setup.username') }}</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
:disabled="isLoading"
|
||||
:placeholder="$t('setup.usernamePlaceholder')"
|
||||
class="w-full px-4 py-3 border border-border/50 rounded-lg bg-input text-foreground text-base shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('setup.password') }}</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
:disabled="isLoading"
|
||||
:placeholder="$t('setup.passwordPlaceholder')"
|
||||
class="w-full px-4 py-3 border border-border/50 rounded-lg bg-input text-foreground text-base shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('setup.confirmPassword') }}</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
v-model="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
:disabled="isLoading"
|
||||
:placeholder="$t('setup.confirmPasswordPlaceholder')"
|
||||
class="w-full px-4 py-3 border border-border/50 rounded-lg bg-input text-foreground text-base shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="text-error bg-error/10 border border-error/20 px-4 py-2 rounded text-center text-sm"> <!-- Adjusted padding -->
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-if="successMessage" class="text-success bg-success/10 border border-success/20 px-4 py-2 rounded text-center text-sm"> <!-- Adjusted padding -->
|
||||
{{ successMessage }}
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="isLoading"
|
||||
class="w-full py-3 px-4 bg-primary text-white border-none rounded-lg text-base font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:bg-gray-400 disabled:opacity-70 disabled:cursor-not-allowed">
|
||||
<span v-if="isLoading">{{ $t('setup.settingUp') }}</span>
|
||||
<span v-else>{{ $t('setup.submitButton') }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAuthStore } from '../stores/auth.store'; // *** 导入 Auth Store ***
|
||||
import AuthPanelLayout from '../components/AuthPanelLayout.vue';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { useAuthStore } from '../stores/auth.store';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore(); // *** 获取 Auth Store 实例 ***
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
@@ -111,44 +27,105 @@ const handleSetup = async () => {
|
||||
}
|
||||
|
||||
if (!username.value || !password.value) {
|
||||
error.value = t('setup.error.fieldsRequired');
|
||||
return;
|
||||
error.value = t('setup.error.fieldsRequired');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// 确保调用正确的后端 API 端点
|
||||
await apiClient.post('/auth/setup', { // 使用 apiClient 并移除 base URL
|
||||
await apiClient.post('/auth/setup', {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
confirmPassword: confirmPassword.value
|
||||
confirmPassword: confirmPassword.value,
|
||||
});
|
||||
|
||||
successMessage.value = t('setup.success');
|
||||
// *** 手动更新 needsSetup 状态 ***
|
||||
authStore.needsSetup = false;
|
||||
// *** 重置认证状态,因为设置完成后需要重新登录 ***
|
||||
authStore.isAuthenticated = false;
|
||||
authStore.user = null;
|
||||
// 禁用表单或按钮,防止重复提交
|
||||
isLoading.value = true; // Keep loading state to disable button
|
||||
// Redirect to login immediately after showing success message (removed setTimeout)
|
||||
// The success message will be briefly visible before navigation.
|
||||
router.push('/login');
|
||||
} catch (err: any) {
|
||||
console.error('Setup failed:', err);
|
||||
if (err.response?.data?.message) {
|
||||
// 尝试从后端响应中获取更具体的错误信息
|
||||
error.value = err.response.data.message;
|
||||
} else if (err.message) {
|
||||
error.value = err.message;
|
||||
error.value = err.message;
|
||||
} else {
|
||||
error.value = t('setup.error.generic');
|
||||
error.value = t('setup.error.generic');
|
||||
}
|
||||
isLoading.value = false; // Re-enable button on error
|
||||
isLoading.value = false;
|
||||
}
|
||||
// Removed finally block setting isLoading to false on success to keep button disabled
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Copied styles from LoginView.vue -->
|
||||
<template>
|
||||
<AuthPanelLayout
|
||||
:title="t('setup.title')"
|
||||
:subtitle="t('setup.description')"
|
||||
accent-label="Slate Bootstrap"
|
||||
>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-5"
|
||||
:title="t('setup.bootstrapHint', '创建首个管理员账号后即可进入完整控制中心。')"
|
||||
/>
|
||||
|
||||
<el-form label-position="top" @submit.prevent="handleSetup">
|
||||
<div class="grid gap-5">
|
||||
<el-form-item :label="t('setup.username')">
|
||||
<el-input
|
||||
v-model="username"
|
||||
:disabled="isLoading"
|
||||
:placeholder="t('setup.usernamePlaceholder')"
|
||||
size="large"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="fas fa-user text-text-secondary"></i>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('setup.password')">
|
||||
<el-input
|
||||
v-model="password"
|
||||
:disabled="isLoading"
|
||||
:placeholder="t('setup.passwordPlaceholder')"
|
||||
type="password"
|
||||
show-password
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="fas fa-lock text-text-secondary"></i>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('setup.confirmPassword')">
|
||||
<el-input
|
||||
v-model="confirmPassword"
|
||||
:disabled="isLoading"
|
||||
:placeholder="t('setup.confirmPasswordPlaceholder')"
|
||||
type="password"
|
||||
show-password
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="fas fa-check text-text-secondary"></i>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon />
|
||||
<el-alert v-if="successMessage" :title="successMessage" type="success" :closable="false" show-icon />
|
||||
|
||||
<el-button native-type="submit" type="primary" size="large" class="w-full" :loading="isLoading">
|
||||
{{ t('setup.submitButton') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</AuthPanelLayout>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user