fix: 修复数据获取及展示问题,优化字段命名与UI显示

This commit is contained in:
Ray.Hao
2026-04-05 16:20:36 +08:00
parent 9654913a21
commit 00d06de84f
9 changed files with 49 additions and 52 deletions

View File

@@ -181,8 +181,7 @@ export default defineMock([
"2024-07-07", "2024-07-07",
], ],
pvList: [1751, 5168, 4882, 5301, 4721, 4885, 1901, 1003], pvList: [1751, 5168, 4882, 5301, 4721, 4885, 1901, 1003],
uvList: null, uvList: [207, 566, 565, 631, 579, 496, 222, 152],
ipList: [207, 566, 565, 631, 579, 496, 222, 152],
}, },
msg: "一切ok", msg: "一切ok",
}, },

3
pnpm-lock.yaml generated
View File

@@ -205,6 +205,9 @@ devDependencies:
vite-plugin-mock-dev-server: vite-plugin-mock-dev-server:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1(vite@8.0.3) version: 2.1.1(vite@8.0.3)
vue-eslint-parser:
specifier: ^10.4.0
version: 10.4.0(eslint@10.1.0)
vue-tsc: vue-tsc:
specifier: ^3.2.5 specifier: ^3.2.5
version: 3.2.6(typescript@5.9.3) version: 3.2.6(typescript@5.9.3)

View File

@@ -36,13 +36,10 @@ const AuthAPI = {
/** 刷新 token 接口*/ /** 刷新 token 接口*/
refreshToken(refreshToken: string) { refreshToken(refreshToken: string) {
const params = new URLSearchParams();
params.append("refresh_token", refreshToken);
return request<any, LoginResponse>({ return request<any, LoginResponse>({
url: `${AUTH_BASE_URL}/refresh-token`, url: `${AUTH_BASE_URL}/refresh-token`,
method: "post", method: "post",
data: params, params: { refreshToken },
headers: { headers: {
Authorization: "no-auth", Authorization: "no-auth",
}, },

View File

@@ -4,7 +4,7 @@ import type {
LogItem, LogItem,
VisitTrendQueryParams, VisitTrendQueryParams,
VisitTrendDetail, VisitTrendDetail,
VisitStatsDetail, VisitOverviewDetail,
} from "./types"; } from "./types";
const LOG_BASE_URL = "/api/v1/logs"; const LOG_BASE_URL = "/api/v1/logs";
@@ -30,7 +30,7 @@ const LogAPI = {
/** 获取访问概览统计 */ /** 获取访问概览统计 */
getVisitOverview() { getVisitOverview() {
return request<any, VisitStatsDetail>({ return request<any, VisitOverviewDetail>({
url: `${LOG_BASE_URL}/analytics/overview`, url: `${LOG_BASE_URL}/analytics/overview`,
method: "get", method: "get",
}); });

View File

@@ -68,12 +68,10 @@ export interface VisitTrendDetail {
pvList: number[]; pvList: number[];
/** 访客数(UV)列表 */ /** 访客数(UV)列表 */
uvList: number[]; uvList: number[];
/** IP数列表 */
ipList: number[];
} }
/** 访问量统计视图对象 */ /** 访问概览视图对象 */
export interface VisitStatsDetail { export interface VisitOverviewDetail {
/** 今日独立访客数(UV) */ /** 今日独立访客数(UV) */
todayUvCount: number; todayUvCount: number;
/** 累计独立访客数(UV) */ /** 累计独立访客数(UV) */

View File

@@ -132,6 +132,8 @@ function createSseConnection(options: UseSseOptions = {}) {
reader = r; reader = r;
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ""; let buffer = "";
let currentEvent = "message";
let currentData = "";
// SSE 文本协议解析:event / data / 空行分隔 // SSE 文本协议解析:event / data / 空行分隔
const processChunk = ({ const processChunk = ({
@@ -148,15 +150,13 @@ function createSseConnection(options: UseSseOptions = {}) {
const lines = buffer.split("\n"); const lines = buffer.split("\n");
buffer = lines.pop() || ""; buffer = lines.pop() || "";
let currentEvent = "message";
let currentData = "";
for (const line of lines) { for (const line of lines) {
if (line.startsWith(":")) continue; if (line.startsWith(":")) continue;
if (line.startsWith("event:")) { if (line.startsWith("event:")) {
currentEvent = line.slice(6).trim(); currentEvent = line.slice(6).trim();
} else if (line.startsWith("data:")) { } else if (line.startsWith("data:")) {
currentData = line.slice(5).trim(); const dataLine = line.slice(5).trim();
currentData = currentData ? `${currentData}\n${dataLine}` : dataLine;
} else if (line === "") { } else if (line === "") {
if (currentData) { if (currentData) {
const handlers = eventHandlers.get(currentEvent); const handlers = eventHandlers.get(currentEvent);

View File

@@ -159,7 +159,7 @@
<!-- 访客量UV) --> <!-- 访客量UV) -->
<el-col :span="8" :xs="24" class="mb-xs-3"> <el-col :span="8" :xs="24" class="mb-xs-3">
<el-skeleton :loading="visitStatsLoading" :rows="5" animated> <el-skeleton :loading="visitOverviewLoading" :rows="5" animated>
<template #template> <template #template>
<el-card> <el-card>
<template #header> <template #header>
@@ -179,7 +179,7 @@
</div> </div>
</el-card> </el-card>
</template> </template>
<template v-if="!visitStatsLoading"> <template v-if="!visitOverviewLoading">
<el-card <el-card
shadow="never" shadow="never"
class="h-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg" class="h-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
@@ -200,16 +200,16 @@
</span> </span>
<span <span
v-if="uvGrowthText !== null" v-if="uvGrowthText !== null"
:class="['text-xs', computeGrowthRateClass(visitStatsData.uvGrowthRate)]" :class="['text-xs', computeGrowthRateClass(visitOverviewData.uvGrowthRate)]"
> >
<el-icon <el-icon
v-if=" v-if="
visitStatsData.uvGrowthRate !== undefined && visitOverviewData.uvGrowthRate !== undefined &&
visitStatsData.uvGrowthRate !== null visitOverviewData.uvGrowthRate !== null
" "
> >
<Top v-if="visitStatsData.uvGrowthRate > 0" /> <Top v-if="visitOverviewData.uvGrowthRate > 0" />
<Bottom v-else-if="visitStatsData.uvGrowthRate < 0" /> <Bottom v-else-if="visitOverviewData.uvGrowthRate < 0" />
</el-icon> </el-icon>
{{ uvGrowthText }} {{ uvGrowthText }}
</span> </span>
@@ -227,7 +227,7 @@
<!-- 浏览量(PV) --> <!-- 浏览量(PV) -->
<el-col :span="8" :xs="24"> <el-col :span="8" :xs="24">
<el-skeleton :loading="visitStatsLoading" :rows="5" animated> <el-skeleton :loading="visitOverviewLoading" :rows="5" animated>
<template #template> <template #template>
<el-card> <el-card>
<template #header> <template #header>
@@ -247,7 +247,7 @@
</div> </div>
</el-card> </el-card>
</template> </template>
<template v-if="!visitStatsLoading"> <template v-if="!visitOverviewLoading">
<el-card <el-card
shadow="never" shadow="never"
class="h-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg" class="h-full transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
@@ -268,16 +268,16 @@
</span> </span>
<span <span
v-if="pvGrowthText !== null" v-if="pvGrowthText !== null"
:class="['text-xs', computeGrowthRateClass(visitStatsData.pvGrowthRate)]" :class="['text-xs', computeGrowthRateClass(visitOverviewData.pvGrowthRate)]"
> >
<el-icon <el-icon
v-if=" v-if="
visitStatsData.pvGrowthRate !== undefined && visitOverviewData.pvGrowthRate !== undefined &&
visitStatsData.pvGrowthRate !== null visitOverviewData.pvGrowthRate !== null
" "
> >
<Top v-if="visitStatsData.pvGrowthRate > 0" /> <Top v-if="visitOverviewData.pvGrowthRate > 0" />
<Bottom v-else-if="visitStatsData.pvGrowthRate < 0" /> <Bottom v-else-if="visitOverviewData.pvGrowthRate < 0" />
</el-icon> </el-icon>
{{ pvGrowthText }} {{ pvGrowthText }}
</span> </span>
@@ -383,7 +383,7 @@ import { dayjs } from "element-plus";
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import LogAPI from "@/api/system/log"; import LogAPI from "@/api/system/log";
import type { VisitStatsDetail, VisitTrendDetail } from "@/api/system/log"; import type { VisitOverviewDetail, VisitTrendDetail } from "@/api/system/log";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { formatGrowthRate } from "@/utils"; import { formatGrowthRate } from "@/utils";
import { useTransition, useDateFormat } from "@vueuse/core"; import { useTransition, useDateFormat } from "@vueuse/core";
@@ -434,9 +434,9 @@ const greetings = computed(() => {
}); });
// 访客统计数据加载状态 // 访客统计数据加载状态
const visitStatsLoading = ref(true); const visitOverviewLoading = ref(true);
// 访客统计数据 // 访客统计数据
const visitStatsData = ref<VisitStatsDetail>({ const visitOverviewData = ref<VisitOverviewDetail>({
todayUvCount: 0, todayUvCount: 0,
uvGrowthRate: 0, uvGrowthRate: 0,
totalUvCount: 0, totalUvCount: 0,
@@ -447,27 +447,27 @@ const visitStatsData = ref<VisitStatsDetail>({
const uvGrowthText = computed(() => { const uvGrowthText = computed(() => {
if ( if (
visitStatsData.value.uvGrowthRate === undefined || visitOverviewData.value.uvGrowthRate === undefined ||
visitStatsData.value.uvGrowthRate === null visitOverviewData.value.uvGrowthRate === null
) { ) {
return "--"; return "--";
} }
return formatGrowthRate(visitStatsData.value.uvGrowthRate); return formatGrowthRate(visitOverviewData.value.uvGrowthRate);
}); });
const pvGrowthText = computed(() => { const pvGrowthText = computed(() => {
if ( if (
visitStatsData.value.pvGrowthRate === undefined || visitOverviewData.value.pvGrowthRate === undefined ||
visitStatsData.value.pvGrowthRate === null visitOverviewData.value.pvGrowthRate === null
) { ) {
return "--"; return "--";
} }
return formatGrowthRate(visitStatsData.value.pvGrowthRate); return formatGrowthRate(visitOverviewData.value.pvGrowthRate);
}); });
// 数字过渡动画 // 数字过渡动画
const transitionUvCount = useTransition( const transitionUvCount = useTransition(
computed(() => visitStatsData.value.todayUvCount), computed(() => visitOverviewData.value.todayUvCount),
{ {
duration: 1000, duration: 1000,
transition: [0.25, 0.1, 0.25, 1.0], // CSS cubic-bezier transition: [0.25, 0.1, 0.25, 1.0], // CSS cubic-bezier
@@ -475,7 +475,7 @@ const transitionUvCount = useTransition(
); );
const transitionTotalUvCount = useTransition( const transitionTotalUvCount = useTransition(
computed(() => visitStatsData.value.totalUvCount), computed(() => visitOverviewData.value.totalUvCount),
{ {
duration: 1200, duration: 1200,
transition: [0.25, 0.1, 0.25, 1.0], transition: [0.25, 0.1, 0.25, 1.0],
@@ -483,7 +483,7 @@ const transitionTotalUvCount = useTransition(
); );
const transitionPvCount = useTransition( const transitionPvCount = useTransition(
computed(() => visitStatsData.value.todayPvCount), computed(() => visitOverviewData.value.todayPvCount),
{ {
duration: 1000, duration: 1000,
transition: [0.25, 0.1, 0.25, 1.0], transition: [0.25, 0.1, 0.25, 1.0],
@@ -491,7 +491,7 @@ const transitionPvCount = useTransition(
); );
const transitionTotalPvCount = useTransition( const transitionTotalPvCount = useTransition(
computed(() => visitStatsData.value.totalPvCount), computed(() => visitOverviewData.value.totalPvCount),
{ {
duration: 1200, duration: 1200,
transition: [0.25, 0.1, 0.25, 1.0], transition: [0.25, 0.1, 0.25, 1.0],
@@ -520,13 +520,13 @@ const visitTrendChartOptions = ref();
/** /**
* 获取访客统计数据 * 获取访客统计数据
*/ */
const fetchVisitStatsData = () => { const fetchVisitOverviewData = () => {
LogAPI.getVisitOverview() LogAPI.getVisitOverview()
.then((data) => { .then((data) => {
visitStatsData.value = data; visitOverviewData.value = data;
}) })
.finally(() => { .finally(() => {
visitStatsLoading.value = false; visitOverviewLoading.value = false;
}); });
}; };
@@ -558,7 +558,7 @@ const updateVisitTrendChartOptions = (data: VisitTrendDetail) => {
trigger: "axis", trigger: "axis",
}, },
legend: { legend: {
data: ["浏览量(PV)", "访客量UV)"], data: ["浏览量(PV)", "访客数(UV)"],
bottom: 0, bottom: 0,
}, },
grid: { grid: {
@@ -597,9 +597,9 @@ const updateVisitTrendChartOptions = (data: VisitTrendDetail) => {
}, },
}, },
{ {
name: "访客量UV)", name: "访客量(UV)",
type: "line", type: "line",
data: data.ipList, data: data.uvList,
areaStyle: { areaStyle: {
color: "rgba(103, 194, 58, 0.1)", color: "rgba(103, 194, 58, 0.1)",
}, },
@@ -644,7 +644,7 @@ watch(
// 组件挂载后加载访客统计数据和通知公告数据 // 组件挂载后加载访客统计数据和通知公告数据
onMounted(() => { onMounted(() => {
fetchVisitStatsData(); fetchVisitOverviewData();
}); });
</script> </script>

View File

@@ -245,7 +245,7 @@ function handleSelectionChange(selection: DictItem[]): void {
function openDialog(row?: DictItem): void { function openDialog(row?: DictItem): void {
resetForm(); resetForm();
dialogState.visible = true; dialogState.visible = true;
dialogState.title = row ? "编辑字典值" : "新增字典值"; dialogState.title = row ? "编辑字典值" : "新增字典项";
if (row?.id) { if (row?.id) {
DictAPI.getDictItemFormData(dictCode.value, row.id).then((data) => { DictAPI.getDictItemFormData(dictCode.value, row.id).then((data) => {

View File

@@ -94,7 +94,7 @@
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
> >
<el-table-column type="selection" width="50" align="center" /> <el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="username" /> <el-table-column label="用户名" prop="username" min-width="120" show-overflow-tooltip />
<el-table-column label="昵称" width="200" align="center" prop="nickname" /> <el-table-column label="昵称" width="200" align="center" prop="nickname" />
<el-table-column label="性别" width="100" align="center"> <el-table-column label="性别" width="100" align="center">
<template #default="scope"> <template #default="scope">