feat: 添加首页数据展示

This commit is contained in:
皓月归尘 2025-02-18 04:11:56 +08:00
parent d8c2d1db4b
commit f039e020e1
3 changed files with 363 additions and 135 deletions

View File

@ -123,3 +123,34 @@ export const postUploadAvatarAPI = (id: string, data: { file: Blob }) => {
data
});
};
/**用户查询统计结果 */
interface UserQueryStatistics {
/**今日统计 */
today_count: number;
/**当月统计 */
this_month_count: number;
/**上月统计 */
last_month_count: number;
/**最近14天内成功查询统计 */
before_14day_count_success: {
/**日期 */
date: string;
/**查询次数 */
count: number;
}[];
/**最近14天内失败查询统计 */
before_14day_count_fail: {
/**日期 */
date: string;
/**查询次数 */
count: number;
}[];
}
/**
*
*/
export const getUserQueryStatisticsAPI = () => {
return http.request<UserQueryStatistics>("get", "/api/user/statistics");
};

View File

@ -3,11 +3,15 @@ import { useDark, useECharts } from "@pureadmin/utils";
import { type PropType, ref, computed, watch, nextTick } from "vue";
const props = defineProps({
requireData: {
dates: {
type: Array as PropType<Array<string>>,
default: () => []
},
successData: {
type: Array as PropType<Array<number>>,
default: () => []
},
questionData: {
failData: {
type: Array as PropType<Array<number>>,
default: () => []
}
@ -28,70 +32,89 @@ watch(
await nextTick(); // DOM
setOptions({
container: ".bar-card",
color: ["#41b6ff", "#e85f33"],
color: ["#28a745", "#dc3545"], // 绿
title: {
text: "近14天内查询统计",
left: "center", //
top: 10, //
textStyle: {
fontSize: 16,
fontWeight: "bold",
color: "#333"
}
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "none"
type: "line"
}
},
grid: {
top: "20px",
top: "60px", //
left: "50px",
right: 0
right: "20px",
bottom: "50px"
},
legend: {
data: ["查询数量", "匹配数量"],
data: ["成功查询", "失败查询"],
textStyle: {
color: "#606266",
fontSize: "0.875rem"
},
bottom: 0
top: 10, //
right: 20 //
},
xAxis: [
{
xAxis: {
type: "category",
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
data: props.dates, //
axisLabel: {
fontSize: "0.875rem"
fontSize: "0.875rem",
rotate: 30 //
},
axisPointer: {
type: "shadow"
}
}
],
yAxis: [
{
},
yAxis: {
type: "value",
axisLabel: {
fontSize: "0.875rem"
},
splitLine: {
show: false // 线
show: true,
lineStyle: {
type: "dashed"
}
// name: ": "
}
],
},
series: [
{
name: "查询数量",
type: "bar",
barWidth: 45,
name: "成功查询",
type: "line",
smooth: true,
itemStyle: {
color: "#41b6ff",
borderRadius: [10, 10, 0, 0]
color: "#28a745" // 绿
},
data: props.requireData
label: {
show: true, // 线
position: "top",
color: "#28a745"
},
data: props.successData
},
{
name: "匹配数量",
type: "bar",
barWidth: 45,
name: "失败查询",
type: "line",
smooth: true,
itemStyle: {
color: "#e86033ce",
borderRadius: [10, 10, 0, 0]
color: "#dc3545" //
},
data: props.questionData
label: {
show: true, // 线
position: "top",
color: "#dc3545"
},
data: props.failData
}
]
});
@ -104,5 +127,5 @@ watch(
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 550px" />
<div ref="chartRef" style="width: 100%; height: 560px" />
</template>

View File

@ -2,88 +2,28 @@
defineOptions({
name: "Welcome"
});
import { ref, markRaw } from "vue";
import { ref, markRaw, reactive, onMounted } from "vue";
import dayjs from "dayjs";
import ReCol from "@/components/ReCol";
import { useRouter } from "vue-router";
import ChartBar from "./components/ChartBar.vue";
import { useRenderFlicker } from "@/components/ReFlicker";
import Segmented, { type OptionsType } from "@/components/ReSegmented";
import { useDark, cloneDeep, randomGradient } from "@pureadmin/utils";
import { useDark, cloneDeep, randomGradient, isString } from "@pureadmin/utils";
import GroupLine from "@iconify-icons/ri/group-line";
import GroupLine from "@iconify-icons/ri/stack-line";
import Question from "@iconify-icons/ri/question-answer-line";
import CheckLine from "@iconify-icons/ri/chat-check-line";
import Smile from "@iconify-icons/ri/star-smile-line";
import { getUserQueryStatisticsAPI } from "@/api/user";
import { QueryCodeLogInfo } from "types/code";
import { getCodeLogListAPI } from "@/api/code";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { $t } from "@/plugins/i18n";
const router = useRouter();
const { isDark } = useDark();
const getRandomIntBetween = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
let curWeek = ref(1); // 01
const optionsBasis: Array<OptionsType> = [
{
label: "上周"
},
{
label: "本周"
}
];
const days = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
/** 需求人数、提问数量、解决数量、用户满意度 */
const chartData = [
{
icon: GroupLine,
bgColor: "#effaff",
color: "#41b6ff",
duration: 2200,
name: "需求人数",
value: 36000,
percent: "+88%",
data: [2101, 5288, 4239, 4962, 6752, 5208, 7450] // 线
},
{
icon: Question,
bgColor: "#fff5f4",
color: "#e85f33",
duration: 1600,
name: "提问数量",
value: 16580,
percent: "+70%",
data: [2216, 1148, 1255, 788, 4821, 1973, 4379]
},
{
icon: CheckLine,
bgColor: "#eff8f4",
color: "#26ce83",
duration: 1500,
name: "解决数量",
value: 16499,
percent: "+99%",
data: [861, 1002, 3195, 1715, 3666, 2415, 3645]
},
{
icon: Smile,
bgColor: "#f6f4fe",
color: "#7846e5",
duration: 100,
name: "用户满意度",
value: 100,
percent: "+100%",
data: [100]
}
];
/** 分析概览 */
const barChartData = [
{
requireData: [2101, 5288, 4239, 4962, 6752, 5208, 7450],
questionData: [2216, 1148, 1255, 1788, 4821, 1973, 4379]
},
{
requireData: [2101, 3280, 4400, 4962, 5752, 6889, 7600],
questionData: [2116, 3148, 3255, 3788, 4821, 4970, 5390]
}
];
/** 数据统计 */
const tableData = Array.from({ length: 30 }).map((_, index) => {
return {
@ -95,15 +35,217 @@ const tableData = Array.from({ length: 30 }).map((_, index) => {
date: dayjs().subtract(index, "day").format("YYYY-MM-DD")
};
});
/** 最新动态 */
const latestNewsData = cloneDeep(tableData)
.slice(0, 14)
.map((item, index) => {
return Object.assign(item, {
date: `${dayjs().subtract(index, "day").format("YYYY-MM-DD")} ${
days[dayjs().subtract(index, "day").day()]
}`
/**用户统计 */
const userStatisticsData = reactive({
/**今日统计 */
today_count: 0,
/**当月统计 */
this_month_count: 0,
/**上月统计 */
last_month_count: 0,
/**最近14天内成功查询统计 */
before_14day_count_success: [
{
/**日期 */
date: "",
/**查询次数 */
count: 0
}
],
/**最近14天内失败查询统计 */
before_14day_count_fail: [
{
/**日期 */
date: "",
/**查询次数 */
count: 0
}
]
});
/**日期列表 */
const dates = ref<string[]>([]);
/**成功查询列表 */
const successData = ref<number[]>([]);
/**失败查询列表 */
const failData = ref<number[]>([]);
/**获取用户统计信息 */
const getUserStatistics = async () => {
const res = await getUserQueryStatisticsAPI();
if (res.success) {
Object.assign(userStatisticsData, res.data);
dates.value = res.data.before_14day_count_success.map(item => item.date);
successData.value = res.data.before_14day_count_success.map(
item => item.count
);
failData.value = res.data.before_14day_count_fail.map(item => item.count);
}
};
/**活动列表 */
const codeLogList = ref<QueryCodeLogInfo[]>([]);
/**获取日历活动 */
const getActivityCalendar = async () => {
let [startTime, endTime] = date.value;
const res = await getCodeLogListAPI({
page: 1,
pageSize: 99999999999,
startTime: startTime,
endTime: endTime
});
if (res.success) {
codeLogList.value = res.data.result;
codeLogList.value = cloneDeep(codeLogList.value)
.map((item: QueryCodeLogInfo) => {
return Object.assign(item, {
operation_time: `${dayjs(item.operation_time).format("YYYY年MM月DD日 HH:mm:ss")}`
});
})
.sort((a: QueryCodeLogInfo, b: QueryCodeLogInfo) => {
return b.operation_time < a.operation_time ? -1 : 1;
});
}
};
/**最近时间选择 */
const shortcuts = [
{
text: "上周",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
}
},
{
text: "上个月",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
}
},
{
text: "三个月前",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
}
},
{
text: "半年前",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 180);
return [start, end];
}
},
{
text: "一年前",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 365);
return [start, end];
}
},
{
text: "最近一周",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 3);
end.setTime(end.getTime() + 3600 * 1000 * 24 * 3);
return [start, end];
}
},
{
text: "最近一个月",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 15);
end.setTime(end.getTime() + 3600 * 1000 * 24 * 15);
return [start, end];
}
},
{
text: "最近三个月",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 45);
end.setTime(end.getTime() + 3600 * 1000 * 24 * 45);
return [start, end];
}
},
{
text: "最近半年",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
end.setTime(end.getTime() + 3600 * 1000 * 24 * 90);
return [start, end];
}
},
{
text: "最近一年",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 180);
end.setTime(end.getTime() + 3600 * 1000 * 24 * 180);
return [start, end];
}
}
];
/**日期选择器时间 */
const date = ref();
/**日期选择器变化 */
const onChange = async (e: any) => {
await getActivityCalendar();
};
/**处理详情 */
const onClickDetails = (row: QueryCodeLogInfo) => {
let params = { detailsId: row.id };
Object.keys(params).forEach(param => {
if (!isString(params[param])) {
params[param] = params[param].toString();
}
});
useMultiTagsStoreHook().handleTags("push", {
path: "/code/log/details/:detailsId",
name: "QueryCodedetails",
params: params,
meta: {
title: $t("menus:QueryCodedetails")
},
dynamicLevel: 1
});
router.push({
name: "QueryCodedetails",
params: params
});
};
/**挂载执行 */
onMounted(async () => {
//
const currentDate = new Date();
//
let threeDaysAgo = new Date(currentDate);
threeDaysAgo.setDate(currentDate.getDate() - 1);
let threeDaysAgoTimestamp = threeDaysAgo.getTime();
date.value = [threeDaysAgoTimestamp, currentDate.getTime()];
await getUserStatistics();
await getActivityCalendar();
});
</script>
@ -147,7 +289,9 @@ const latestNewsData = cloneDeep(tableData)
</div>
<div class="flex justify-center items-start mt-3">
<div class="w-1/2 text-center">
<h2 class="font-bold text-blue-500">688780</h2>
<h2 class="font-bold text-blue-500">
{{ userStatisticsData.today_count }}
</h2>
</div>
</div>
</el-card>
@ -185,7 +329,9 @@ const latestNewsData = cloneDeep(tableData)
</div>
<div class="flex justify-center items-start mt-3">
<div class="w-1/2 text-center">
<h2 class="font-bold text-blue-500">79999897098</h2>
<h2 class="font-bold text-blue-500">
{{ userStatisticsData.this_month_count }}
</h2>
</div>
</div>
</el-card>
@ -227,7 +373,9 @@ const latestNewsData = cloneDeep(tableData)
</div>
<div class="flex justify-center items-start mt-3">
<div class="w-1/2 text-center">
<h2 class="font-bold text-blue-500">7898067690</h2>
<h2 class="font-bold text-blue-500">
{{ userStatisticsData.last_month_count }}
</h2>
</div>
</div>
</el-card>
@ -253,12 +401,12 @@ const latestNewsData = cloneDeep(tableData)
<el-card class="bar-card" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">数据概览</span>
<Segmented v-model="curWeek" :options="optionsBasis" />
</div>
<div class="flex justify-between items-start mt-3">
<ChartBar
:requireData="barChartData[curWeek].requireData"
:questionData="barChartData[curWeek].questionData"
:dates="dates"
:successData="successData"
:failData="failData"
/>
</div>
</el-card>
@ -281,14 +429,29 @@ const latestNewsData = cloneDeep(tableData)
}"
>
<el-card shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">查询动态</span>
<div class="flex justify-between flex-wrap">
<el-date-picker
v-model="date"
type="daterange"
unlink-panels
format="YYYY年MM月DD日"
range-separator="到"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="x"
:shortcuts="shortcuts"
@change="onChange"
/>
<div class="text-md font-medium mt-3 text-center w-full">
查询动态
</div>
<el-scrollbar max-height="558" class="mt-3">
<el-timeline>
</div>
<el-scrollbar max-height="500" class="mt-3 !h-[518px]">
<el-timeline v-if="codeLogList.length">
<el-timeline-item
v-for="(item, index) in latestNewsData"
v-for="(item, index) in codeLogList"
:key="index"
class="ml-2"
center
placement="top"
:icon="
@ -300,15 +463,26 @@ const latestNewsData = cloneDeep(tableData)
})
)
"
:timestamp="item.date"
:timestamp="
item.operation_time as unknown as unknown as unknown as string
"
@click="onClickDetails(item)"
>
<p class="text-text_color_regular text-sm">
<el-tag type="primary" effect="plain">
{{ item.department_name }}
</el-tag>
<el-tag type="warning" effect="plain">
{{ item.operator_nickname }}
</el-tag>
{{
`查询 ${item.requiredNumber} 条数据,${item.resolveNumber} 条已匹配`
`查询${item.query_count}条数据,成功匹配${item.result_count}条,耗时${item.cost_time.toFixed(2)}ms`
}}
</p>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无查询动态" class="!h-[540px]" />
</el-scrollbar>
</el-card>
</re-col>