group info

This commit is contained in:
hptangxi
2025-07-06 12:16:44 +08:00
parent e5cd8f7b34
commit fd9bcc1aba
12 changed files with 124 additions and 284 deletions

View File

@@ -15,7 +15,7 @@ export const keysApi = {
},
// 更新分组
async updateGroup(groupId: number, group: Partial<Group>): Promise<void> {
async updateGroup(groupId: number, group: Partial<Group>): Promise<Group> {
const res = await http.put(`/groups/${groupId}`, group);
return res.data;
},

View File

@@ -203,20 +203,4 @@ onMounted(() => {
:deep(.n-grid-item) {
min-width: 0;
}
@media (max-width: 1200px) {
:deep(.n-grid) {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
:deep(.n-grid) {
grid-template-columns: 1fr;
}
.stat-value {
font-size: 2rem;
}
}
</style>

View File

@@ -180,13 +180,4 @@ function handleClose() {
border-radius: var(--border-radius-sm);
margin-top: 8px;
}
@media (max-width: 768px) {
.global-task-progress {
left: 20px;
right: 20px;
width: auto;
top: 10px;
}
}
</style>

View File

@@ -97,8 +97,6 @@ import NavBar from "@/components/NavBar.vue";
}
.content-wrapper {
width: 1400px;
margin: 0 auto;
padding: 24px 12px;
}
</style>

View File

@@ -248,18 +248,4 @@ onMounted(() => {
border-radius: 4px;
backdrop-filter: blur(4px);
}
@media (max-width: 768px) {
.chart-legend {
gap: 16px;
}
.chart-area {
height: 200px;
}
.chart-svg {
height: 160px;
}
}
</style>

View File

@@ -24,7 +24,7 @@ interface Props {
interface Emits {
(e: "update:show", value: boolean): void;
(e: "success"): void;
(e: "success", value: Group): void;
}
// 配置项类型
@@ -138,8 +138,8 @@ function loadGroupData() {
: [{ url: "", weight: 1 }],
channel_type: props.group.channel_type || "openai",
sort: props.group.sort || 1,
test_model: props.group.config?.test_model || "",
param_overrides: JSON.stringify(props.group.config?.param_overrides || {}, null, 2),
test_model: props.group.test_model || "",
param_overrides: JSON.stringify(props.group.param_overrides || {}, null, 2),
config: {},
configItems,
});
@@ -241,17 +241,18 @@ async function handleSubmit() {
config,
};
let res: Group;
if (props.group?.id) {
// 编辑模式
await keysApi.updateGroup(props.group.id, submitData);
res = await keysApi.updateGroup(props.group.id, submitData);
message.success("分组更新成功");
} else {
// 新建模式
await keysApi.createGroup(submitData);
res = await keysApi.createGroup(submitData);
message.success("分组创建成功");
}
emit("success");
emit("success", res);
handleClose();
} finally {
loading.value = false;

View File

@@ -7,25 +7,32 @@ import {
NCard,
NCollapse,
NCollapseItem,
NDescriptions,
NDescriptionsItem,
NGrid,
NGridItem,
NFlex,
NForm,
NFormItem,
NSpin,
NTag,
useMessage,
} from "naive-ui";
import { onMounted, ref, watch } from "vue";
import GroupFormModal from "./GroupFormModal.vue";
interface Props {
group: Group | null;
}
interface Emits {
(e: "refresh", value: Group): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const stats = ref<GroupStats | null>(null);
const loading = ref(false);
const message = useMessage();
const showEditModal = ref(false);
onMounted(() => {
loadStats();
@@ -53,7 +60,14 @@ async function loadStats() {
}
function handleEdit() {
message.info("编辑分组功能开发中...");
showEditModal.value = true;
}
function handleGroupEdited(newGroup: Group) {
showEditModal.value = false;
if (newGroup) {
emit("refresh", newGroup);
}
}
function handleDelete() {
@@ -136,40 +150,27 @@ function copyUrl(url: string) {
<!-- 统计摘要区 -->
<div class="stats-summary">
<n-spin :show="loading" size="small">
<n-grid :cols="5" :x-gap="12" :y-gap="12" responsive="screen">
<n-grid-item span="1">
<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 || 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 || 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 || 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 || 0)" size="large">
<template #header-extra><span class="status-title">近7天</span></template>
</n-card>
</n-grid-item>
</n-grid>
<n-flex class="status-cards-container">
<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-card
class="status-card-failure"
:title="formatPercentage(stats?.failure_rate_24h || 0)"
size="large"
>
<template #header-extra><span class="status-title">失败率</span></template>
</n-card>
<n-card :title="formatNumber(stats?.requests_1h || 0)" size="large">
<template #header-extra><span class="status-title">近1小时</span></template>
</n-card>
<n-card :title="formatNumber(stats?.requests_24h || 0)" size="large">
<template #header-extra><span class="status-title">近24小时</span></template>
</n-card>
<n-card :title="formatNumber(stats?.requests_7d || 0)" size="large">
<template #header-extra><span class="status-title">近7天</span></template>
</n-card>
</n-flex>
</n-spin>
</div>
@@ -180,61 +181,73 @@ function copyUrl(url: string) {
<div class="details-content">
<div class="detail-section">
<h4 class="section-title">基础信息</h4>
<n-descriptions :column="2" size="small">
<n-descriptions-item label="分组名称">
<n-form label-placement="left" label-width="100px">
<n-form-item label="分组名称">
{{ group?.name || "-" }}
</n-descriptions-item>
<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>
</n-form-item>
<n-form-item label="显示名称:">
{{ group?.display_name || "-" }}
</n-form-item>
<n-form-item label="描述:">
{{ group?.description || "-" }}
</n-form-item>
<n-form-item label="渠道类型:">
{{ group?.channel_type || "-" }}
</n-form-item>
<n-form-item label="测试模型:">
{{ group?.test_model || "-" }}
</n-form-item>
<n-form-item label="排序:">
{{ group?.sort || 0 }}
</n-form-item>
</n-form>
</div>
<div class="detail-section">
<h4 class="section-title">上游地址</h4>
<n-descriptions :column="1" size="small">
<n-descriptions-item
<n-form label-placement="left" label-width="100px">
<n-form-item
v-for="(upstream, index) in group?.upstreams ?? []"
:key="index"
:label="`上游 ${index + 1}`"
:label="`上游 ${index + 1}:`"
>
<span class="upstream-url">{{ upstream.url }}</span>
<n-tag size="small" type="info" class="upstream-weight">
权重: {{ upstream.weight }}
</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-form-item>
</n-form>
</div>
<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>
<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"
label="参数覆盖"
:span="2"
<div
class="detail-section"
v-if="
(group?.config && Object.keys(group.config).length > 0) || group?.param_overrides
"
>
<h4 class="section-title">高级配置</h4>
<n-form label-placement="left">
<n-form-item
v-for="(value, key) in group?.config || {}"
:key="key"
:label="`${key}:`"
>
{{ value || "-" }}
</n-form-item>
<n-form-item v-if="group?.param_overrides" label="参数覆盖:" :span="2">
<pre class="config-json">{{
JSON.stringify(group?.config?.param_overrides || "", null, 2)
JSON.stringify(group?.param_overrides || "", null, 2)
}}</pre>
</n-descriptions-item>
</n-descriptions>
</n-form-item>
</n-form>
</div>
</div>
</n-collapse-item>
</n-collapse>
</div>
</n-card>
<group-form-modal v-model:show="showEditModal" :group="group" @success="handleGroupEdited" />
</div>
</template>
@@ -305,6 +318,10 @@ function copyUrl(url: string) {
text-align: center;
}
.status-cards-container:deep(.n-card) {
width: 160px;
}
:deep(.status-card-failure .n-card-header__main) {
color: #d03050;
}
@@ -371,42 +388,7 @@ function copyUrl(url: string) {
}
}
/* 响应式网格 */
:deep(.n-grid) {
gap: 8px;
}
:deep(.n-grid-item) {
min-width: 0;
}
@media (max-width: 768px) {
:deep(.n-grid) {
grid-template-columns: repeat(2, 1fr);
}
.group-title {
font-size: 1rem;
}
.section-title {
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
:deep(.n-grid) {
grid-template-columns: 1fr;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.header-actions {
align-self: flex-end;
}
:deep(.n-form-item-feedback-wrapper) {
min-height: 0;
}
</style>

View File

@@ -265,18 +265,4 @@ function handleGroupCreated() {
.groups-list::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
@media (max-width: 768px) {
.group-item {
padding: 6px;
}
.group-name {
font-size: 11px;
}
.group-meta {
font-size: 9px;
}
}
</style>

View File

@@ -852,40 +852,4 @@ function changePageSize(size: number) {
font-size: 12px;
color: #6c757d;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.keys-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 1024px) {
.toolbar {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.toolbar-left,
.toolbar-right {
justify-content: center;
}
}
@media (max-width: 768px) {
.keys-grid {
grid-template-columns: 1fr;
}
.key-bottom {
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.key-actions {
align-self: flex-end;
}
}
</style>

View File

@@ -49,14 +49,4 @@ import { NSpace } from "naive-ui";
transform: translateY(0);
}
}
@media (max-width: 768px) {
.dashboard-title {
font-size: 1.75rem;
}
.dashboard-subtitle {
font-size: 1rem;
}
}
</style>

View File

@@ -31,59 +31,47 @@ function handleGroupSelect(group: Group) {
selectedGroup.value = group;
}
function handleGroupRefresh() {
loadGroups();
async function handleGroupRefresh() {
await loadGroups();
if (selectedGroup.value) {
// 重新加载当前选中的分组信息
selectedGroup.value = groups.value.find(g => g.id === selectedGroup.value?.id) || null;
}
}
</script>
<template>
<div class="keys-container">
<div class="keys-content">
<div class="sidebar">
<group-list
:groups="groups"
:selected-group="selectedGroup"
:loading="loading"
@group-select="handleGroupSelect"
@refresh="handleGroupRefresh"
/>
<div class="sidebar">
<group-list
:groups="groups"
:selected-group="selectedGroup"
:loading="loading"
@group-select="handleGroupSelect"
@refresh="handleGroupRefresh"
/>
</div>
<!-- 右侧主内容区域占80% -->
<div class="main-content">
<!-- 分组信息卡片更紧凑 -->
<div class="group-info">
<group-info-card :group="selectedGroup" @refresh="handleGroupRefresh" />
</div>
<!-- 右侧主内容区域80% -->
<div class="main-content">
<!-- 分组信息卡片更紧凑 -->
<div class="group-info">
<group-info-card :group="selectedGroup" @refresh="handleGroupRefresh" />
</div>
<!-- 密钥表格区域占主要空间 -->
<div class="key-table-section">
<key-table :selected-group="selectedGroup" />
</div>
<!-- 密钥表格区域主要空间 -->
<div class="key-table-section">
<key-table :selected-group="selectedGroup" />
</div>
</div>
</div>
</template>
<style scoped>
.page-header {
margin-bottom: 12px;
padding-bottom: 6px;
border-bottom: 1px solid #e9ecef;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0;
}
.keys-content {
.keys-container {
display: flex;
gap: 12px;
flex: 1;
min-height: 0;
width: 100%;
}
.sidebar {
@@ -97,7 +85,6 @@ function handleGroupRefresh() {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.group-info {
@@ -110,18 +97,4 @@ function handleGroupRefresh() {
flex-direction: column;
min-height: 0;
}
@media (max-width: 1024px) {
.keys-content {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.main-content {
width: 100%;
}
}
</style>

View File

@@ -99,19 +99,4 @@ import { NCard, NH3, NSpace, NTag, NText } from "naive-ui";
transform: translateY(0);
}
}
@media (max-width: 768px) {
.placeholder-card {
margin: 0 16px;
padding: 32px 24px;
}
.page-title {
font-size: 1.75rem;
}
.page-subtitle {
font-size: 1rem;
}
}
</style>