feat: UI美化
This commit is contained in:
@@ -10,7 +10,34 @@ const isLoggedIn = computed(() => !!authKey.value);
|
||||
|
||||
<template>
|
||||
<global-providers>
|
||||
<layout v-if="isLoggedIn" />
|
||||
<router-view v-else />
|
||||
<div id="app-root">
|
||||
<transition name="app-transition" mode="out-in">
|
||||
<layout v-if="isLoggedIn" key="layout" />
|
||||
<router-view v-else key="auth" />
|
||||
</transition>
|
||||
</div>
|
||||
</global-providers>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app-root {
|
||||
width: 100%;
|
||||
/* height: 100vh; */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-transition-enter-active,
|
||||
.app-transition-leave-active {
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.app-transition-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.app-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,28 +1,65 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-family:
|
||||
"Inter",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
"Helvetica Neue",
|
||||
Arial,
|
||||
sans-serif;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* 现代化颜色方案 */
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
--success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
--warning-gradient: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
--surface-gradient: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||
--dark-gradient: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
||||
|
||||
/* 阴影效果 */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
|
||||
/* 边框半径 */
|
||||
--border-radius-sm: 8px;
|
||||
--border-radius-md: 12px;
|
||||
--border-radius-lg: 16px;
|
||||
--border-radius-xl: 24px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
max-width: 1400px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 工具类 */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -78,3 +115,216 @@ body {
|
||||
.shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
/* 动画和过渡 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-enter-from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* 美化滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.modern-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modern-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
/* 按钮增强 */
|
||||
.modern-button {
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.modern-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* 输入框增强 */
|
||||
.modern-input {
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modern-input:focus {
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* 选择文本样式 */
|
||||
::selection {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* Focus样式增强 */
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 2px solid rgba(102, 126, 234, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 加载状态样式 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* 响应式网格 */
|
||||
.responsive-grid {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.responsive-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 文本渐变 */
|
||||
.text-gradient {
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 玻璃态效果 */
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
/* 悬停效果增强 */
|
||||
.hover-lift {
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
/* 脉冲动画 */
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 弹跳动画 */
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
20%,
|
||||
53%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
40%,
|
||||
43% {
|
||||
transform: translate3d(0, -30px, 0);
|
||||
}
|
||||
70% {
|
||||
transform: translate3d(0, -15px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3d(0, -4px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.bounce {
|
||||
animation: bounce 1s ease-in-out;
|
||||
}
|
||||
|
@@ -1,5 +1,232 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
// 模拟数据
|
||||
const stats = ref([
|
||||
{
|
||||
title: "总请求数",
|
||||
value: "125,842",
|
||||
icon: "📈",
|
||||
color: "var(--primary-gradient)",
|
||||
trend: "+12.5%",
|
||||
trendUp: true,
|
||||
},
|
||||
{
|
||||
title: "活跃连接",
|
||||
value: "1,234",
|
||||
icon: "🔗",
|
||||
color: "var(--success-gradient)",
|
||||
trend: "+5.2%",
|
||||
trendUp: true,
|
||||
},
|
||||
{
|
||||
title: "响应时间",
|
||||
value: "245ms",
|
||||
icon: "⚡",
|
||||
color: "var(--warning-gradient)",
|
||||
trend: "-8.1%",
|
||||
trendUp: false,
|
||||
},
|
||||
{
|
||||
title: "错误率",
|
||||
value: "0.12%",
|
||||
icon: "🛡️",
|
||||
color: "var(--secondary-gradient)",
|
||||
trend: "-2.3%",
|
||||
trendUp: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const animatedValues = ref<Record<string, number>>({});
|
||||
|
||||
onMounted(() => {
|
||||
// 动画效果
|
||||
stats.value.forEach((stat, index) => {
|
||||
setTimeout(() => {
|
||||
animatedValues.value[stat.title] = 1;
|
||||
}, index * 150);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card title="基础统计">
|
||||
<div>基础统计</div>
|
||||
</n-card>
|
||||
<div class="stats-container">
|
||||
<div class="stats-grid">
|
||||
<div
|
||||
v-for="(stat, index) in stats"
|
||||
:key="stat.title"
|
||||
class="stat-card modern-card"
|
||||
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||
>
|
||||
<div class="stat-header">
|
||||
<div class="stat-icon" :style="{ background: stat.color }">
|
||||
{{ stat.icon }}
|
||||
</div>
|
||||
<div
|
||||
class="stat-trend"
|
||||
:class="{ 'trend-up': stat.trendUp, 'trend-down': !stat.trendUp }"
|
||||
>
|
||||
{{ stat.trend }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-title">{{ stat.title }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-bar">
|
||||
<div
|
||||
class="stat-bar-fill"
|
||||
:style="{
|
||||
background: stat.color,
|
||||
width: `${animatedValues[stat.title] * 100}%`,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stats-container {
|
||||
width: 100%;
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 24px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: slideInUp 0.6s ease-out both;
|
||||
}
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trend-up {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.trend-down {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.trend-up::before {
|
||||
content: "↗";
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.trend-down::before {
|
||||
content: "↘";
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 0.95rem;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stat-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 1s ease-out;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,14 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { appState } from "@/utils/app-state";
|
||||
import {
|
||||
NConfigProvider,
|
||||
NDialogProvider,
|
||||
NLoadingBarProvider,
|
||||
NMessageProvider,
|
||||
useLoadingBar,
|
||||
useMessage,
|
||||
type GlobalThemeOverrides,
|
||||
} from "naive-ui";
|
||||
import { defineComponent, watch } from "vue";
|
||||
|
||||
// 自定义主题配置
|
||||
const themeOverrides: GlobalThemeOverrides = {
|
||||
common: {
|
||||
primaryColor: "#667eea",
|
||||
primaryColorHover: "#5a6fd8",
|
||||
primaryColorPressed: "#4c63d2",
|
||||
primaryColorSuppl: "#8b9df5",
|
||||
borderRadius: "12px",
|
||||
borderRadiusSmall: "8px",
|
||||
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
},
|
||||
Card: {
|
||||
paddingMedium: "24px",
|
||||
},
|
||||
Button: {
|
||||
fontWeight: "600",
|
||||
heightMedium: "40px",
|
||||
heightLarge: "48px",
|
||||
},
|
||||
Input: {
|
||||
heightMedium: "40px",
|
||||
heightLarge: "48px",
|
||||
},
|
||||
Menu: {
|
||||
itemHeight: "42px",
|
||||
},
|
||||
};
|
||||
|
||||
function useGlobalMessage() {
|
||||
window.$message = useMessage();
|
||||
}
|
||||
@@ -39,13 +69,15 @@ const Message = defineComponent({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-loading-bar-provider>
|
||||
<n-message-provider>
|
||||
<n-dialog-provider>
|
||||
<slot />
|
||||
<loading-bar />
|
||||
<message />
|
||||
</n-dialog-provider>
|
||||
</n-message-provider>
|
||||
</n-loading-bar-provider>
|
||||
<n-config-provider :theme-overrides="themeOverrides">
|
||||
<n-loading-bar-provider>
|
||||
<n-message-provider>
|
||||
<n-dialog-provider>
|
||||
<slot />
|
||||
<loading-bar />
|
||||
<message />
|
||||
</n-dialog-provider>
|
||||
</n-message-provider>
|
||||
</n-loading-bar-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
@@ -4,25 +4,176 @@ import NavBar from "@/components/NavBar.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-layout>
|
||||
<n-layout-header class="flex items-center">
|
||||
<h1 class="layout-header-title">T.COM</h1>
|
||||
<nav-bar />
|
||||
<logout />
|
||||
<n-layout class="main-layout">
|
||||
<n-layout-header class="layout-header">
|
||||
<div class="header-content">
|
||||
<div class="header-brand">
|
||||
<div class="brand-icon">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="brand-title">GPT Load</h1>
|
||||
</div>
|
||||
|
||||
<nav-bar class="header-nav" />
|
||||
|
||||
<div class="header-actions">
|
||||
<logout />
|
||||
</div>
|
||||
</div>
|
||||
</n-layout-header>
|
||||
<n-layout-content class="layout-content" content-style="padding: 24px;">
|
||||
<router-view />
|
||||
|
||||
<n-layout-content class="layout-content">
|
||||
<div class="content-wrapper">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout-header-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
margin-right: 20px;
|
||||
.main-layout {
|
||||
/* height: 100vh; */
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.brand-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
:deep(.n-layout-header) {
|
||||
height: 64px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.n-layout-content) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,265 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
// 模拟图表数据
|
||||
const chartData = ref({
|
||||
labels: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00", "24:00"],
|
||||
datasets: [
|
||||
{
|
||||
label: "请求数量",
|
||||
data: [120, 150, 300, 450, 380, 280, 200],
|
||||
color: "#667eea",
|
||||
},
|
||||
{
|
||||
label: "响应时间",
|
||||
data: [200, 180, 250, 300, 220, 190, 160],
|
||||
color: "#f093fb",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const chartContainer = ref<HTMLElement>();
|
||||
const animationProgress = ref(0);
|
||||
|
||||
// 生成SVG路径
|
||||
const generatePath = (data: number[]) => {
|
||||
const points = data.map((value, index) => {
|
||||
const x = (index / (data.length - 1)) * 380 + 10;
|
||||
const y = 200 - (value / 500) * 180 - 10;
|
||||
return `${x},${y}`;
|
||||
});
|
||||
return `M ${points.join(" L ")}`;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 简单的动画效果
|
||||
let start = 0;
|
||||
const animate = (timestamp: number) => {
|
||||
if (!start) {
|
||||
start = timestamp;
|
||||
}
|
||||
const progress = Math.min((timestamp - start) / 2000, 1);
|
||||
animationProgress.value = progress;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card title="拆线图">
|
||||
<div>拆线图</div>
|
||||
</n-card>
|
||||
<div class="chart-container">
|
||||
<n-card class="chart-card modern-card" :bordered="false">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">性能监控</h3>
|
||||
<p class="chart-subtitle">实时系统性能指标</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="chartContainer" class="chart-content">
|
||||
<div class="chart-legend">
|
||||
<div v-for="dataset in chartData.datasets" :key="dataset.label" class="legend-item">
|
||||
<div class="legend-color" :style="{ backgroundColor: dataset.color }" />
|
||||
<span class="legend-label">{{ dataset.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-area">
|
||||
<div class="chart-grid">
|
||||
<div
|
||||
v-for="(label, index) in chartData.labels"
|
||||
:key="label"
|
||||
class="grid-line"
|
||||
:style="{ left: `${(index / (chartData.labels.length - 1)) * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<svg class="chart-svg" viewBox="0 0 400 200">
|
||||
<!-- 数据线条 -->
|
||||
<g v-for="dataset in chartData.datasets" :key="dataset.label">
|
||||
<path
|
||||
:d="generatePath(dataset.data)"
|
||||
:stroke="dataset.color"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="chart-line"
|
||||
:style="{
|
||||
strokeDasharray: '1000',
|
||||
strokeDashoffset: `${1000 * (1 - animationProgress)}`,
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- 数据点 -->
|
||||
<g v-for="(value, index) in dataset.data" :key="index">
|
||||
<circle
|
||||
:cx="(index / (dataset.data.length - 1)) * 380 + 10"
|
||||
:cy="200 - (value / 500) * 180 - 10"
|
||||
:r="animationProgress > index / dataset.data.length ? 4 : 0"
|
||||
:fill="dataset.color"
|
||||
class="chart-point"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div class="chart-labels">
|
||||
<div
|
||||
v-for="(label, index) in chartData.labels"
|
||||
:key="label"
|
||||
class="chart-label"
|
||||
:style="{ left: `${(index / (chartData.labels.length - 1)) * 100}%` }"
|
||||
>
|
||||
{{ label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.chart-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 0.9rem;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
position: relative;
|
||||
height: 240px;
|
||||
background: linear-gradient(180deg, rgba(102, 126, 234, 0.02) 0%, rgba(102, 126, 234, 0.08) 100%);
|
||||
border-radius: var(--border-radius-md);
|
||||
border: 1px solid rgba(102, 126, 234, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-grid {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.grid-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.chart-svg {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chart-line {
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||
transition: stroke-dashoffset 2s ease-out;
|
||||
}
|
||||
|
||||
.chart-point {
|
||||
transition: r 0.3s ease;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
.chart-point:hover {
|
||||
r: 6;
|
||||
}
|
||||
|
||||
.chart-labels {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
position: absolute;
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chart-legend {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.chart-svg {
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -12,5 +12,38 @@ const handleLogout = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-button quaternary round @click="handleLogout">退出</n-button>
|
||||
<n-button quaternary round class="logout-button" @click="handleLogout">
|
||||
<template #icon>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.59L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
退出登录
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logout-button {
|
||||
color: #64748b;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
color: #dc2626;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
:deep(.n-button__content) {
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -4,16 +4,16 @@ import { computed, h } from "vue";
|
||||
import { RouterLink, useRoute } from "vue-router";
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
renderMenuItem("dashboard", "仪表盘"),
|
||||
renderMenuItem("keys", "密钥管理"),
|
||||
renderMenuItem("logs", "日志"),
|
||||
renderMenuItem("settings", "系统设置"),
|
||||
renderMenuItem("dashboard", "仪表盘", "📊"),
|
||||
renderMenuItem("keys", "密钥管理", "🔑"),
|
||||
renderMenuItem("logs", "日志", "📋"),
|
||||
renderMenuItem("settings", "系统设置", "⚙️"),
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
const activeMenu = computed(() => route.name);
|
||||
|
||||
function renderMenuItem(key: string, label: string): MenuOption {
|
||||
function renderMenuItem(key: string, label: string, icon: string): MenuOption {
|
||||
return {
|
||||
label: () =>
|
||||
h(
|
||||
@@ -22,8 +22,14 @@ function renderMenuItem(key: string, label: string): MenuOption {
|
||||
to: {
|
||||
name: key,
|
||||
},
|
||||
class: "nav-menu-item",
|
||||
},
|
||||
{ default: () => label }
|
||||
{
|
||||
default: () => [
|
||||
h("span", { class: "nav-item-icon" }, icon),
|
||||
h("span", { class: "nav-item-text" }, label),
|
||||
],
|
||||
}
|
||||
),
|
||||
key,
|
||||
};
|
||||
@@ -31,5 +37,86 @@ function renderMenuItem(key: string, label: string): MenuOption {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-menu mode="horizontal" :options="menuOptions" :value="activeMenu" responsive />
|
||||
<div class="navbar-container">
|
||||
<n-menu mode="horizontal" :options="menuOptions" :value="activeMenu" class="modern-menu" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.navbar-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.nav-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-menu-item:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nav-item-icon {
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-item-text {
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
:deep(.n-menu) {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.n-menu-item) {
|
||||
border-radius: var(--border-radius-md);
|
||||
margin: 0 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.n-menu-item:hover) {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
:deep(.n-menu-item--selected) {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
:deep(.n-menu-item--selected:hover) {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
:deep(.n-menu-item-content) {
|
||||
border-radius: var(--border-radius-md);
|
||||
}
|
||||
|
||||
:deep(.n-menu-item-content-header) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.navbar-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -4,12 +4,81 @@ import LineChart from "@/components/LineChart.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<base-info-card />
|
||||
<line-chart class="chart" />
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-header">
|
||||
<h2 class="dashboard-title">仪表盘</h2>
|
||||
<p class="dashboard-subtitle">系统概览与实时监控</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-content">
|
||||
<base-info-card />
|
||||
<line-chart class="dashboard-chart" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chart {
|
||||
margin-top: 20px;
|
||||
.dashboard-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 700;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.dashboard-chart {
|
||||
animation: fadeInUp 0.6s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,3 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
// 这里可以添加密钥管理相关的逻辑
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>keys</div>
|
||||
<div class="keys-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">密钥管理</h2>
|
||||
<p class="page-subtitle">管理API密钥和访问凭证</p>
|
||||
</div>
|
||||
|
||||
<div class="content-placeholder">
|
||||
<div class="placeholder-card modern-card">
|
||||
<div class="placeholder-icon">🔑</div>
|
||||
<h3 class="placeholder-title">密钥管理功能</h3>
|
||||
<p class="placeholder-description">
|
||||
此功能正在开发中,将提供完整的API密钥管理功能,包括添加、删除、编辑和监控密钥使用情况。
|
||||
</p>
|
||||
<div class="placeholder-features">
|
||||
<div class="feature-item">✨ 密钥添加与删除</div>
|
||||
<div class="feature-item">📊 使用情况统计</div>
|
||||
<div class="feature-item">🔒 安全性验证</div>
|
||||
<div class="feature-item">🔄 自动轮换</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.keys-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 700;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
padding: 48px 32px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.placeholder-description {
|
||||
font-size: 1rem;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.placeholder-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
padding: 12px 16px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.placeholder-card {
|
||||
margin: 0 16px;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.placeholder-features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -12,7 +12,7 @@ const { login } = useAuthService();
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!authKey.value) {
|
||||
message.error("Please enter Auth Key");
|
||||
message.error("请输入授权密钥");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
@@ -21,42 +21,224 @@ const handleLogin = async () => {
|
||||
if (success) {
|
||||
router.push("/");
|
||||
} else {
|
||||
message.error("Login failed, please check your Auth Key");
|
||||
message.error("登录失败,请检查您的授权密钥");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<n-card class="login-card" title="登录">
|
||||
<n-space vertical>
|
||||
<n-input
|
||||
v-model:value="authKey"
|
||||
type="password"
|
||||
placeholder="Auth Key"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
<n-button class="login-btn" type="primary" block @click="handleLogin" :loading="loading">
|
||||
Login
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<div class="login-background">
|
||||
<div class="login-decoration" />
|
||||
<div class="login-decoration-2" />
|
||||
</div>
|
||||
|
||||
<div class="login-content">
|
||||
<div class="login-header">
|
||||
<h1 class="login-title">GPT Load</h1>
|
||||
<p class="login-subtitle">智能负载均衡管理平台</p>
|
||||
</div>
|
||||
|
||||
<n-card class="login-card modern-card" :bordered="false">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">欢迎回来</h2>
|
||||
<p class="card-subtitle">请输入您的授权密钥以继续</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-space vertical size="large">
|
||||
<n-input
|
||||
v-model:value="authKey"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="请输入授权密钥"
|
||||
class="modern-input"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M6 10v-4a6 6 0 1 1 12 0v4h1a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h1zm6-6a4 4 0 0 0-4 4v4h8v-4a4 4 0 0 0-4-4z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
</n-input>
|
||||
|
||||
<n-button
|
||||
class="login-btn modern-button"
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
@click="handleLogin"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
>
|
||||
<template v-if="!loading">
|
||||
<span>立即登录</span>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<div class="login-footer">
|
||||
<p class="footer-text">© 2024 GPT Load. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.login-decoration {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -20%;
|
||||
width: 800px;
|
||||
height: 800px;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 50%;
|
||||
opacity: 0.1;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.login-decoration-2 {
|
||||
position: absolute;
|
||||
bottom: -50%;
|
||||
left: -20%;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: var(--secondary-gradient);
|
||||
border-radius: 50%;
|
||||
opacity: 0.08;
|
||||
animation: float 8s ease-in-out infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
.login-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
max-width: 400px;
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 0.95rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 10px;
|
||||
background: var(--primary-gradient);
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
height: 48px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
--n-border-radius: 12px;
|
||||
--n-height: 48px;
|
||||
}
|
||||
|
||||
:deep(.n-input__input-el) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
:deep(.n-input__prefix) {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
:deep(.n-card-header) {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.n-card__content) {
|
||||
padding-top: 0;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,3 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
// 这里可以添加日志管理相关的逻辑
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>logs</div>
|
||||
<div class="logs-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">系统日志</h2>
|
||||
<p class="page-subtitle">查看系统运行日志和操作记录</p>
|
||||
</div>
|
||||
|
||||
<div class="content-placeholder">
|
||||
<div class="placeholder-card modern-card">
|
||||
<div class="placeholder-icon">📋</div>
|
||||
<h3 class="placeholder-title">日志管理功能</h3>
|
||||
<p class="placeholder-description">
|
||||
此功能正在开发中,将提供完整的系统日志查看和管理功能,包括实时日志、历史记录和日志分析。
|
||||
</p>
|
||||
<div class="placeholder-features">
|
||||
<div class="feature-item">📝 实时日志流</div>
|
||||
<div class="feature-item">🔍 日志搜索过滤</div>
|
||||
<div class="feature-item">📈 错误统计分析</div>
|
||||
<div class="feature-item">💾 日志导出功能</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logs-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 700;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
padding: 48px 32px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.placeholder-description {
|
||||
font-size: 1rem;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.placeholder-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
padding: 12px 16px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.placeholder-card {
|
||||
margin: 0 16px;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.placeholder-features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -42,63 +42,286 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-form ref="formRef" :model="form" label-placement="left" label-width="110">
|
||||
<n-card
|
||||
v-for="(category, cIndex) in settingList"
|
||||
:key="cIndex"
|
||||
:bordered="false"
|
||||
:title="category.category_name"
|
||||
>
|
||||
<n-space>
|
||||
<n-form-item
|
||||
v-for="item in category.settings"
|
||||
:key="item.key"
|
||||
:path="item.key"
|
||||
style="margin-right: 10px"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: `请输入${item.name}`,
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<span>{{ item.name }}</span>
|
||||
</template>
|
||||
<span>{{ item.description }}</span>
|
||||
</n-tooltip>
|
||||
<div class="settings-container">
|
||||
<div class="settings-header">
|
||||
<h2 class="settings-title">系统设置</h2>
|
||||
<p class="settings-subtitle">配置系统参数和选项</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<n-form ref="formRef" :model="form" label-placement="top" class="settings-form">
|
||||
<div v-for="(category, cIndex) in settingList" :key="cIndex" class="settings-category">
|
||||
<n-card class="category-card modern-card" :bordered="false" size="small">
|
||||
<template #header>
|
||||
<div class="category-header">
|
||||
<h3 class="category-title">{{ category.category_name }}</h3>
|
||||
<div class="category-divider" />
|
||||
</div>
|
||||
</template>
|
||||
<n-input-number
|
||||
v-if="item.type === 'int'"
|
||||
v-model:value="form[item.key]"
|
||||
:min="item.min_value! >= 0 ? item.min_value : undefined"
|
||||
style="width: 120px"
|
||||
placeholder=""
|
||||
clearable
|
||||
/>
|
||||
<n-input
|
||||
v-else
|
||||
v-model:value="form[item.key]"
|
||||
style="width: 120px"
|
||||
placeholder=""
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-form>
|
||||
|
||||
<div class="settings-grid">
|
||||
<n-form-item
|
||||
v-for="item in category.settings"
|
||||
:key="item.key"
|
||||
:path="item.key"
|
||||
class="setting-item"
|
||||
:rule="{
|
||||
required: true,
|
||||
message: `请输入${item.name}`,
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="setting-label">
|
||||
<span class="label-text">{{ item.name }}</span>
|
||||
<n-tooltip trigger="hover" placement="top">
|
||||
<template #trigger>
|
||||
<div class="label-help">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<div class="tooltip-content">{{ item.description }}</div>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-input-number
|
||||
v-if="item.type === 'int'"
|
||||
v-model:value="form[item.key]"
|
||||
:min="item.min_value! >= 0 ? item.min_value : undefined"
|
||||
class="modern-input setting-input"
|
||||
placeholder="请输入数值"
|
||||
clearable
|
||||
/>
|
||||
<n-input
|
||||
v-else
|
||||
v-model:value="form[item.key]"
|
||||
class="modern-input setting-input"
|
||||
placeholder="请输入内容"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-form>
|
||||
|
||||
<div class="settings-actions">
|
||||
<n-button
|
||||
v-show="settingList.length > 0"
|
||||
type="primary"
|
||||
size="large"
|
||||
class="save-button modern-button"
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<template #icon>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M17 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ isSaving ? "保存中..." : "保存设置" }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-flex justify="center">
|
||||
<n-button
|
||||
v-show="settingList.length > 0"
|
||||
type="primary"
|
||||
style="width: 200px"
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存设置
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 700;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.settings-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.settings-category {
|
||||
animation: fadeInUp 0.6s ease-out both;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.settings-category:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.settings-category:nth-child(3) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.settings-category:nth-child(4) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.category-divider {
|
||||
height: 3px;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 2px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 12px 10px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.label-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
cursor: help;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.label-help:hover {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
max-width: 250px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.save-button {
|
||||
min-width: 200px;
|
||||
background: var(--primary-gradient);
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
height: 48px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.save-button:hover {
|
||||
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-form-item-label) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
--n-border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.n-input-number) {
|
||||
--n-border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.n-card-header) {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user