feat: 前端搭建-未完成

This commit is contained in:
tbphp
2025-06-29 21:59:32 +08:00
parent ab95af0bbe
commit 731315144e
62 changed files with 4831 additions and 604 deletions

View File

@@ -0,0 +1,53 @@
<template>
<div class="dashboard-page">
<h1>仪表盘</h1>
<div v-if="loading">加载中...</div>
<div v-if="stats" class="stats-grid">
<el-card>
<el-statistic title="总请求数" :value="stats.total_requests" />
</el-card>
<el-card>
<el-statistic title="成功请求数" :value="stats.success_requests" />
</el-card>
<el-card>
<el-statistic title="成功率" :value="stats.success_rate" :formatter="rateFormatter" />
</el-card>
</div>
<el-card v-if="stats && stats.group_stats.length > 0" class="chart-card">
<StatsChart :data="stats.group_stats" />
</el-card>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useDashboardStore } from '@/stores/dashboardStore';
import StatsChart from '@/components/StatsChart.vue';
const dashboardStore = useDashboardStore();
const { stats, loading } = storeToRefs(dashboardStore);
onMounted(() => {
dashboardStore.fetchStats();
});
const rateFormatter = (rate: number) => {
return `${(rate * 100).toFixed(2)}%`;
};
</script>
<style scoped>
.dashboard-page {
padding: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.chart-card {
margin-top: 20px;
}
</style>

53
web/src/views/Groups.vue Normal file
View File

@@ -0,0 +1,53 @@
<template>
<div class="groups-view">
<el-row :gutter="20" class="main-layout">
<el-col :span="6" class="left-panel">
<group-list />
</el-col>
<el-col :span="18" class="right-panel">
<div class="config-section">
<group-config-form />
</div>
<div class="keys-section">
<key-table />
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import GroupList from '@/components/GroupList.vue';
import GroupConfigForm from '@/components/GroupConfigForm.vue';
import KeyTable from '@/components/KeyTable.vue';
import { ElRow, ElCol } from 'element-plus';
</script>
<style scoped>
.groups-view {
height: 100%;
padding: 20px;
box-sizing: border-box;
}
.main-layout, .left-panel, .right-panel {
height: 100%;
}
.left-panel {
background-color: #fff;
border-radius: 4px;
overflow-y: auto;
}
.right-panel {
display: flex;
flex-direction: column;
gap: 20px;
}
.config-section, .keys-section {
background-color: #fff;
border-radius: 4px;
}
</style>

63
web/src/views/Logs.vue Normal file
View File

@@ -0,0 +1,63 @@
<template>
<div class="logs-page">
<h1>日志查询</h1>
<LogFilter />
<el-table :data="logs" v-loading="loading" style="width: 100%">
<el-table-column prop="timestamp" label="时间" width="180" :formatter="formatDate" />
<el-table-column prop="group_id" label="分组ID" width="100" />
<el-table-column prop="key_id" label="密钥ID" width="100" />
<el-table-column prop="source_ip" label="源IP" width="150" />
<el-table-column prop="status_code" label="状态码" width="100" />
<el-table-column prop="request_path" label="请求路径" />
<el-table-column prop="request_body_snippet" label="请求体片段" />
</el-table>
<el-pagination
background
layout="prev, pager, next, sizes"
:total="pagination.total"
:page-size="pagination.size"
:current-page="pagination.page"
@current-change="handlePageChange"
@size-change="handleSizeChange"
class="pagination-container"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useLogStore } from '@/stores/logStore';
import LogFilter from '@/components/LogFilter.vue';
import type { RequestLog } from '@/types/models';
const logStore = useLogStore();
const { logs, loading, pagination } = storeToRefs(logStore);
onMounted(() => {
logStore.fetchLogs();
});
const handlePageChange = (page: number) => {
logStore.setPage(page);
};
const handleSizeChange = (size: number) => {
logStore.setSize(size);
};
const formatDate = (_row: RequestLog, _column: any, cellValue: string) => {
return new Date(cellValue).toLocaleString();
};
</script>
<style scoped>
.logs-page {
padding: 20px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div v-loading="loading">
<h1>Settings</h1>
<el-form :model="form" label-width="200px">
<el-form-item v-for="setting in settings" :key="setting.key" :label="setting.key">
<el-input v-model="form[setting.key]"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveSettings">Save</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { useSettingStore } from '@/stores/settingStore';
import { storeToRefs } from 'pinia';
import type { Setting } from '@/types/models';
const settingStore = useSettingStore();
const { settings, loading } = storeToRefs(settingStore);
const form = ref<Record<string, string>>({});
onMounted(() => {
settingStore.fetchSettings();
});
watch(settings, (newSettings) => {
form.value = newSettings.reduce((acc, setting) => {
acc[setting.key] = setting.value;
return acc;
}, {} as Record<string, string>);
}, { immediate: true, deep: true });
const saveSettings = () => {
const settingsToUpdate: Setting[] = Object.entries(form.value).map(([key, value]) => ({
key,
value,
}));
settingStore.updateSettings(settingsToUpdate);
};
</script>