Merge pull request #88 from OrzMiku/main

feat: 移动端适配
This commit is contained in:
tbphp
2025-07-31 22:03:24 +08:00
committed by GitHub
14 changed files with 374 additions and 155 deletions

45
web/package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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%);

View File

@@ -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);
} }

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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 }}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>