feat: 前端样式调整
This commit is contained in:
@@ -24,22 +24,6 @@ const isLoggedIn = computed(() => !!authKey.value);
|
|||||||
<style>
|
<style>
|
||||||
#app-root {
|
#app-root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* height: 100vh; */
|
|
||||||
overflow: hidden;
|
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>
|
</style>
|
||||||
|
@@ -143,8 +143,8 @@ body {
|
|||||||
|
|
||||||
/* 美化滚动条 */
|
/* 美化滚动条 */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 2px;
|
width: 6px;
|
||||||
height: 2px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
|
@@ -58,7 +58,7 @@ onMounted(() => {
|
|||||||
<n-card
|
<n-card
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
class="stat-card"
|
class="stat-card"
|
||||||
:style="{ animationDelay: `${index * 0.07}s` }"
|
:style="{ animationDelay: `${index * 0.05}s` }"
|
||||||
>
|
>
|
||||||
<div class="stat-header">
|
<div class="stat-header">
|
||||||
<div class="stat-icon" :style="{ background: stat.color }">
|
<div class="stat-icon" :style="{ background: stat.color }">
|
||||||
|
@@ -121,11 +121,9 @@ import NavBar from "@/components/NavBar.vue";
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
max-width: 1400px;
|
width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 24px 12px;
|
padding: 24px 12px;
|
||||||
height: calc(100vh - 64px);
|
|
||||||
overflow: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
|
@@ -17,7 +17,7 @@ import {
|
|||||||
import { onMounted, ref, watch } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
group: Group;
|
group: Group | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
@@ -38,6 +38,11 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
|
if (!props.group) {
|
||||||
|
stats.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
stats.value = await keysApi.getGroupStats(props.group.id);
|
stats.value = await keysApi.getGroupStats(props.group.id);
|
||||||
@@ -69,6 +74,17 @@ function formatNumber(num: number): string {
|
|||||||
function formatPercentage(num: number): string {
|
function formatPercentage(num: number): string {
|
||||||
return `${num.toFixed(1)}%`;
|
return `${num.toFixed(1)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyUrl(url: string) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(url)
|
||||||
|
.then(() => {
|
||||||
|
window.$message.success("地址已复制到剪贴板");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
window.$message.error("复制失败");
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -78,7 +94,14 @@ function formatPercentage(num: number): string {
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h3 class="group-title">
|
<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>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
@@ -114,33 +137,36 @@ function formatPercentage(num: number): string {
|
|||||||
<!-- 统计摘要区 -->
|
<!-- 统计摘要区 -->
|
||||||
<div class="stats-summary">
|
<div class="stats-summary">
|
||||||
<n-spin :show="loading" size="small">
|
<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-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>
|
<template #header-extra><span class="status-title">密钥数量</span></template>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item span="1">
|
<n-grid-item span="1">
|
||||||
<n-card
|
<n-card
|
||||||
class="status-card-failure"
|
class="status-card-failure"
|
||||||
:title="formatPercentage(stats.failure_rate_24h)"
|
:title="formatPercentage(stats?.failure_rate_24h || 0)"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<template #header-extra><span class="status-title">失败率</span></template>
|
<template #header-extra><span class="status-title">失败率</span></template>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item span="1">
|
<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>
|
<template #header-extra><span class="status-title">近1小时</span></template>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item span="1">
|
<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>
|
<template #header-extra><span class="status-title">近24小时</span></template>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item span="1">
|
<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>
|
<template #header-extra><span class="status-title">近7天</span></template>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
@@ -156,13 +182,15 @@ function formatPercentage(num: number): string {
|
|||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4 class="section-title">基础信息</h4>
|
<h4 class="section-title">基础信息</h4>
|
||||||
<n-descriptions :column="2" size="small">
|
<n-descriptions :column="2" size="small">
|
||||||
<n-descriptions-item label="分组名称">{{ group.name }}</n-descriptions-item>
|
<n-descriptions-item label="分组名称">
|
||||||
<n-descriptions-item label="渠道类型">
|
{{ group?.name || "-" }}
|
||||||
{{ group.channel_type }}
|
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="排序">{{ group.sort }}</n-descriptions-item>
|
<n-descriptions-item label="渠道类型">
|
||||||
<n-descriptions-item v-if="group.description" label="描述" :span="2">
|
{{ group?.channel_type || "openai" }}
|
||||||
{{ group.description }}
|
</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-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,7 +199,7 @@ function formatPercentage(num: number): string {
|
|||||||
<h4 class="section-title">上游地址</h4>
|
<h4 class="section-title">上游地址</h4>
|
||||||
<n-descriptions :column="1" size="small">
|
<n-descriptions :column="1" size="small">
|
||||||
<n-descriptions-item
|
<n-descriptions-item
|
||||||
v-for="(upstream, index) in group.upstreams"
|
v-for="(upstream, index) in group?.upstreams ?? []"
|
||||||
:key="index"
|
:key="index"
|
||||||
:label="`上游 ${index + 1}`"
|
:label="`上游 ${index + 1}`"
|
||||||
>
|
>
|
||||||
@@ -186,19 +214,19 @@ function formatPercentage(num: number): string {
|
|||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h4 class="section-title">配置信息</h4>
|
<h4 class="section-title">配置信息</h4>
|
||||||
<n-descriptions :column="2" size="small">
|
<n-descriptions :column="2" size="small">
|
||||||
<n-descriptions-item v-if="group.config.test_model" label="测试模型">
|
<n-descriptions-item v-if="group?.config?.test_model || ''" label="测试模型">
|
||||||
{{ group.config.test_model }}
|
{{ group?.config?.test_model || "" }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item v-if="group.config.request_timeout" label="请求超时">
|
<n-descriptions-item v-if="group?.config?.request_timeout || 0" label="请求超时">
|
||||||
{{ group.config.request_timeout }}ms
|
{{ group?.config?.request_timeout || 0 }}ms
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<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="参数覆盖"
|
label="参数覆盖"
|
||||||
:span="2"
|
:span="2"
|
||||||
>
|
>
|
||||||
<pre class="config-json">{{
|
<pre class="config-json">{{
|
||||||
JSON.stringify(group.config.param_overrides, null, 2)
|
JSON.stringify(group?.config?.param_overrides || "", null, 2)
|
||||||
}}</pre>
|
}}</pre>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
@@ -245,6 +273,17 @@ function formatPercentage(num: number): string {
|
|||||||
margin: 0 0 8px 0;
|
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 {
|
/* .group-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@@ -120,7 +120,7 @@ async function createDemoGroup() {
|
|||||||
<n-tag size="tiny" :type="getChannelTagType(group.channel_type)">
|
<n-tag size="tiny" :type="getChannelTagType(group.channel_type)">
|
||||||
{{ group.channel_type }}
|
{{ group.channel_type }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<span class="group-id">#{{ group.id }}</span>
|
<span class="group-id">#{{ group.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -185,14 +185,12 @@ function formatRelativeTime(date: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusClass(status: "active" | "inactive" | "error") {
|
function getStatusClass(status: "active" | "inactive") {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "active":
|
case "active":
|
||||||
return "status-valid";
|
return "status-valid";
|
||||||
case "inactive":
|
case "inactive":
|
||||||
return "status-invalid";
|
return "status-invalid";
|
||||||
case "error":
|
|
||||||
return "status-error";
|
|
||||||
default:
|
default:
|
||||||
return "status-unknown";
|
return "status-unknown";
|
||||||
}
|
}
|
||||||
@@ -385,6 +383,8 @@ function changePageSize(size: number) {
|
|||||||
<!-- 主要信息行:Key + 快速操作 -->
|
<!-- 主要信息行:Key + 快速操作 -->
|
||||||
<div class="key-main">
|
<div class="key-main">
|
||||||
<div class="key-section">
|
<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>
|
<span class="key-text" :title="key.key_value">{{ maskKey(key.key_value) }}</span>
|
||||||
<div class="quick-actions">
|
<div class="quick-actions">
|
||||||
<n-button size="tiny" text @click="toggleKeyVisibility(key)" title="显示/隐藏">
|
<n-button size="tiny" text @click="toggleKeyVisibility(key)" title="显示/隐藏">
|
||||||
@@ -417,12 +417,15 @@ function changePageSize(size: number) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="key-actions">
|
<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
|
<n-button
|
||||||
v-if="key.status !== 'active'"
|
v-if="key.status !== 'active'"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@click="restoreKey(key)"
|
@click="restoreKey(key)"
|
||||||
title="恢复密钥"
|
title="恢复密钥"
|
||||||
|
type="warning"
|
||||||
>
|
>
|
||||||
恢复
|
恢复
|
||||||
</n-button>
|
</n-button>
|
||||||
|
@@ -3,7 +3,7 @@ export interface APIKey {
|
|||||||
id: number;
|
id: number;
|
||||||
group_id: number;
|
group_id: number;
|
||||||
key_value: string;
|
key_value: string;
|
||||||
status: "active" | "inactive" | "error";
|
status: "active" | "inactive";
|
||||||
request_count: number;
|
request_count: number;
|
||||||
failure_count: number;
|
failure_count: number;
|
||||||
last_used_at?: string;
|
last_used_at?: string;
|
||||||
|
@@ -55,7 +55,7 @@ function handleGroupRefresh() {
|
|||||||
<!-- 右侧主内容区域,占80% -->
|
<!-- 右侧主内容区域,占80% -->
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<!-- 分组信息卡片,更紧凑 -->
|
<!-- 分组信息卡片,更紧凑 -->
|
||||||
<div v-if="selectedGroup" class="group-info">
|
<div class="group-info">
|
||||||
<group-info-card :group="selectedGroup" @refresh="handleGroupRefresh" />
|
<group-info-card :group="selectedGroup" @refresh="handleGroupRefresh" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,15 +69,6 @@ function handleGroupRefresh() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.keys-container {
|
|
||||||
/* padding: 12px 0; */
|
|
||||||
/* max-width: 1600px; */
|
|
||||||
/* margin: 0 auto; */
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
@@ -101,6 +92,7 @@ function handleGroupRefresh() {
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
height: calc(100vh - 88px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
|
@@ -43,14 +43,14 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<!-- <div class="settings-header">
|
|
||||||
<h2 class="settings-title">系统设置</h2>
|
|
||||||
<p class="settings-subtitle">配置系统参数和选项</p>
|
|
||||||
</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">
|
||||||
<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">
|
<n-card class="category-card modern-card" :bordered="false" size="small">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="category-header">
|
<div class="category-header">
|
||||||
@@ -134,11 +134,6 @@ async function handleSubmit() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* .settings-container {
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.settings-header {
|
.settings-header {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -173,18 +168,6 @@ async function handleSubmit() {
|
|||||||
margin-bottom: 12px;
|
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 {
|
.category-card {
|
||||||
background: rgba(255, 255, 255, 0.98);
|
background: rgba(255, 255, 255, 0.98);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user