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

View File

@@ -123,8 +123,9 @@ import NavBar from "@/components/NavBar.vue";
.content-wrapper { .content-wrapper {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
padding: 24px; padding: 24px 12px;
min-height: calc(100vh - 64px); 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-value">{{ stats.active_keys }}/{{ stats.total_keys }}</div>
<div class="stat-label">密钥数量</div> <div class="stat-label">密钥数量</div>
</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-item">
<div class="stat-value">{{ formatNumber(stats.requests_1h) }}</div> <div class="stat-value">{{ formatNumber(stats.requests_1h) }}</div>
<div class="stat-label">近1小时</div> <div class="stat-label">近1小时</div>
@@ -101,10 +107,8 @@ function formatPercentage(num: number): string {
<div class="stat-label">近24小时</div> <div class="stat-label">近24小时</div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<div class="stat-value failure-rate"> <div class="stat-value">{{ formatNumber(stats.requests_7d) }}</div>
{{ formatPercentage(stats.failure_rate_24h) }} <div class="stat-label">近7天</div>
</div>
<div class="stat-label">失败率</div>
</div> </div>
</div> </div>
</div> </div>
@@ -304,7 +308,7 @@ function formatPercentage(num: number): string {
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(5, 1fr);
gap: 12px; gap: 12px;
} }

View File

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

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { keysApi } from "@/api/keys"; import { keysApi } from "@/api/keys";
import type { APIKey, Group } from "@/types/models"; import type { APIKey, Group } from "@/types/models";
import { NButton, NDropdown, NEmpty, NInput, NSelect, NSpace, NSpin } from "naive-ui";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
interface Props { interface Props {
@@ -16,10 +17,28 @@ const statusFilter = ref<"all" | "valid" | "invalid">("all");
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(20); const pageSize = ref(20);
const totalKeys = ref(0); const totalKeys = ref(0);
const showMoreMenu = ref(false);
const totalPages = computed(() => Math.ceil(totalKeys.value / pageSize.value)); 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( watch(
() => props.selectedGroup, () => props.selectedGroup,
async newGroup => { async newGroup => {
@@ -35,6 +54,30 @@ watch([currentPage, pageSize, statusFilter, searchText], async () => {
await loadKeys(); 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() { async function loadKeys() {
if (!props.selectedGroup) { if (!props.selectedGroup) {
return; return;
@@ -298,118 +341,134 @@ function changePageSize(size: number) {
<!-- 工具栏 --> <!-- 工具栏 -->
<div class="toolbar"> <div class="toolbar">
<div class="toolbar-left"> <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>
<div class="toolbar-right"> <div class="toolbar-right">
<div class="filter-group"> <n-space :size="12">
<select v-model="statusFilter" class="filter-select"> <n-select
<option value="all">全部</option> v-model:value="statusFilter"
<option value="valid">有效</option> :options="statusOptions"
<option value="invalid">无效</option> size="small"
</select> style="width: 100px"
</div> />
<div class="filter-group"> <n-input
<input v-model="searchText" type="text" placeholder="Key 模糊查询" class="search-input" /> v-model:value="searchText"
</div> placeholder="Key 模糊查询"
<div class="more-actions"> size="small"
<button @click="showMoreMenu = !showMoreMenu" class="btn btn-secondary btn-sm"> style="width: 180px"
<span class="more-icon"></span> />
</button> <n-dropdown :options="moreOptions" trigger="click" @select="handleMoreAction">
<div v-if="showMoreMenu" class="more-menu"> <n-button size="small" secondary>
<button @click="copyAllKeys" class="menu-item">复制所有 Key</button> <template #icon>
<button @click="copyValidKeys" class="menu-item">复制有效 Key</button> <span style="font-size: 16px; font-weight: bold"></span>
<button @click="copyInvalidKeys" class="menu-item">复制无效 Key</button> </template>
<div class="menu-divider" /> </n-button>
<button @click="restoreAllInvalid" class="menu-item">恢复所有无效 Key</button> </n-dropdown>
<button @click="validateAllKeys" class="menu-item">验证所有 Key</button> </n-space>
<div class="menu-divider" />
<button @click="clearAllInvalid" class="menu-item danger">清空所有无效 Key</button>
</div>
</div>
</div> </div>
</div> </div>
<!-- 密钥卡片网格 --> <!-- 密钥卡片网格 -->
<div class="keys-grid-container"> <div class="keys-grid-container">
<div v-if="loading" class="loading-state"> <n-spin :show="loading">
<div class="loading-spinner">加载中...</div> <div v-if="keys.length === 0 && !loading" class="empty-container">
</div> <n-empty description="没有找到匹配的密钥" />
<div v-else-if="keys.length === 0" class="empty-state"> </div>
<div class="empty-text">没有找到匹配的密钥</div> <div v-else class="keys-grid">
</div> <div
<div v-else class="keys-grid"> v-for="key in keys"
<div v-for="key in keys" :key="key.id" class="key-card" :class="getStatusClass(key.status)"> :key="key.id"
<!-- 主要信息行Key + 快速操作 --> class="key-card"
<div class="key-main"> :class="getStatusClass(key.status)"
<div class="key-section"> >
<span class="key-text" :title="key.key_value">{{ maskKey(key.key_value) }}</span> <!-- 主要信息行Key + 快速操作 -->
<div class="quick-actions"> <div class="key-main">
<button @click="toggleKeyVisibility(key)" class="quick-btn" title="显示/隐藏"> <div class="key-section">
👁 <span class="key-text" :title="key.key_value">{{ maskKey(key.key_value) }}</span>
</button> <div class="quick-actions">
<button @click="copyKey(key)" class="quick-btn" title="复制">📋</button> <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>
</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>
</div> </n-spin>
</div> </div>
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-container"> <div class="pagination-container">
<div class="pagination-info"> <div class="pagination-info">
<span> {{ totalKeys }} 条记录</span> <span> {{ totalKeys }} 条记录</span>
<select v-model="pageSize" @change="changePageSize(pageSize)" class="page-size-select"> <n-select
<option :value="10">10/</option> v-model:value="pageSize"
<option :value="20">20/</option> :options="[
<option :value="50">50/</option> { label: '10条/页', value: 10 },
<option :value="100">100/</option> { label: '20条/页', value: 20 },
</select> { label: '50条/页', value: 50 },
{ label: '100条/页', value: 100 },
]"
size="small"
style="width: 100px; margin-left: 12px"
@update:value="changePageSize"
/>
</div> </div>
<div class="pagination-controls"> <div class="pagination-controls">
<button <n-button size="small" :disabled="currentPage <= 1" @click="changePage(currentPage - 1)">
@click="changePage(currentPage - 1)"
:disabled="currentPage <= 1"
class="btn btn-secondary btn-sm"
>
上一页 上一页
</button> </n-button>
<span class="page-info"> {{ currentPage }} {{ totalPages }} </span> <span class="page-info"> {{ currentPage }} {{ totalPages }} </span>
<button <n-button
@click="changePage(currentPage + 1)" size="small"
:disabled="currentPage >= totalPages" :disabled="currentPage >= totalPages"
class="btn btn-secondary btn-sm" @click="changePage(currentPage + 1)"
> >
下一页 下一页
</button> </n-button>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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