目标:在Vue开源项目Pure Admin的基础上,增加菜单和标签页,实现同期温度对比的柱状图,支持按时段查询。
先贴上效果图:
增加菜单
新建src\router\modules\weather.ts,
export default {
path: "/weather",
meta: {
title: "天气"
},
children: [
{
path: "/weather/index",
name: "Weather",
component: () => import("@/views/weather/index.vue"),
meta: {
title: "天气",
showParent: true
}
}
]
};
跨域访问配置:
在vite.config.ts中增加proxy配置:
proxy: {
"/api": {
// 这里填写后端地址
target: "http://127.0.0.1:5000",
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, "")
}
},
新建src\api\utils.ts,将所有api/xxx的访问通过代理转发至http://127.0.0.1:5000
export const baseUrlApi = (url: string) => `/api/${url}`;
发送post请求(在下文的hook.tsx代码中):
const getWeather = (data?: object) => {
return http.request<any>("post", baseUrlApi("getWeather"), { data });
};
const { data } = await getWeather(form);
前端柱状图
新建src\views\weather\index.vue,在其中编写网页代码。
<script setup lang="ts">
import { weatherRole } from "./hook";
import { ref } from "vue";
import { getPickerShortcuts } from "../monitor/utils";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import Refresh from "@iconify-icons/ep/refresh";
import Segmented, { type OptionsType } from "@/components/ReSegmented";
import { ChartBar } from "./components/charts";
import ReCol from "@/components/ReCol";
defineOptions({
// name 作为一种规范最好必须写上并且和路由的name保持一致
name: "Weather",
});
const formRef = ref();
const {
form,
resetForm,
onSearch,
loading,
oneData,
twoData,
headData,
getWeather
} = weatherRole();
onSearch();
</script>
<template>
<div class="main">
<el-row :gutter="24" justify="space-around">
<re-col
v-motion
class="mb-[18px]"
:value="24"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 400
}
}"
>
<el-form
ref="formRef"
:inline="true"
:model="form"
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px] overflow-auto"
>
<el-form-item label="时间" prop="loginTime">
<el-date-picker
v-model="form.loginTime"
:shortcuts="getPickerShortcuts()"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期时间"
end-placeholder="结束日期时间"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="useRenderIcon('ri:search-line')"
@click="onSearch"
>
搜索
</el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
</re-col>
</el-row>
<el-row :gutter="24" justify="space-around">
<re-col
v-motion
class="mb-[18px]"
:value="24"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 400
}
}"
>
<el-card class="bar-card" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">天气概览</span>
</div>
<div class="flex justify-between items-start mt-3">
<ChartBar
:oneData="oneData"
:twoData="twoData"
:headData="headData"
/>
</div>
</el-card>
</re-col>
</el-row>
</div>
</template>
新建src\views\weather\components\charts\ChartBar.vue,在其中编写Echarts柱状图代码:
<script setup lang="ts">
import { useDark, useECharts } from "@pureadmin/utils";
import { type PropType, ref, computed, watch, nextTick } from "vue";
const props = defineProps({
oneData: {
type: Array as PropType<Array<number>>,
default: () => []
},
twoData: {
type: Array as PropType<Array<number>>,
default: () => []
},
headData: {
type: Array as PropType<Array<string>>,
default: () => []
}
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme
});
watch(
() => props,
async () => {
await nextTick(); // 确保DOM更新完成后再执行
setOptions({
container: ".bar-card",
color: ["#41b6ff", "#e85f33"],
tooltip: {
trigger: "axis",
axisPointer: {
type: "none"
}
},
grid: {
top: "20px",
left: "50px",
right: 0
},
legend: {
data: ["去年", "今年"],
textStyle: {
color: "#606266",
fontSize: "0.875rem"
},
bottom: 0
},
xAxis: [
{
type: "category",
data: props.headData,
axisLabel: {
fontSize: "0.875rem"
},
axisPointer: {
type: "shadow"
}
}
],
yAxis: [
{
type: "value",
axisLabel: {
fontSize: "0.875rem"
},
splitLine: {
show: false // 去网格线
}
// name: "单位: 个"
}
],
series: [
{
name: "去年",
type: "bar",
barWidth: 10,
itemStyle: {
color: "#41b6ff",
borderRadius: [10, 10, 0, 0]
},
data: props.oneData
},
{
name: "今年",
type: "bar",
barWidth: 10,
itemStyle: {
color: "#e86033ce",
borderRadius: [10, 10, 0, 0]
},
data: props.twoData
}
]
});
},
{
deep: true,
immediate: true
}
);
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 365px" />
</template>
新建src\views\weather\components\charts\index.ts,导出ChartBar:
export { default as ChartBar } from "./ChartBar.vue";
前后端数据交互:
新建src\views\weather\hook.tsx,编写数据交互和逻辑:
import { type Ref, ref, reactive } from "vue";
import { http } from "@/utils/http";
import { baseUrlApi } from "../../api/utils";
export function weatherRole() {
const form = reactive({
username: "",
status: "",
loginTime: ""
});
const loading = ref(true);
const resetForm = formEl => {
if (!formEl) return;
formEl.resetFields();
onSearch();
};
async function onSearch() {
const { data } = await getWeather(form);
oneData.value = data.one;
twoData.value = data.two;
headData.value = data.head;
console.log("banner====>", data);
};
async function onSearch_bak() {
getWeather(form).then(res => {
console.log("banner11111====>", res);
oneData.value = res.data;
twoData.value = res.data.two;
headData.value = res.data.head;
});
};
const oneData = ref([]);
const twoData = ref([]);
const headData = ref([]);
const getWeather = (data?: object) => {
return http.request<any>("post", baseUrlApi("getWeather"), { data });
};
return {
form,
loading,
resetForm,
onSearch,
oneData,
twoData,
headData,
getWeather
};
}
后端Python 代码:
import numpy as np
import pandas as pd
from datetime import datetime
def cvtTime(time_array):
#time_array = ['2024-5-05T16:00:00.000Z', '2024-06-05T16:00:00.000Z']
date_format = '%Y-%m-%dT%H:%M:%S.%fZ'
t1 = datetime.strptime(time_array[0], date_format)
t2 = datetime.strptime(time_array[1], date_format)
return t1,t2
def getTemperatureByTime(t1,t2):
#读取Excel
df1 = pd.read_excel('23.xls',header=0,encoding='gbk')
df2 = pd.read_excel('24.xls',header=0,encoding='gbk')
df = pd.concat([df1,df2])
df['Time'] = pd.to_datetime(df['Time'],format='%d.%m.%Y %H:%M')
df.set_index('Time', inplace=True)#以时间为索引的时间序列值
df = df[["Temperature"]]#选取温度列
df = df.resample('24H').agg(func=['max','min','mean'])#上采样
data_this_year = df[(df.index >= t1) & (df.index <= t2)]#选取指定时段
data_last_year = df[(df.index >= (t1 + pd.DateOffset(years=-1)))
& (df.index <= (t2 + pd.DateOffset(years=-1)))]#去年同期
ts = [str(x).replace(' 00:00:00','') for x in list(data_this_year.index)]#日期数组(年月日)
oneT = list(data_last_year['Temperature']['max'].values)
twoT = list(data_this_year['Temperature']['max'].values)
data = {"data": {
"one":oneT,
"two":twoT,
"head":ts
}}
return data
from flask import Flask,url_for,request
from flask_cors import CORS
import json
app = Flask(__name__)
CORS(app)
CORS(app, origins='http://localhost:8000/')
@app.route("/getWeather", methods=['POST'])
def getWeather():
loginTime = request.json.get("loginTime")
print(loginTime)
t1,t2 = cvtTime(loginTime)
data = getTemperatureByTime(t1,t2)
return json.dumps(data).encode('utf-8')
app.run()
天气数据的xls格式如下: