feat: 前端搭建-未完成
This commit is contained in:
80
web/src/components/GroupConfigForm.vue
Normal file
80
web/src/components/GroupConfigForm.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="group-config-form">
|
||||
<el-card v-if="groupStore.selectedGroupDetails" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>分组配置</span>
|
||||
<el-button type="primary" @click="handleSave" :loading="isSaving">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="formData" label-width="120px" ref="formRef">
|
||||
<el-form-item label="分组名称" prop="name" :rules="[{ required: true, message: '请输入分组名称' }]">
|
||||
<el-input v-model="formData.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="设为默认" prop="is_default">
|
||||
<el-switch v-model="formData.is_default"></el-switch>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-empty v-else description="请先从左侧选择一个分组"></el-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive } from 'vue';
|
||||
import { useGroupStore } from '@/stores/groupStore';
|
||||
import { updateGroup } from '@/api/groups';
|
||||
import { ElCard, ElForm, ElFormItem, ElInput, ElButton, ElSwitch, ElMessage, ElEmpty } from 'element-plus';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
const groupStore = useGroupStore();
|
||||
const formRef = ref<FormInstance>();
|
||||
const isSaving = ref(false);
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
is_default: false,
|
||||
});
|
||||
|
||||
watch(() => groupStore.selectedGroupDetails, (newGroup) => {
|
||||
if (newGroup) {
|
||||
formData.name = newGroup.name;
|
||||
formData.description = newGroup.description;
|
||||
formData.is_default = newGroup.is_default;
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formRef.value || !groupStore.selectedGroupId) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
isSaving.value = true;
|
||||
await updateGroup(groupStore.selectedGroupId, {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
is_default: formData.is_default,
|
||||
});
|
||||
ElMessage.success('保存成功');
|
||||
// 刷新列表以获取最新数据
|
||||
await groupStore.fetchGroups();
|
||||
} catch (error) {
|
||||
console.error('Failed to save group config:', error);
|
||||
ElMessage.error('保存失败,请查看控制台');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
61
web/src/components/GroupList.vue
Normal file
61
web/src/components/GroupList.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="group-list" v-loading="groupStore.isLoading">
|
||||
<el-menu
|
||||
:default-active="groupStore.selectedGroupId || undefined"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="group in groupStore.groups"
|
||||
:key="group.id"
|
||||
:index="group.id"
|
||||
>
|
||||
<template #title>
|
||||
<span>{{ group.name }}</span>
|
||||
<el-tag v-if="group.is_default" size="small" style="margin-left: 8px"
|
||||
>默认</el-tag
|
||||
>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<div
|
||||
v-if="!groupStore.isLoading && groupStore.groups.length === 0"
|
||||
class="empty-state"
|
||||
>
|
||||
暂无分组
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useGroupStore } from "@/stores/groupStore";
|
||||
import { ElMenu, ElMenuItem, ElTag, vLoading } from "element-plus";
|
||||
|
||||
const groupStore = useGroupStore();
|
||||
|
||||
onMounted(() => {
|
||||
// 组件挂载时获取分组数据
|
||||
if (groupStore.groups.length === 0) {
|
||||
groupStore.fetchGroups();
|
||||
}
|
||||
});
|
||||
|
||||
const handleSelect = (index: string) => {
|
||||
groupStore.selectGroup(index);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.group-list {
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
height: 100%;
|
||||
}
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
219
web/src/components/KeyTable.vue
Normal file
219
web/src/components/KeyTable.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div class="key-table">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>密钥管理</span>
|
||||
<el-button type="primary" @click="handleAddKey" :disabled="!groupStore.selectedGroupId">添加密钥</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="keyStore.keys" v-loading="keyStore.isLoading" style="width: 100%">
|
||||
<el-table-column prop="api_key" label="API Key (部分)" min-width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.api_key.substring(0, 3) }}...{{ scope.row.api_key.slice(-4) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="platform" label="平台" width="100" />
|
||||
<el-table-column prop="model_types" label="可用模型" min-width="150">
|
||||
<template #default="scope">
|
||||
<el-tag v-for="model in scope.row.model_types" :key="model" style="margin-right: 5px;">{{ model }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rate_limit" label="速率限制" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.rate_limit }} / {{ scope.row.rate_limit_unit }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_active" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.is_active ? 'success' : 'danger'">
|
||||
{{ scope.row.is_active ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEditKey(scope.row)">编辑</el-button>
|
||||
<el-popconfirm
|
||||
title="确定要删除这个密钥吗?"
|
||||
@confirm="handleDeleteKey(scope.row.id)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!keyStore.isLoading && keyStore.keys.length === 0" description="该分组下暂无密钥"></el-empty>
|
||||
</el-card>
|
||||
|
||||
<!-- Add/Edit Dialog -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
|
||||
<el-form :model="keyFormData" label-width="120px" ref="keyFormRef" :rules="keyFormRules">
|
||||
<el-form-item label="API Key" prop="api_key">
|
||||
<el-input v-model="keyFormData.api_key" placeholder="请输入完整的API Key"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台" prop="platform">
|
||||
<el-select v-model="keyFormData.platform" placeholder="请选择平台">
|
||||
<el-option label="OpenAI" value="OpenAI"></el-option>
|
||||
<el-option label="Gemini" value="Gemini"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="可用模型" prop="model_types">
|
||||
<el-select
|
||||
v-model="keyFormData.model_types"
|
||||
multiple
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="请输入或选择可用模型">
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="速率限制" prop="rate_limit">
|
||||
<el-input-number v-model="keyFormData.rate_limit" :min="0"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="限制单位" prop="rate_limit_unit">
|
||||
<el-select v-model="keyFormData.rate_limit_unit">
|
||||
<el-option label="分钟" value="minute"></el-option>
|
||||
<el-option label="小时" value="hour"></el-option>
|
||||
<el-option label="天" value="day"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态" prop="is_active">
|
||||
<el-switch v-model="keyFormData.is_active"></el-switch>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmSave" :loading="isSaving">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { useKeyStore } from '@/stores/keyStore';
|
||||
import { useGroupStore } from '@/stores/groupStore';
|
||||
import * as keyApi from '@/api/keys';
|
||||
import type { Key } from '@/types/models';
|
||||
import { ElCard, ElTable, ElTableColumn, ElButton, ElTag, ElPopconfirm, ElDialog, ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElSwitch, ElMessage, ElEmpty, ElInputNumber } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
|
||||
const keyStore = useKeyStore();
|
||||
const groupStore = useGroupStore();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const currentKeyId = ref<string | null>(null);
|
||||
const keyFormRef = ref<FormInstance>();
|
||||
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑密钥' : '添加密钥'));
|
||||
|
||||
const initialFormData: Omit<Key, 'id' | 'group_id' | 'usage' | 'created_at' | 'updated_at'> = {
|
||||
api_key: '',
|
||||
platform: 'OpenAI',
|
||||
model_types: [],
|
||||
rate_limit: 60,
|
||||
rate_limit_unit: 'minute',
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
const keyFormData = reactive({ ...initialFormData });
|
||||
|
||||
const keyFormRules = reactive<FormRules>({
|
||||
api_key: [{ required: true, message: '请输入API Key', trigger: 'blur' }],
|
||||
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
|
||||
model_types: [{ required: true, message: '请至少输入一个可用模型', trigger: 'change' }],
|
||||
});
|
||||
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(keyFormData, initialFormData);
|
||||
currentKeyId.value = null;
|
||||
};
|
||||
|
||||
const handleAddKey = () => {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEditKey = (key: Key) => {
|
||||
isEdit.value = true;
|
||||
resetForm();
|
||||
currentKeyId.value = key.id;
|
||||
// 只填充表单所需字段
|
||||
keyFormData.api_key = key.api_key;
|
||||
keyFormData.platform = key.platform;
|
||||
keyFormData.model_types = key.model_types;
|
||||
keyFormData.rate_limit = key.rate_limit;
|
||||
keyFormData.rate_limit_unit = key.rate_limit_unit;
|
||||
keyFormData.is_active = key.is_active;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDeleteKey = async (id: string) => {
|
||||
try {
|
||||
await keyApi.deleteKey(id);
|
||||
ElMessage.success('删除成功');
|
||||
if (groupStore.selectedGroupId) {
|
||||
keyStore.fetchKeys(groupStore.selectedGroupId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete key:', error);
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSave = async () => {
|
||||
if (!keyFormRef.value || !groupStore.selectedGroupId) return;
|
||||
|
||||
try {
|
||||
await keyFormRef.value.validate();
|
||||
isSaving.value = true;
|
||||
|
||||
const dataToSave = {
|
||||
api_key: keyFormData.api_key,
|
||||
platform: keyFormData.platform,
|
||||
model_types: keyFormData.model_types,
|
||||
rate_limit: keyFormData.rate_limit,
|
||||
rate_limit_unit: keyFormData.rate_limit_unit,
|
||||
is_active: keyFormData.is_active,
|
||||
};
|
||||
|
||||
if (isEdit.value && currentKeyId.value) {
|
||||
await keyApi.updateKey(currentKeyId.value, dataToSave);
|
||||
} else {
|
||||
await keyApi.createKey(groupStore.selectedGroupId, dataToSave);
|
||||
}
|
||||
|
||||
ElMessage.success('保存成功');
|
||||
dialogVisible.value = false;
|
||||
await keyStore.fetchKeys(groupStore.selectedGroupId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to save key:', error);
|
||||
ElMessage.error('保存失败,请检查表单或查看控制台');
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.el-table {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
66
web/src/components/LogFilter.vue
Normal file
66
web/src/components/LogFilter.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<el-form :inline="true" :model="filterData" class="log-filter-form">
|
||||
<el-form-item label="分组">
|
||||
<el-select v-model="filterData.group_id" placeholder="所有分组" clearable>
|
||||
<el-option v-for="group in groups" :key="group.id" :label="group.name" :value="group.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态码">
|
||||
<el-input v-model.number="filterData.status_code" placeholder="例如 200" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="applyFilters">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useLogStore } from '@/stores/logStore';
|
||||
import { useGroupStore } from '@/stores/groupStore';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { LogQuery } from '@/api/logs';
|
||||
|
||||
const logStore = useLogStore();
|
||||
const groupStore = useGroupStore();
|
||||
const { groups } = storeToRefs(groupStore);
|
||||
|
||||
const filterData = reactive<LogQuery>({});
|
||||
const dateRange = ref<[Date, Date] | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
groupStore.fetchGroups();
|
||||
});
|
||||
|
||||
const handleDateChange = (dates: [Date, Date] | null) => {
|
||||
if (dates) {
|
||||
filterData.start_time = dates[0].toISOString();
|
||||
filterData.end_time = dates[1].toISOString();
|
||||
} else {
|
||||
filterData.start_time = undefined;
|
||||
filterData.end_time = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
logStore.setFilters(filterData);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-filter-form {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
52
web/src/components/StatsChart.vue
Normal file
52
web/src/components/StatsChart.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div ref="chart" style="width: 100%; height: 400px;"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { GroupRequestStat } from '@/types/models';
|
||||
|
||||
const props = defineProps<{
|
||||
data: GroupRequestStat[];
|
||||
}>();
|
||||
|
||||
const chart = ref<HTMLElement | null>(null);
|
||||
let myChart: echarts.ECharts | null = null;
|
||||
|
||||
const initChart = () => {
|
||||
if (chart.value) {
|
||||
myChart = echarts.init(chart.value);
|
||||
updateChart();
|
||||
}
|
||||
};
|
||||
|
||||
const updateChart = () => {
|
||||
if (!myChart) return;
|
||||
myChart.setOption({
|
||||
title: {
|
||||
text: '各分组请求量',
|
||||
},
|
||||
tooltip: {},
|
||||
xAxis: {
|
||||
data: props.data.map(item => item.group_name),
|
||||
},
|
||||
yAxis: {},
|
||||
series: [
|
||||
{
|
||||
name: '请求量',
|
||||
type: 'bar',
|
||||
data: props.data.map(item => item.request_count),
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
});
|
||||
|
||||
watch(() => props.data, () => {
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
</script>
|
Reference in New Issue
Block a user