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:
yinjianm
2026-03-25 04:50:08 +08:00
parent 10df92ffa3
commit 91aa6e83ca
20 changed files with 2727 additions and 1632 deletions
+82 -105
View File
@@ -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>