group info
This commit is contained in:
@@ -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;
|
||||
},
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -97,8 +97,6 @@ import NavBar from "@/components/NavBar.vue";
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 12px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user