feat: 前端样式调整

This commit is contained in:
tbphp
2025-07-04 22:46:35 +08:00
parent c1b9e9d247
commit 3f9439b11f
10 changed files with 81 additions and 82 deletions

View File

@@ -24,22 +24,6 @@ const isLoggedIn = computed(() => !!authKey.value);
<style>
#app-root {
width: 100%;
/* height: 100vh; */
overflow: hidden;
}
.app-transition-enter-active,
.app-transition-leave-active {
transition: all 0.4s ease;
}
.app-transition-enter-from {
opacity: 0;
transform: translateY(20px);
}
.app-transition-leave-to {
opacity: 0;
transform: translateY(-20px);
}
</style>

View File

@@ -143,8 +143,8 @@ body {
/* 美化滚动条 */
::-webkit-scrollbar {
width: 2px;
height: 2px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {

View File

@@ -58,7 +58,7 @@ onMounted(() => {
<n-card
:bordered="false"
class="stat-card"
:style="{ animationDelay: `${index * 0.07}s` }"
:style="{ animationDelay: `${index * 0.05}s` }"
>
<div class="stat-header">
<div class="stat-icon" :style="{ background: stat.color }">

View File

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

View File

@@ -17,7 +17,7 @@ import {
import { onMounted, ref, watch } from "vue";
interface Props {
group: Group;
group: Group | null;
}
const props = defineProps<Props>();
@@ -38,6 +38,11 @@ watch(
);
async function loadStats() {
if (!props.group) {
stats.value = null;
return;
}
try {
loading.value = true;
stats.value = await keysApi.getGroupStats(props.group.id);
@@ -69,6 +74,17 @@ function formatNumber(num: number): string {
function formatPercentage(num: number): string {
return `${num.toFixed(1)}%`;
}
function copyUrl(url: string) {
navigator.clipboard
.writeText(url)
.then(() => {
window.$message.success("地址已复制到剪贴板");
})
.catch(() => {
window.$message.error("复制失败");
});
}
</script>
<template>
@@ -78,7 +94,14 @@ function formatPercentage(num: number): string {
<div class="card-header">
<div class="header-left">
<h3 class="group-title">
{{ group.display_name || group.name }}
{{ group?.display_name || group?.name || "请选择分组" }}
<code
v-if="group"
class="group-url"
@click="copyUrl(`https://gpt-load.com/${group?.name}`)"
>
https://gpt-load.com/{{ group?.name }}
</code>
</h3>
</div>
<div class="header-actions">
@@ -114,33 +137,36 @@ function formatPercentage(num: number): string {
<!-- 统计摘要区 -->
<div class="stats-summary">
<n-spin :show="loading" size="small">
<n-grid v-if="stats" :cols="5" :x-gap="12" :y-gap="12" responsive="screen">
<n-grid :cols="5" :x-gap="12" :y-gap="12" responsive="screen">
<n-grid-item span="1">
<n-card :title="`${stats.active_keys} / ${stats.total_keys}`" size="large">
<n-card
:title="`${stats?.active_keys || 0} / ${stats?.total_keys || 0}`"
size="large"
>
<template #header-extra><span class="status-title">密钥数量</span></template>
</n-card>
</n-grid-item>
<n-grid-item span="1">
<n-card
class="status-card-failure"
:title="formatPercentage(stats.failure_rate_24h)"
:title="formatPercentage(stats?.failure_rate_24h || 0)"
size="large"
>
<template #header-extra><span class="status-title">失败率</span></template>
</n-card>
</n-grid-item>
<n-grid-item span="1">
<n-card :title="formatNumber(stats.requests_1h)" size="large">
<n-card :title="formatNumber(stats?.requests_1h || 0)" size="large">
<template #header-extra><span class="status-title">近1小时</span></template>
</n-card>
</n-grid-item>
<n-grid-item span="1">
<n-card :title="formatNumber(stats.requests_24h)" size="large">
<n-card :title="formatNumber(stats?.requests_24h || 0)" size="large">
<template #header-extra><span class="status-title">近24小时</span></template>
</n-card>
</n-grid-item>
<n-grid-item span="1">
<n-card :title="formatNumber(stats.requests_7d)" size="large">
<n-card :title="formatNumber(stats?.requests_7d || 0)" size="large">
<template #header-extra><span class="status-title">近7天</span></template>
</n-card>
</n-grid-item>
@@ -156,13 +182,15 @@ function formatPercentage(num: number): string {
<div class="detail-section">
<h4 class="section-title">基础信息</h4>
<n-descriptions :column="2" size="small">
<n-descriptions-item label="分组名称">{{ group.name }}</n-descriptions-item>
<n-descriptions-item label="渠道类型">
{{ group.channel_type }}
<n-descriptions-item label="分组名称">
{{ group?.name || "-" }}
</n-descriptions-item>
<n-descriptions-item label="排序">{{ group.sort }}</n-descriptions-item>
<n-descriptions-item v-if="group.description" label="描述" :span="2">
{{ group.description }}
<n-descriptions-item label="渠道类型">
{{ group?.channel_type || "openai" }}
</n-descriptions-item>
<n-descriptions-item label="排序">{{ group?.sort || 0 }}</n-descriptions-item>
<n-descriptions-item v-if="group?.description || ''" label="描述" :span="2">
{{ group?.description || "" }}
</n-descriptions-item>
</n-descriptions>
</div>
@@ -171,7 +199,7 @@ function formatPercentage(num: number): string {
<h4 class="section-title">上游地址</h4>
<n-descriptions :column="1" size="small">
<n-descriptions-item
v-for="(upstream, index) in group.upstreams"
v-for="(upstream, index) in group?.upstreams ?? []"
:key="index"
:label="`上游 ${index + 1}`"
>
@@ -186,19 +214,19 @@ function formatPercentage(num: number): string {
<div class="detail-section">
<h4 class="section-title">配置信息</h4>
<n-descriptions :column="2" size="small">
<n-descriptions-item v-if="group.config.test_model" label="测试模型">
{{ group.config.test_model }}
<n-descriptions-item v-if="group?.config?.test_model || ''" label="测试模型">
{{ group?.config?.test_model || "" }}
</n-descriptions-item>
<n-descriptions-item v-if="group.config.request_timeout" label="请求超时">
{{ group.config.request_timeout }}ms
<n-descriptions-item v-if="group?.config?.request_timeout || 0" label="请求超时">
{{ group?.config?.request_timeout || 0 }}ms
</n-descriptions-item>
<n-descriptions-item
v-if="Object.keys(group.config.param_overrides || {}).length > 0"
v-if="Object.keys(group?.config?.param_overrides || {}).length > 0"
label="参数覆盖"
:span="2"
>
<pre class="config-json">{{
JSON.stringify(group.config.param_overrides, null, 2)
JSON.stringify(group?.config?.param_overrides || "", null, 2)
}}</pre>
</n-descriptions-item>
</n-descriptions>
@@ -245,6 +273,17 @@ function formatPercentage(num: number): string {
margin: 0 0 8px 0;
}
.group-url {
font-size: 0.8rem;
color: #2563eb;
margin-left: 8px;
font-family: monospace;
background: rgba(37, 99, 235, 0.1);
border-radius: 4px;
padding: 2px 6px;
margin-right: 4px;
}
/* .group-meta {
display: flex;
align-items: center;

View File

@@ -120,7 +120,7 @@ async function createDemoGroup() {
<n-tag size="tiny" :type="getChannelTagType(group.channel_type)">
{{ group.channel_type }}
</n-tag>
<span class="group-id">#{{ group.id }}</span>
<span class="group-id">#{{ group.name }}</span>
</div>
</div>
</div>

View File

@@ -185,14 +185,12 @@ function formatRelativeTime(date: string) {
}
}
function getStatusClass(status: "active" | "inactive" | "error") {
function getStatusClass(status: "active" | "inactive") {
switch (status) {
case "active":
return "status-valid";
case "inactive":
return "status-invalid";
case "error":
return "status-error";
default:
return "status-unknown";
}
@@ -385,6 +383,8 @@ function changePageSize(size: number) {
<!-- 主要信息行Key + 快速操作 -->
<div class="key-main">
<div class="key-section">
<n-tag v-if="key.status === 'active'" type="info">有效</n-tag>
<n-tag v-else>无效</n-tag>
<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="显示/隐藏">
@@ -417,12 +417,15 @@ function changePageSize(size: number) {
</span>
</div>
<div class="key-actions">
<n-button size="tiny" @click="testKey(key)" title="测试密钥">测试</n-button>
<n-button type="info" size="tiny" @click="testKey(key)" title="测试密钥">
测试
</n-button>
<n-button
v-if="key.status !== 'active'"
size="tiny"
@click="restoreKey(key)"
title="恢复密钥"
type="warning"
>
恢复
</n-button>

View File

@@ -3,7 +3,7 @@ export interface APIKey {
id: number;
group_id: number;
key_value: string;
status: "active" | "inactive" | "error";
status: "active" | "inactive";
request_count: number;
failure_count: number;
last_used_at?: string;

View File

@@ -55,7 +55,7 @@ function handleGroupRefresh() {
<!-- 右侧主内容区域占80% -->
<div class="main-content">
<!-- 分组信息卡片更紧凑 -->
<div v-if="selectedGroup" class="group-info">
<div class="group-info">
<group-info-card :group="selectedGroup" @refresh="handleGroupRefresh" />
</div>
@@ -69,15 +69,6 @@ function handleGroupRefresh() {
</template>
<style scoped>
.keys-container {
/* padding: 12px 0; */
/* max-width: 1600px; */
/* margin: 0 auto; */
height: 100%;
display: flex;
flex-direction: column;
}
.page-header {
margin-bottom: 12px;
padding-bottom: 6px;
@@ -101,6 +92,7 @@ function handleGroupRefresh() {
.sidebar {
width: 240px;
flex-shrink: 0;
height: calc(100vh - 88px);
}
.main-content {

View File

@@ -43,14 +43,14 @@ async function handleSubmit() {
<template>
<div class="settings-container">
<!-- <div class="settings-header">
<h2 class="settings-title">系统设置</h2>
<p class="settings-subtitle">配置系统参数和选项</p>
</div> -->
<div class="settings-content">
<n-form ref="formRef" :model="form" label-placement="top" class="settings-form">
<div v-for="(category, cIndex) in settingList" :key="cIndex" class="settings-category">
<div
v-for="(category, cIndex) in settingList"
:key="cIndex"
class="settings-category"
:style="{ animationDelay: `${cIndex * 0.05}s` }"
>
<n-card class="category-card modern-card" :bordered="false" size="small">
<template #header>
<div class="category-header">
@@ -134,11 +134,6 @@ async function handleSubmit() {
</template>
<style scoped>
/* .settings-container {
max-width: 1000px;
margin: 0 auto;
} */
.settings-header {
margin-bottom: 32px;
text-align: center;
@@ -173,18 +168,6 @@ async function handleSubmit() {
margin-bottom: 12px;
}
.settings-category:nth-child(2) {
animation-delay: 0.07s;
}
.settings-category:nth-child(3) {
animation-delay: 0.14s;
}
.settings-category:nth-child(4) {
animation-delay: 0.21s;
}
.category-card {
background: rgba(255, 255, 255, 0.98);
}