45
web/package-lock.json
generated
45
web/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vicons/ionicons5": "^0.13.0",
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
|
"@vueuse/core": "^13.6.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"naive-ui": "^2.41.0",
|
"naive-ui": "^2.41.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
@@ -1122,6 +1123,12 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/web-bluetooth": {
|
||||||
|
"version": "0.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
|
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.35.1",
|
"version": "8.35.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz",
|
||||||
@@ -1590,6 +1597,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueuse/core": {
|
||||||
|
"version": "13.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.6.0.tgz",
|
||||||
|
"integrity": "sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
|
"@vueuse/metadata": "13.6.0",
|
||||||
|
"@vueuse/shared": "13.6.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/metadata": {
|
||||||
|
"version": "13.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.6.0.tgz",
|
||||||
|
"integrity": "sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/shared": {
|
||||||
|
"version": "13.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.6.0.tgz",
|
||||||
|
"integrity": "sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
|
@@ -4,7 +4,14 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "GPT Load Balancer Frontend - A modern Vue 3 frontend for GPT load balancing service",
|
"description": "GPT Load Balancer Frontend - A modern Vue 3 frontend for GPT load balancing service",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": ["vue3", "typescript", "vite", "naive-ui", "gpt-load", "frontend"],
|
"keywords": [
|
||||||
|
"vue3",
|
||||||
|
"typescript",
|
||||||
|
"vite",
|
||||||
|
"naive-ui",
|
||||||
|
"gpt-load",
|
||||||
|
"frontend"
|
||||||
|
],
|
||||||
"author": "tbphp",
|
"author": "tbphp",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -32,6 +39,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vicons/ionicons5": "^0.13.0",
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
|
"@vueuse/core": "^13.6.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"naive-ui": "^2.41.0",
|
"naive-ui": "^2.41.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
@@ -218,7 +218,7 @@ onMounted(() => {
|
|||||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
height: 52px;
|
min-height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-container {
|
.footer-container {
|
||||||
@@ -231,7 +231,6 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
flex-wrap: wrap;
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +268,7 @@ onMounted(() => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-clickable {
|
.version-clickable {
|
||||||
@@ -301,6 +301,7 @@ onMounted(() => {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-link:hover {
|
.footer-link:hover {
|
||||||
@@ -345,6 +346,7 @@ onMounted(() => {
|
|||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.app-footer {
|
.app-footer {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-main {
|
.footer-main {
|
||||||
@@ -353,7 +355,7 @@ onMounted(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.footer-main :deep(.n-divider) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -59,7 +59,7 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="stats-container">
|
<div class="stats-container">
|
||||||
<n-space vertical size="medium">
|
<n-space vertical size="medium">
|
||||||
<n-grid :cols="4" :x-gap="20" :y-gap="20" responsive="screen">
|
<n-grid cols="2 s:4" :x-gap="20" :y-gap="20" responsive="screen">
|
||||||
<!-- 密钥数量 -->
|
<!-- 密钥数量 -->
|
||||||
<n-grid-item span="1">
|
<n-grid-item span="1">
|
||||||
<n-card :bordered="false" class="stat-card" style="animation-delay: 0s">
|
<n-card :bordered="false" class="stat-card" style="animation-delay: 0s">
|
||||||
@@ -217,13 +217,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
width: 48px;
|
width: 40px;
|
||||||
height: 48px;
|
height: 40px;
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 1.5rem;
|
font-size: 1.4rem;
|
||||||
color: white;
|
color: white;
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
@@ -262,7 +262,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 2.5rem;
|
font-size: 2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
color: #1e293b;
|
color: #1e293b;
|
||||||
|
@@ -170,7 +170,8 @@ function getTaskTitle(): string {
|
|||||||
bottom: 62px;
|
bottom: 62px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
width: 350px;
|
width: 95%;
|
||||||
|
max-width: 350px;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
@@ -178,6 +179,14 @@ function getTaskTitle(): string {
|
|||||||
animation: slideIn 0.3s ease-out;
|
animation: slideIn 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.global-task-progress {
|
||||||
|
bottom: 72px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
|
@@ -3,6 +3,21 @@ import AppFooter from "@/components/AppFooter.vue";
|
|||||||
import GlobalTaskProgressBar from "@/components/GlobalTaskProgressBar.vue";
|
import GlobalTaskProgressBar from "@/components/GlobalTaskProgressBar.vue";
|
||||||
import Logout from "@/components/Logout.vue";
|
import Logout from "@/components/Logout.vue";
|
||||||
import NavBar from "@/components/NavBar.vue";
|
import NavBar from "@/components/NavBar.vue";
|
||||||
|
import { useMediaQuery } from "@vueuse/core";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
const isMenuOpen = ref(false);
|
||||||
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
|
|
||||||
|
watch(isMobile, value => {
|
||||||
|
if (!value) {
|
||||||
|
isMenuOpen.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
isMenuOpen.value = !isMenuOpen.value;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -13,17 +28,33 @@ import NavBar from "@/components/NavBar.vue";
|
|||||||
<div class="brand-icon">
|
<div class="brand-icon">
|
||||||
<img src="@/assets/logo.png" alt="" />
|
<img src="@/assets/logo.png" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="brand-title">GPT Load</h1>
|
<h1 v-if="!isMobile" class="brand-title">GPT Load</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav-bar class="header-nav" />
|
<nav v-if="!isMobile" class="header-nav">
|
||||||
|
<nav-bar />
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<logout />
|
<logout v-if="!isMobile" />
|
||||||
|
<n-button v-else text @click="toggleMenu">
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path fill="currentColor" d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
|
||||||
|
</svg>
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-layout-header>
|
</n-layout-header>
|
||||||
|
|
||||||
|
<n-drawer v-model:show="isMenuOpen" :width="240" placement="right">
|
||||||
|
<n-drawer-content title="GPT Load" body-content-style="padding: 0;">
|
||||||
|
<nav-bar mode="vertical" @close="isMenuOpen = false" />
|
||||||
|
<div class="mobile-actions">
|
||||||
|
<logout />
|
||||||
|
</div>
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
|
|
||||||
<n-layout-content class="layout-content">
|
<n-layout-content class="layout-content">
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
@@ -56,14 +87,14 @@ import NavBar from "@/components/NavBar.vue";
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
padding: 0 24px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px;
|
padding: 8px 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -101,6 +132,13 @@ import NavBar from "@/components/NavBar.vue";
|
|||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-actions {
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-content {
|
.layout-content {
|
||||||
@@ -113,7 +151,7 @@ import NavBar from "@/components/NavBar.vue";
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
padding: 24px 12px;
|
padding: 16px;
|
||||||
min-height: calc(100vh - 111px);
|
min-height: calc(100vh - 111px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -792,9 +792,34 @@ onMounted(() => {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-legend {
|
.chart-legend {
|
||||||
|
position: relative;
|
||||||
|
transform: none;
|
||||||
|
left: auto;
|
||||||
|
top: auto;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: transparent;
|
||||||
|
backdrop-filter: none;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-svg {
|
.chart-svg {
|
||||||
|
@@ -1,18 +1,37 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MenuOption } from "naive-ui";
|
import { type MenuOption } from "naive-ui";
|
||||||
import { computed, h } from "vue";
|
import { computed, h, watch } from "vue";
|
||||||
import { RouterLink, useRoute } from "vue-router";
|
import { RouterLink, useRoute } from "vue-router";
|
||||||
|
|
||||||
const menuOptions: MenuOption[] = [
|
const props = defineProps({
|
||||||
renderMenuItem("dashboard", "仪表盘", "📊"),
|
mode: {
|
||||||
renderMenuItem("keys", "密钥管理", "🔑"),
|
type: String,
|
||||||
renderMenuItem("logs", "日志", "📋"),
|
default: "horizontal",
|
||||||
renderMenuItem("settings", "系统设置", "⚙️"),
|
},
|
||||||
];
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["close"]);
|
||||||
|
|
||||||
|
const menuOptions = computed<MenuOption[]>(() => {
|
||||||
|
const options: MenuOption[] = [
|
||||||
|
renderMenuItem("dashboard", "仪表盘", "📊"),
|
||||||
|
renderMenuItem("keys", "密钥管理", "🔑"),
|
||||||
|
renderMenuItem("logs", "日志", "📋"),
|
||||||
|
renderMenuItem("settings", "系统设置", "⚙️"),
|
||||||
|
];
|
||||||
|
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const activeMenu = computed(() => route.name);
|
const activeMenu = computed(() => route.name);
|
||||||
|
|
||||||
|
watch(activeMenu, () => {
|
||||||
|
if (props.mode === "vertical") {
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function renderMenuItem(key: string, label: string, icon: string): MenuOption {
|
function renderMenuItem(key: string, label: string, icon: string): MenuOption {
|
||||||
return {
|
return {
|
||||||
label: () =>
|
label: () =>
|
||||||
@@ -39,10 +58,9 @@ function renderMenuItem(key: string, label: string, icon: string): MenuOption {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-menu
|
<n-menu
|
||||||
mode="horizontal"
|
:mode="mode"
|
||||||
:options="menuOptions"
|
:options="menuOptions"
|
||||||
:value="activeMenu"
|
:value="activeMenu"
|
||||||
responsive
|
|
||||||
class="modern-menu"
|
class="modern-menu"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,24 +79,22 @@ function renderMenuItem(key: string, label: string, icon: string): MenuOption {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-menu-item-content) {
|
|
||||||
padding: 0 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.nav-item-text) {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
letter-spacing: 0.2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.n-menu-item) {
|
:deep(.n-menu-item) {
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
margin: 0 4px;
|
}
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
:deep(.n-menu--vertical .n-menu-item-content) {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-menu--vertical .n-menu-item) {
|
||||||
|
margin: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-menu-item:hover) {
|
:deep(.n-menu-item:hover) {
|
||||||
background: rgba(102, 126, 234, 0.1);
|
background: rgba(102, 126, 234, 0.1);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-menu-item--selected) {
|
:deep(.n-menu-item--selected) {
|
||||||
@@ -86,6 +102,7 @@ function renderMenuItem(key: string, label: string, icon: string): MenuOption {
|
|||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-menu-item--selected:hover) {
|
:deep(.n-menu-item--selected:hover) {
|
||||||
|
@@ -366,6 +366,10 @@ function handleConfigKeyChange(index: number, key: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getConfigOption = (key: string) => {
|
||||||
|
return configOptions.value.find(opt => opt.key === key);
|
||||||
|
};
|
||||||
|
|
||||||
// 关闭弹窗
|
// 关闭弹窗
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
emit("update:show", false);
|
emit("update:show", false);
|
||||||
@@ -440,7 +444,7 @@ async function handleSubmit() {
|
|||||||
<template>
|
<template>
|
||||||
<n-modal :show="show" @update:show="handleClose" class="group-form-modal">
|
<n-modal :show="show" @update:show="handleClose" class="group-form-modal">
|
||||||
<n-card
|
<n-card
|
||||||
style="width: 800px"
|
class="group-form-card"
|
||||||
:title="group ? '编辑分组' : '创建分组'"
|
:title="group ? '编辑分组' : '创建分组'"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="huge"
|
size="huge"
|
||||||
@@ -462,6 +466,7 @@ async function handleSubmit() {
|
|||||||
label-placement="left"
|
label-placement="left"
|
||||||
label-width="120px"
|
label-width="120px"
|
||||||
require-mark-placement="right-hanging"
|
require-mark-placement="right-hanging"
|
||||||
|
class="group-form"
|
||||||
>
|
>
|
||||||
<!-- 基础信息 -->
|
<!-- 基础信息 -->
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
@@ -677,9 +682,14 @@ async function handleSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="upstream-weight">
|
<div class="upstream-weight">
|
||||||
<span class="weight-label">权重</span>
|
<span class="weight-label">权重</span>
|
||||||
<n-tooltip trigger="hover" placement="top">
|
<n-tooltip trigger="hover" placement="top" style="width: 100%">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-input-number v-model:value="upstream.weight" :min="1" placeholder="权重" />
|
<n-input-number
|
||||||
|
v-model:value="upstream.weight"
|
||||||
|
:min="1"
|
||||||
|
placeholder="权重"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
负载均衡权重,数值越大被选中的概率越高。例如:权重为2的上游被选中的概率是权重为1的两倍
|
负载均衡权重,数值越大被选中的概率越高。例如:权重为2的上游被选中的概率是权重为1的两倍
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
@@ -771,11 +781,16 @@ async function handleSubmit() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-value">
|
<div class="config-value">
|
||||||
<n-input-number
|
<n-tooltip trigger="hover" placement="top">
|
||||||
v-model:value="configItem.value"
|
<template #trigger>
|
||||||
placeholder="参数值"
|
<n-input-number
|
||||||
:precision="0"
|
v-model:value="configItem.value"
|
||||||
/>
|
placeholder="参数值"
|
||||||
|
:precision="0"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
{{ getConfigOption(configItem.key)?.description || "设置此配置项的值" }}
|
||||||
|
</n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-actions">
|
<div class="config-actions">
|
||||||
<n-button
|
<n-button
|
||||||
@@ -1106,4 +1121,49 @@ async function handleSubmit() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.group-form-card {
|
||||||
|
width: 100vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-form {
|
||||||
|
label-width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item-half {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upstream-row,
|
||||||
|
.config-item-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upstream-weight {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upstream-actions,
|
||||||
|
.config-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -275,7 +275,7 @@ function resetPage() {
|
|||||||
<!-- 统计摘要区 -->
|
<!-- 统计摘要区 -->
|
||||||
<div class="stats-summary">
|
<div class="stats-summary">
|
||||||
<n-spin :show="loading" size="small">
|
<n-spin :show="loading" size="small">
|
||||||
<n-grid :cols="4" :x-gap="12" :y-gap="12" responsive="screen">
|
<n-grid cols="2 s:4" :x-gap="12" :y-gap="12" responsive="screen">
|
||||||
<n-grid-item span="1">
|
<n-grid-item span="1">
|
||||||
<n-statistic :label="`密钥数量:${stats?.key_stats?.total_keys ?? 0}`">
|
<n-statistic :label="`密钥数量:${stats?.key_stats?.total_keys ?? 0}`">
|
||||||
<n-tooltip trigger="hover">
|
<n-tooltip trigger="hover">
|
||||||
@@ -379,7 +379,7 @@ function resetPage() {
|
|||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4 class="section-title">基础信息</h4>
|
<h4 class="section-title">基础信息</h4>
|
||||||
<n-form label-placement="left" label-width="85px" label-align="right">
|
<n-form label-placement="left" label-width="85px" label-align="right">
|
||||||
<n-grid :cols="2">
|
<n-grid cols="1 m:2">
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-form-item label="分组名称:">
|
<n-form-item label="分组名称:">
|
||||||
{{ group?.name }}
|
{{ group?.name }}
|
||||||
|
@@ -815,8 +815,8 @@ function resetPage() {
|
|||||||
|
|
||||||
.keys-grid {
|
.keys-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-card {
|
.key-card {
|
||||||
@@ -1026,4 +1026,23 @@ function resetPage() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-left,
|
||||||
|
.toolbar-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right .n-space {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -235,80 +235,70 @@ function changePageSize(size: number) {
|
|||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<!-- 第一行:基础筛选 -->
|
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<div class="filter-group">
|
<div class="filter-grid">
|
||||||
<n-date-picker
|
<div class="filter-item">
|
||||||
v-model:value="filters.start_time"
|
<n-input
|
||||||
type="datetime"
|
v-model:value="filters.group_name"
|
||||||
clearable
|
placeholder="分组名"
|
||||||
size="small"
|
size="small"
|
||||||
placeholder="开始时间"
|
clearable
|
||||||
style="width: 180px"
|
@keyup.enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-item">
|
||||||
<n-date-picker
|
<n-input
|
||||||
v-model:value="filters.end_time"
|
v-model:value="filters.key_value"
|
||||||
type="datetime"
|
placeholder="密钥"
|
||||||
clearable
|
size="small"
|
||||||
size="small"
|
clearable
|
||||||
placeholder="结束时间"
|
@keyup.enter="handleSearch"
|
||||||
style="width: 180px"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div class="filter-item">
|
||||||
<div class="filter-group">
|
<n-input
|
||||||
<n-select
|
v-model:value="filters.status_code"
|
||||||
v-model:value="filters.is_success"
|
placeholder="状态码"
|
||||||
:options="successOptions"
|
size="small"
|
||||||
size="small"
|
clearable
|
||||||
style="width: 166px"
|
@keyup.enter="handleSearch"
|
||||||
@update:value="handleSearch"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div class="filter-item">
|
||||||
<div class="filter-group">
|
<n-select
|
||||||
<n-input
|
v-model:value="filters.is_success"
|
||||||
v-model:value="filters.status_code"
|
:options="successOptions"
|
||||||
placeholder="状态码"
|
size="small"
|
||||||
size="small"
|
@update:value="handleSearch"
|
||||||
clearable
|
/>
|
||||||
style="width: 166px"
|
</div>
|
||||||
@keyup.enter="handleSearch"
|
<div class="filter-item">
|
||||||
/>
|
<n-input
|
||||||
</div>
|
v-model:value="filters.error_contains"
|
||||||
<div class="filter-group">
|
placeholder="错误信息"
|
||||||
<n-input
|
size="small"
|
||||||
v-model:value="filters.group_name"
|
clearable
|
||||||
placeholder="分组名"
|
@keyup.enter="handleSearch"
|
||||||
size="small"
|
/>
|
||||||
clearable
|
</div>
|
||||||
style="width: 166px"
|
<div class="filter-item">
|
||||||
@keyup.enter="handleSearch"
|
<n-date-picker
|
||||||
/>
|
v-model:value="filters.start_time"
|
||||||
</div>
|
type="datetime"
|
||||||
<div class="filter-group">
|
clearable
|
||||||
<n-input
|
size="small"
|
||||||
v-model:value="filters.key_value"
|
placeholder="开始时间"
|
||||||
placeholder="密钥"
|
/>
|
||||||
size="small"
|
</div>
|
||||||
clearable
|
<div class="filter-item">
|
||||||
style="width: 166px"
|
<n-date-picker
|
||||||
@keyup.enter="handleSearch"
|
v-model:value="filters.end_time"
|
||||||
/>
|
type="datetime"
|
||||||
</div>
|
clearable
|
||||||
</div>
|
size="small"
|
||||||
|
placeholder="结束时间"
|
||||||
<!-- 第二行:详细筛选和操作 -->
|
/>
|
||||||
<div class="filter-row">
|
</div>
|
||||||
<div class="filter-group">
|
|
||||||
<n-input
|
|
||||||
v-model:value="filters.error_contains"
|
|
||||||
placeholder="错误信息"
|
|
||||||
size="small"
|
|
||||||
clearable
|
|
||||||
style="width: 384px"
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<n-button ghost size="small" :disabled="loading" @click="handleSearch">
|
<n-button ghost size="small" :disabled="loading" @click="handleSearch">
|
||||||
@@ -400,50 +390,47 @@ function changePageSize(size: number) {
|
|||||||
|
|
||||||
.filter-row {
|
.filter-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end; /* Aligns buttons with the bottom of the filter items */
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group {
|
.filter-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
|
flex: 1 1 auto; /* Let it take available space and wrap */
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-item {
|
||||||
font-size: 13px;
|
flex: 1 1 180px; /* Each item will have a base width of 180px and can grow */
|
||||||
color: #666;
|
min-width: 180px; /* Prevent from becoming too narrow */
|
||||||
white-space: nowrap;
|
|
||||||
min-width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-separator {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-actions {
|
.filter-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-left: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 768px) {
|
||||||
.filter-row {
|
.pagination-container {
|
||||||
gap: 16px;
|
flex-direction: column;
|
||||||
}
|
gap: 12px;
|
||||||
|
|
||||||
.filter-group {
|
|
||||||
min-width: auto;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
.filter-actions {
|
.filter-actions {
|
||||||
margin-left: 0;
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.filter-actions .n-button {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-main {
|
.table-main {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -453,7 +440,7 @@ function changePageSize(size: number) {
|
|||||||
/* background: white;
|
/* background: white;
|
||||||
border-radius: 8px; */
|
border-radius: 8px; */
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.empty-container {
|
.empty-container {
|
||||||
|
@@ -7,10 +7,8 @@ import { NSpace } from "naive-ui";
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-container">
|
<div class="dashboard-container">
|
||||||
<n-space vertical size="large">
|
<n-space vertical size="large">
|
||||||
<n-space vertical size="large" style="gap: 24px">
|
<base-info-card />
|
||||||
<base-info-card />
|
<line-chart class="dashboard-chart" />
|
||||||
<line-chart class="dashboard-chart" />
|
|
||||||
</n-space>
|
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -106,14 +106,14 @@ function handleGroupDelete(deletedGroup: Group) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.keys-container {
|
.keys-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 240px;
|
width: 100%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: calc(100vh - 159px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
@@ -133,4 +133,15 @@ function handleGroupDelete(deletedGroup: Group) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.keys-container {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
height: calc(100vh - 159px);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Reference in New Issue
Block a user