Files
hzhub/hzhub-admin/apps/web-antd/src/views/erp/stats/index.vue
大壮 c2513849b4 feat: 添加ERP服务和系统服务,完善员工门户功能
## 新增服务模块

### 1. ERP服务 (hzhub-erp)
- 新增独立的ERP数据适配服务
- 支持SQL Server 2008 R2数据源
- 提供动态API配置管理系统
- 包含客户管理、销售数据等业务接口

### 2. 系统服务 (hzhub-system)
- 新增独立的系统管理服务
- 用户、角色、权限、部门、菜单管理
- 租户管理、操作日志、在线用户监控
- 工作流引擎(warm-flow)集成
- 企业微信审批同步功能

### 3. API网关 (hzhub-gateway)
- 新增Spring Cloud Gateway网关服务
- JWT认证、路由分发、限流熔断
- XSS防护、请求日志记录
- 统一入口端口8080

## 后台管理功能增强

### ERP动态API管理
- 新增动态API配置管理界面
- API测试、文档预览、统计监控
- 错误日志查看、缓存管理
- 从数据库表自动导入API配置

### 系统管理增强
- 企业微信配置管理
- 企业微信审批同步配置
- 部门和用户管理优化

## 员工门户功能完善

### 业务页面
- 审批中心:工作流审批、待办任务
- CRM管理:客户关系管理
- 经销商管理:经销商数据展示
- 供应链管理:采购、库存、销售
- BI报表:数据可视化分析
- ERP数据探索:SQL Server数据查询

### 个人中心
- 基本设置:个人信息管理
- 安全设置:密码修改、登录日志
- 锁屏功能:自动锁屏、手动锁屏

### 其他功能
- 标签页管理:多标签页导航
- 页面缓存:keepAlive缓存机制
- 会话超时:自动检测并提示

## 经销商门户

### 页面路由
- 新增经销商管理页面路由
- AI聊天界面完善

## 文档更新

- ERP API数据库初始化指南
- ERP API前端完整实现文档
- ERP API测试和验证指南
- Gateway路由迁移计划
- 项目配置文档更新

## 部署脚本

- 统一启动/停止/重启脚本
- Docker Compose配置优化
- Nginx配置文件更新

## 技术栈

- 后端: Spring Boot 3.5.8, Java 17
- 前端: Vue 3, TypeScript, Element Plus, Vben Admin
- 工作流: warm-flow 1.8.2
- 网关: Spring Cloud Gateway
- 数据库: MySQL 8.0, SQL Server 2008 R2
- 缓存: Redis 7
- 向量库: Weaviate 1.25.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 08:00:19 +00:00

588 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import {
Card,
Col,
Row,
Statistic,
Table,
Tabs,
TabPane,
DatePicker,
Space,
Tag,
Progress,
Empty,
Alert,
Divider,
Select,
Button,
Spin,
message,
Switch,
InputNumber,
Tooltip,
} from 'ant-design-vue';
import { ref, onMounted, computed, watch, onUnmounted } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { apiConfigList, apiConfigStats, apiConfigErrorLog } from '#/api/erp/api';
import type { ErpApiConfigVO } from '#/api/erp/api';
import ErrorDetailModal from './error-detail-modal.vue';
// 时间范围选择
const timeRange = ref<any[]>([]);
const startTime = computed(() => timeRange.value[0] || undefined);
const endTime = computed(() => timeRange.value[1] || undefined);
// API列表用于选择查看统计
const apiList = ref<ErpApiConfigVO[]>([]);
const selectedApiId = ref<number>();
// Loading状态
const loading = ref(false);
// 慢查询阈值(毫秒)
const slowQueryThreshold = ref<number>(1000);
// 自动刷新配置
const autoRefreshEnabled = ref(false);
const refreshInterval = ref<number>(30); // 刷新间隔(秒)
let refreshTimer: number | null = null;
// 错误详情弹窗
const errorDetailVisible = ref(false);
const selectedErrorRecord = ref<any>(null);
// 加载API列表
onMounted(async () => {
try {
const response = await apiConfigList({ pageNum: 1, pageSize: 100 });
apiList.value = response.rows || [];
if (apiList.value.length > 0) {
selectedApiId.value = apiList.value[0].apiId;
await loadStats();
}
} catch (error) {
console.error('加载API列表失败', error);
message.error('加载API列表失败');
}
});
// 统计数据
const statsData = ref({
totalCalls: 0,
successCalls: 0,
errorCalls: 0,
avgResponseTime: 0,
maxResponseTime: 0,
minResponseTime: 0,
errorRate: 0,
slowCalls: 0, // 慢查询数量
cacheEnabled: false, // 是否启用缓存
});
// 加载统计数据
async function loadStats() {
if (!selectedApiId.value) {
message.warning('请先选择API');
return;
}
loading.value = true;
try {
const stats = await apiConfigStats(
selectedApiId.value,
startTime.value,
endTime.value
);
console.log('统计数据:', stats);
// 获取选中的API配置检查是否启用缓存
const selectedApi = apiList.value.find(api => api.apiId === selectedApiId.value);
statsData.value = {
totalCalls: stats.totalCalls || 0,
successCalls: stats.successCalls || 0,
errorCalls: stats.errorCalls || 0,
avgResponseTime: stats.avgResponseTime || 0,
maxResponseTime: stats.maxResponseTime || 0,
minResponseTime: stats.minResponseTime || 0,
errorRate: stats.errorRate || 0,
slowCalls: stats.slowCalls || 0, // TODO: 后端需要添加慢查询统计
cacheEnabled: selectedApi?.enableCache === 1,
};
// 同时加载错误日志
await loadErrorLog();
message.success('统计数据已更新');
} catch (error) {
console.error('加载统计数据失败', error);
message.error('加载统计数据失败');
} finally {
loading.value = false;
}
}
// 错误日志表格配置
const errorLogColumns = [
{ title: '调用时间', dataIndex: 'callTime', width: 180 },
{ title: '响应时间', dataIndex: 'responseTime', width: 100, customRender: ({ text }) => `${text}ms` },
{ title: '客户端IP', dataIndex: 'clientIp', width: 150 },
{ title: '用户ID', dataIndex: 'userId', width: 120 },
{ title: '错误消息', dataIndex: 'errorMessage', ellipsis: true, width: 200 },
{ title: '操作', dataIndex: 'action', width: 80, fixed: 'right' as const },
];
const errorLogData = ref<any[]>([]);
// 加载错误日志
async function loadErrorLog() {
if (!selectedApiId.value) return;
try {
const logs = await apiConfigErrorLog(selectedApiId.value, 50);
console.log('错误日志:', logs);
errorLogData.value = logs || [];
} catch (error) {
console.error('加载错误日志失败', error);
errorLogData.value = [];
}
}
// API选择变化
function handleApiChange(apiId: number) {
selectedApiId.value = apiId;
}
// 查询按钮
function handleQuery() {
loadStats();
}
// 刷新按钮
async function handleRefresh() {
message.info('正在刷新统计数据...');
await loadStats();
}
// 重置时间范围
function handleResetTimeRange() {
timeRange.value = [];
loadStats();
}
// 查看错误详情
function viewErrorDetail(record: any) {
selectedErrorRecord.value = record;
errorDetailVisible.value = true;
}
// 关闭错误详情弹窗
function closeErrorDetail() {
errorDetailVisible.value = false;
selectedErrorRecord.value = null;
}
// 开启/关闭自动刷新
function toggleAutoRefresh(enabled: boolean) {
autoRefreshEnabled.value = enabled;
if (enabled) {
startAutoRefresh();
message.success(`已开启自动刷新,每${refreshInterval.value}秒刷新一次`);
} else {
stopAutoRefresh();
message.info('已关闭自动刷新');
}
}
// 启动自动刷新定时器
function startAutoRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer);
}
refreshTimer = window.setInterval(() => {
if (selectedApiId.value && !loading.value) {
loadStats();
}
}, refreshInterval.value * 1000);
}
// 停止自动刷新
function stopAutoRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
}
// 监听刷新间隔变化
watch(refreshInterval, (newInterval) => {
if (autoRefreshEnabled.value) {
stopAutoRefresh();
startAutoRefresh();
}
});
// 组件卸载时清理定时器
onMounted(() => {
// ... existing code
});
onUnmounted(() => {
stopAutoRefresh();
});
// 当前Tab
const currentTab = ref('overview');
// 汇总统计所有API- 使用computed自动计算
const overallStats = computed(() => {
return {
totalApis: apiList.value.length,
enabledApis: apiList.value.filter(api => api.status === 1).length,
disabledApis: apiList.value.filter(api => api.status === 0).length,
cachedApis: apiList.value.filter(api => api.enableCache === 1).length,
};
});
// 计算性能健康度0-100
function calculateHealthScore(): number {
if (statsData.value.totalCalls === 0) return 100;
// 综合考虑错误率和响应时间
const errorScore = Math.max(0, 100 - statsData.value.errorRate * 10); // 错误率影响每1%扣10分
const timeScore = Math.max(0, 100 - (statsData.value.avgResponseTime / slowQueryThreshold.value) * 20); // 响应时间影响
return Math.round((errorScore + timeScore) / 2);
}
// 获取健康度颜色
function getHealthColor(): string {
const score = calculateHealthScore();
if (score >= 80) return '#3f8600'; // 优秀
if (score >= 60) return '#faad14'; // 一般
return '#cf1322'; // 差
}
// 获取健康度描述
function getHealthDescription(): string {
const score = calculateHealthScore();
if (score >= 80) return '性能优秀,系统运行正常';
if (score >= 60) return '性能一般,建议优化';
if (score >= 40) return '性能较差,需要优化';
return '性能很差,紧急优化';
}
// 计算响应时间评分0-100
function calculateResponseTimeScore(): number {
if (statsData.value.maxResponseTime === 0) return 100;
const avgRatio = statsData.value.avgResponseTime / slowQueryThreshold.value;
return Math.max(0, Math.min(100, 100 - avgRatio * 50));
}
// 获取响应时间颜色
function getResponseTimeColor(): string {
const score = calculateResponseTimeScore();
if (score >= 80) return '#3f8600';
if (score >= 60) return '#1890ff';
if (score >= 40) return '#faad14';
return '#cf1322';
}
</script>
<template>
<Page :auto-content-height="true">
<!-- 上方汇总统计卡片 -->
<Card title="API概览" size="small" class="mb-4">
<Row :gutter="16">
<Col :span="6">
<Statistic title="API总数" :value="overallStats.totalApis" />
</Col>
<Col :span="6">
<Statistic title="启用API" :value="overallStats.enabledApis" :value-style="{ color: '#3f8600' }" />
</Col>
<Col :span="6">
<Statistic title="禁用API" :value="overallStats.disabledApis" :value-style="{ color: '#cf1322' }" />
</Col>
<Col :span="6">
<Statistic title="启用缓存" :value="overallStats.cachedApis" :value-style="{ color: '#1890ff' }" />
</Col>
</Row>
</Card>
<!-- 下方详细统计 -->
<Card size="small">
<!-- API选择和时间范围 -->
<div class="filter-section mb-4">
<Space wrap>
<span>选择API</span>
<Select
v-model:value="selectedApiId"
style="width: 300px"
placeholder="请选择API"
>
<Select.Option v-for="api in apiList" :key="api.apiId" :value="api.apiId">
{{ api.apiName }} ({{ api.apiPath }})
</Select.Option>
</Select>
<span>时间范围</span>
<DatePicker.RangePicker
v-model:value="timeRange"
style="width: 240px"
/>
<Button type="primary" :loading="loading" @click="handleQuery">
查询
</Button>
<Button :loading="loading" @click="handleRefresh">
刷新
</Button>
<Button @click="handleResetTimeRange">
重置
</Button>
<Divider type="vertical" />
<Tooltip title="开启后将按设定的间隔自动刷新统计数据">
<Space>
<span>自动刷新</span>
<Switch v-model:checked="autoRefreshEnabled" @change="toggleAutoRefresh" />
</Space>
</Tooltip>
<Tooltip title="自动刷新的时间间隔(秒)">
<Space v-if="autoRefreshEnabled">
<span>间隔</span>
<InputNumber
v-model:value="refreshInterval"
:min="10"
:max="300"
:step="10"
style="width: 80px"
/>
<span></span>
</Space>
</Tooltip>
<Tooltip title="响应时间超过此阈值的调用视为慢查询">
<Space>
<span>慢查询阈值</span>
<InputNumber
v-model:value="slowQueryThreshold"
:min="100"
:max="10000"
:step="100"
style="width: 100px"
/>
<span>ms</span>
</Space>
</Tooltip>
</Space>
</div>
<!-- Loading提示 -->
<Spin :spinning="loading" tip="正在加载统计数据...">
<!-- 统计卡片 -->
<Row :gutter="16" class="mb-4">
<Col :span="3">
<Statistic title="总调用次数" :value="statsData.totalCalls" />
</Col>
<Col :span="3">
<Statistic title="成功次数" :value="statsData.successCalls" :value-style="{ color: '#3f8600' }" />
</Col>
<Col :span="3">
<Statistic title="错误次数" :value="statsData.errorCalls" :value-style="{ color: '#cf1322' }" />
</Col>
<Col :span="3">
<Statistic title="错误率" :value="statsData.errorRate.toFixed(2)" suffix="%" :value-style="{ color: statsData.errorRate > 5 ? '#cf1322' : '#3f8600' }" />
</Col>
<Col :span="3">
<Statistic title="平均响应时间" :value="statsData.avgResponseTime" suffix="ms" :value-style="{ color: statsData.avgResponseTime > slowQueryThreshold ? '#faad14' : '#1890ff' }" />
</Col>
<Col :span="3">
<Statistic title="最大响应时间" :value="statsData.maxResponseTime" suffix="ms" :value-style="{ color: statsData.maxResponseTime > slowQueryThreshold ? '#cf1322' : '#1890ff' }" />
</Col>
<Col :span="3">
<Statistic title="最小响应时间" :value="statsData.minResponseTime" suffix="ms" />
</Col>
<Col :span="3">
<Statistic title="慢查询次数" :value="statsData.slowCalls" :value-style="{ color: '#faad14' }">
<template #suffix>
<Tooltip title="响应时间超过阈值的调用">
<Tag color="warning">>{{ slowQueryThreshold }}ms</Tag>
</Tooltip>
</template>
</Statistic>
</Col>
</Row>
<!-- 性能指标卡片 -->
<Row :gutter="16" class="mb-4">
<Col :span="6">
<Card size="small" title="性能健康度" :bordered="false">
<Progress
:percent="calculateHealthScore()"
:stroke-color="getHealthColor()"
trail-color="#f0f0f0"
/>
<div class="metric-desc">{{ getHealthDescription() }}</div>
</Card>
</Col>
<Col :span="6">
<Card size="small" title="成功率" :bordered="false">
<Progress
:percent="100 - statsData.errorRate"
:stroke-color="{ '0%': '#87d068', '100%': '#3f8600' }"
trail-color="#f0f0f0"
/>
<div class="metric-desc">成功 {{ statsData.successCalls }} / 总计 {{ statsData.totalCalls }}</div>
</Card>
</Col>
<Col :span="6">
<Card size="small" title="响应时间分布" :bordered="false">
<Progress
:percent="calculateResponseTimeScore()"
:stroke-color="getResponseTimeColor()"
trail-color="#f0f0f0"
/>
<div class="metric-desc">平均 {{ statsData.avgResponseTime }}ms / 最大 {{ statsData.maxResponseTime }}ms</div>
</Card>
</Col>
<Col :span="6">
<Card size="small" title="缓存状态" :bordered="false">
<Statistic :value="statsData.cacheEnabled ? '已启用' : '未启用'" :value-style="{ color: statsData.cacheEnabled ? '#3f8600' : '#8c8c8c' }" />
<div class="metric-desc">缓存可提升查询性能</div>
</Card>
</Col>
</Row>
<!-- 错误率进度条 -->
<div class="mb-4">
<Progress
:percent="100 - statsData.errorRate"
:stroke-color="{ '0%': '#3f8600', '100%': '#87d068' }"
:trail-color="statsData.errorRate > 10 ? '#cf1322' : '#f0f0f0'"
/>
</div>
<!-- Tabs切换 -->
<Tabs v-model:activeKey="currentTab">
<TabPane key="overview" tab="统计概览">
<Empty v-if="!selectedApiId" description="请选择API查看统计" />
<Alert v-else-if="statsData.totalCalls === 0" type="info">
<template #message>
<div>
<strong>该API暂无调用记录</strong>
<br />
<small>请先调用动态API触发统计记录然后刷新监控页面查看数据</small>
</div>
</template>
</Alert>
<div v-else>
<Alert type="success" message="统计功能已完善包含性能健康度成功率响应时间分布等多维度分析" />
<!-- 性能建议 -->
<Card size="small" title="性能优化建议" class="mt-4" v-if="statsData.totalCalls > 0">
<div v-if="statsData.errorRate > 5">
<Tag color="error">高错误率</Tag>
<span>错误率超过5%建议检查API配置和SQL语句</span>
</div>
<div v-if="statsData.avgResponseTime > slowQueryThreshold">
<Tag color="warning">慢查询较多</Tag>
<span>平均响应时间超过阈值建议优化SQL查询或启用缓存</span>
</div>
<div v-if="!statsData.cacheEnabled && statsData.totalCalls > 10">
<Tag color="info">未启用缓存</Tag>
<span>调用次数较多但未启用缓存,建议启用缓存以提升性能</span>
</div>
<div v-if="statsData.errorRate <= 1 && statsData.avgResponseTime <= slowQueryThreshold / 2">
<Tag color="success">性能优秀</Tag>
<span>API运行状态良好继续保持</span>
</div>
</Card>
</div>
</TabPane>
<TabPane key="errorLog" tab="错误日志">
<Empty v-if="errorLogData.length === 0" description="暂无错误日志" />
<div v-else>
<Alert type="warning" class="mb-2">
<template #message>
<span>最近发现 {{ errorLogData.length }} 条错误记录,建议及时处理</span>
</template>
</Alert>
<Table
:columns="errorLogColumns"
:data-source="errorLogData"
:pagination="{ pageSize: 20 }"
size="small"
:scroll="{ x: 1000 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'callTime'">
{{ record.callTime }}
</template>
<template v-if="column.dataIndex === 'errorMessage'">
<Tooltip :title="record.errorMessage">
<Tag color="error">{{ record.errorMessage.substring(0, 50) }}...</Tag>
</Tooltip>
</template>
<template v-if="column.dataIndex === 'action'">
<Button type="link" size="small" @click="viewErrorDetail(record)">
详情
</Button>
</template>
</template>
</Table>
</div>
</TabPane>
</Tabs>
</Spin>
</Card>
<!-- 错误详情弹窗 -->
<ErrorDetailModal
v-model:visible="errorDetailVisible"
:record="selectedErrorRecord"
@close="closeErrorDetail"
/>
</Page>
</template>
<style scoped>
.filter-section {
padding: 16px;
background: #fafafa;
border-radius: 4px;
}
.mb-4 {
margin-bottom: 16px;
}
.mb-2 {
margin-bottom: 8px;
}
.mt-4 {
margin-top: 16px;
}
.metric-desc {
font-size: 12px;
color: #8c8c8c;
margin-top: 8px;
}
</style>