feat: web优化
This commit is contained in:
@@ -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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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 />
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user