feat: web优化

This commit is contained in:
tbphp
2025-07-04 18:26:48 +08:00
parent b93cee6a6f
commit f2e22a08fa
8 changed files with 181 additions and 123 deletions

View File

@@ -1,6 +1,6 @@
:root {
overflow-y: scroll;
scrollbar-gutter: stable;
/* overflow-y: scroll;
scrollbar-gutter: stable; */
font-family:
"Inter",
-apple-system,
@@ -143,18 +143,18 @@ body {
/* 美化滚动条 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 2px;
height: 2px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
border-radius: 1px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 4px;
border-radius: 1px;
}
::-webkit-scrollbar-thumb:hover {

View File

@@ -123,8 +123,9 @@ import NavBar from "@/components/NavBar.vue";
.content-wrapper {
max-width: 1400px;
margin: 0 auto;
padding: 24px;
min-height: calc(100vh - 64px);
padding: 24px 12px;
height: calc(100vh - 64px);
overflow: auto;
}
/* 响应式设计 */

View File

@@ -92,6 +92,12 @@ function formatPercentage(num: number): string {
<div class="stat-value">{{ stats.active_keys }}/{{ stats.total_keys }}</div>
<div class="stat-label">密钥数量</div>
</div>
<div class="stat-item">
<div class="stat-value failure-rate">
{{ formatPercentage(stats.failure_rate_24h) }}
</div>
<div class="stat-label">失败率</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ formatNumber(stats.requests_1h) }}</div>
<div class="stat-label">近1小时</div>
@@ -101,10 +107,8 @@ function formatPercentage(num: number): string {
<div class="stat-label">近24小时</div>
</div>
<div class="stat-item">
<div class="stat-value failure-rate">
{{ formatPercentage(stats.failure_rate_24h) }}
</div>
<div class="stat-label">失败率</div>
<div class="stat-value">{{ formatNumber(stats.requests_7d) }}</div>
<div class="stat-label">近7天</div>
</div>
</div>
</div>
@@ -304,7 +308,7 @@ function formatPercentage(num: number): string {
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}

View File

@@ -232,7 +232,7 @@ async function createDemoGroup() {
.group-name {
font-weight: 600;
font-size: 12px;
font-size: 14px;
line-height: 1.2;
margin-bottom: 2px;
overflow: hidden;

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { keysApi } from "@/api/keys";
import type { APIKey, Group } from "@/types/models";
import { NButton, NDropdown, NEmpty, NInput, NSelect, NSpace, NSpin } from "naive-ui";
import { computed, ref, watch } from "vue";
interface Props {
@@ -16,10 +17,28 @@ const statusFilter = ref<"all" | "valid" | "invalid">("all");
const currentPage = ref(1);
const pageSize = ref(20);
const totalKeys = ref(0);
const showMoreMenu = ref(false);
const totalPages = computed(() => Math.ceil(totalKeys.value / pageSize.value));
// 状态过滤选项
const statusOptions = [
{ label: "全部", value: "all" },
{ label: "有效", value: "valid" },
{ label: "无效", value: "invalid" },
];
// 更多操作下拉菜单选项
const moreOptions = [
{ label: "复制所有 Key", key: "copyAll" },
{ label: "复制有效 Key", key: "copyValid" },
{ label: "复制无效 Key", key: "copyInvalid" },
{ type: "divider" },
{ label: "恢复所有无效 Key", key: "restoreAll" },
{ label: "验证所有 Key", key: "validateAll" },
{ type: "divider" },
{ label: "清空所有无效 Key", key: "clearInvalid", props: { style: { color: "#d03050" } } },
];
watch(
() => props.selectedGroup,
async newGroup => {
@@ -35,6 +54,30 @@ watch([currentPage, pageSize, statusFilter, searchText], async () => {
await loadKeys();
});
// 处理更多操作菜单
function handleMoreAction(key: string) {
switch (key) {
case "copyAll":
copyAllKeys();
break;
case "copyValid":
copyValidKeys();
break;
case "copyInvalid":
copyInvalidKeys();
break;
case "restoreAll":
restoreAllInvalid();
break;
case "validateAll":
validateAllKeys();
break;
case "clearInvalid":
clearAllInvalid();
break;
}
}
async function loadKeys() {
if (!props.selectedGroup) {
return;
@@ -298,118 +341,134 @@ function changePageSize(size: number) {
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<button @click="addKey" class="btn btn-primary btn-sm">+ 添加密钥</button>
<n-button type="primary" size="small" @click="addKey">
<template #icon>
<span style="font-size: 12px">+</span>
</template>
添加密钥
</n-button>
</div>
<div class="toolbar-right">
<div class="filter-group">
<select v-model="statusFilter" class="filter-select">
<option value="all">全部</option>
<option value="valid">有效</option>
<option value="invalid">无效</option>
</select>
</div>
<div class="filter-group">
<input v-model="searchText" type="text" placeholder="Key 模糊查询" class="search-input" />
</div>
<div class="more-actions">
<button @click="showMoreMenu = !showMoreMenu" class="btn btn-secondary btn-sm">
<span class="more-icon"></span>
</button>
<div v-if="showMoreMenu" class="more-menu">
<button @click="copyAllKeys" class="menu-item">复制所有 Key</button>
<button @click="copyValidKeys" class="menu-item">复制有效 Key</button>
<button @click="copyInvalidKeys" class="menu-item">复制无效 Key</button>
<div class="menu-divider" />
<button @click="restoreAllInvalid" class="menu-item">恢复所有无效 Key</button>
<button @click="validateAllKeys" class="menu-item">验证所有 Key</button>
<div class="menu-divider" />
<button @click="clearAllInvalid" class="menu-item danger">清空所有无效 Key</button>
</div>
</div>
<n-space :size="12">
<n-select
v-model:value="statusFilter"
:options="statusOptions"
size="small"
style="width: 100px"
/>
<n-input
v-model:value="searchText"
placeholder="Key 模糊查询"
size="small"
style="width: 180px"
/>
<n-dropdown :options="moreOptions" trigger="click" @select="handleMoreAction">
<n-button size="small" secondary>
<template #icon>
<span style="font-size: 16px; font-weight: bold"></span>
</template>
</n-button>
</n-dropdown>
</n-space>
</div>
</div>
<!-- 密钥卡片网格 -->
<div class="keys-grid-container">
<div v-if="loading" class="loading-state">
<div class="loading-spinner">加载中...</div>
</div>
<div v-else-if="keys.length === 0" class="empty-state">
<div class="empty-text">没有找到匹配的密钥</div>
</div>
<div v-else class="keys-grid">
<div v-for="key in keys" :key="key.id" class="key-card" :class="getStatusClass(key.status)">
<!-- 主要信息行Key + 快速操作 -->
<div class="key-main">
<div class="key-section">
<span class="key-text" :title="key.key_value">{{ maskKey(key.key_value) }}</span>
<div class="quick-actions">
<button @click="toggleKeyVisibility(key)" class="quick-btn" title="显示/隐藏">
👁
</button>
<button @click="copyKey(key)" class="quick-btn" title="复制">📋</button>
<n-spin :show="loading">
<div v-if="keys.length === 0 && !loading" class="empty-container">
<n-empty description="没有找到匹配的密钥" />
</div>
<div v-else class="keys-grid">
<div
v-for="key in keys"
:key="key.id"
class="key-card"
:class="getStatusClass(key.status)"
>
<!-- 主要信息行Key + 快速操作 -->
<div class="key-main">
<div class="key-section">
<span class="key-text" :title="key.key_value">{{ maskKey(key.key_value) }}</span>
<div class="quick-actions">
<n-button size="tiny" text @click="toggleKeyVisibility(key)" title="显示/隐藏">
<template #icon>
<span style="font-size: 12px">👁</span>
</template>
</n-button>
<n-button size="tiny" text @click="copyKey(key)" title="复制">
<template #icon>
<span style="font-size: 12px">📋</span>
</template>
</n-button>
</div>
</div>
</div>
<!-- 统计信息 + 操作按钮行 -->
<div class="key-bottom">
<div class="key-stats">
<span class="stat-item">
请求
<strong>{{ key.request_count }}</strong>
</span>
<span class="stat-item">
失败
<strong>{{ key.failure_count }}</strong>
</span>
<span class="stat-item">
{{ key.last_used_at ? formatRelativeTime(key.last_used_at) : "从未使用" }}
</span>
</div>
<div class="key-actions">
<n-button size="tiny" @click="testKey(key)" title="测试密钥">测试</n-button>
<n-button
v-if="key.status !== 'active'"
size="tiny"
@click="restoreKey(key)"
title="恢复密钥"
>
恢复
</n-button>
<n-button size="tiny" type="error" @click="deleteKey(key)" title="删除密钥">
删除
</n-button>
</div>
</div>
</div>
<!-- 统计信息 + 操作按钮行 -->
<div class="key-bottom">
<div class="key-stats">
<span class="stat-item">
请求
<strong>{{ key.request_count }}</strong>
</span>
<span class="stat-item">
失败
<strong>{{ key.failure_count }}</strong>
</span>
<span class="stat-item">
{{ key.last_used_at ? formatRelativeTime(key.last_used_at) : "从未使用" }}
</span>
</div>
<div class="key-actions">
<button @click="testKey(key)" class="action-btn primary">测试</button>
<button
v-if="key.status !== 'active'"
@click="restoreKey(key)"
class="action-btn secondary"
>
恢复
</button>
<button @click="deleteKey(key)" class="action-btn danger">删除</button>
</div>
</div>
</div>
</div>
</n-spin>
</div>
<!-- 分页 -->
<div class="pagination-container">
<div class="pagination-info">
<span> {{ totalKeys }} 条记录</span>
<select v-model="pageSize" @change="changePageSize(pageSize)" class="page-size-select">
<option :value="10">10/</option>
<option :value="20">20/</option>
<option :value="50">50/</option>
<option :value="100">100/</option>
</select>
<n-select
v-model:value="pageSize"
:options="[
{ label: '10条/页', value: 10 },
{ label: '20条/页', value: 20 },
{ label: '50条/页', value: 50 },
{ label: '100条/页', value: 100 },
]"
size="small"
style="width: 100px; margin-left: 12px"
@update:value="changePageSize"
/>
</div>
<div class="pagination-controls">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage <= 1"
class="btn btn-secondary btn-sm"
>
<n-button size="small" :disabled="currentPage <= 1" @click="changePage(currentPage - 1)">
上一页
</button>
</n-button>
<span class="page-info"> {{ currentPage }} {{ totalPages }} </span>
<button
@click="changePage(currentPage + 1)"
<n-button
size="small"
:disabled="currentPage >= totalPages"
class="btn btn-secondary btn-sm"
@click="changePage(currentPage + 1)"
>
下一页
</button>
</n-button>
</div>
</div>
</div>

View File

@@ -5,10 +5,10 @@ import LineChart from "@/components/LineChart.vue";
<template>
<div class="dashboard-container">
<div class="dashboard-header">
<!-- <div class="dashboard-header">
<h2 class="dashboard-title">仪表盘</h2>
<p class="dashboard-subtitle">系统概览与实时监控</p>
</div>
</div> -->
<div class="dashboard-content">
<base-info-card />

View File

@@ -41,13 +41,7 @@ function handleGroupRefresh() {
<template>
<div class="keys-container">
<!-- 页面头部更紧凑 -->
<div class="page-header">
<h2 class="page-title">密钥管理</h2>
</div>
<div class="keys-content">
<!-- 左侧分组列表宽度减少到20% -->
<div class="sidebar">
<group-list
:groups="groups"
@@ -76,10 +70,10 @@ function handleGroupRefresh() {
<style scoped>
.keys-container {
padding: 12px;
max-width: 1600px;
margin: 0 auto;
height: 100vh;
/* padding: 12px 0; */
/* max-width: 1600px; */
/* margin: 0 auto; */
height: 100%;
display: flex;
flex-direction: column;
}

View File

@@ -43,10 +43,10 @@ async function handleSubmit() {
<template>
<div class="settings-container">
<div class="settings-header">
<!-- <div class="settings-header">
<h2 class="settings-title">系统设置</h2>
<p class="settings-subtitle">配置系统参数和选项</p>
</div>
</div> -->
<div class="settings-content">
<n-form ref="formRef" :model="form" label-placement="top" class="settings-form">
@@ -134,10 +134,10 @@ async function handleSubmit() {
</template>
<style scoped>
.settings-container {
/* .settings-container {
max-width: 1000px;
margin: 0 auto;
}
} */
.settings-header {
margin-bottom: 32px;
@@ -165,12 +165,12 @@ async function handleSubmit() {
.settings-content {
display: flex;
flex-direction: column;
gap: 24px;
gap: 12px;
}
.settings-category {
animation: fadeInUp 0.2s ease-out both;
margin-bottom: 24px;
margin-bottom: 12px;
}
.settings-category:nth-child(2) {
@@ -211,7 +211,7 @@ async function handleSubmit() {
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 12px 10px;
}
@@ -263,7 +263,7 @@ async function handleSubmit() {
.settings-actions {
display: flex;
justify-content: center;
padding-top: 24px;
padding-top: 12px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
}