feat: 登陆

master
xjs 2025-05-23 17:45:23 +08:00
parent eba5899cd6
commit 020bba07f1
27 changed files with 504 additions and 280 deletions

1
public/icons/avatar.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><mask id="master_svg0_117_7235" style="mask-type:alpha" maskUnits="objectBoundingBox"><g><path d="M0 0C0 0 0 0 0 0L16 0C16 0 16 0 16 0L16 16C16 16 16 16 16 16L0 16C0 16 0 16 0 16Z" fill="#FFFFFF" fill-opacity="1"/></g></mask></defs><g mask="url(#master_svg0_117_7235)"><g><path d="M3.542045296936035,4.23C3.542045296936035,6.56522,5.470995296936035,8.47,7.8420452969360355,8.47C10.213095296936036,8.47,12.142065296936035,6.56523,12.142065296936035,4.23C12.142065296936035,1.89477,10.212095296936035,0,7.8420452969360355,0C5.471815296936035,0,3.542045296936035,1.89478,3.542045296936035,4.23ZM15.011265296936035,14.1141C15.011265296936035,11.1742,12.575965296936035,8.78406,9.591265296936035,8.78406L6.401265296936035,8.78406C3.416565296936035,8.78406,0.9912652969360352,11.1728,0.9912652969360352,14.1141L0.9912652969360352,14.4341C0.9912652969360352,16.0031,3.376765296936035,16.0041,6.401265296936035,16.0041L9.591265296936035,16.0041C12.496365296936036,16.0041,15.011265296936035,16.0031,15.011265296936035,14.4341L15.011265296936035,14.1141Z" fill-rule="evenodd" fill="#0C90FF" fill-opacity="1"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

6
public/icons/eye.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<path d="M10,50 C20,30 40,20 50,20 C60,20 80,30 90,50 C80,70 60,80 50,80 C40,80 20,70 10,50 Z" fill="none" stroke="black" stroke-width="5"/>
<circle cx="50" cy="50" r="18" fill="none" stroke="black" stroke-width="5"/>
<circle cx="50" cy="50" r="8" fill="black"/>
<circle cx="50" cy="50" r="4" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 411 B

1
public/icons/logout.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><g><g style="opacity:0;"><path d="M0 0C0 0 0 0 0 0L16 0C16 0 16 0 16 0L16 16C16 16 16 16 16 16L0 16C0 16 0 16 0 16Z" fill="#D8D8D8" fill-opacity="1"/><path d="M0.5 0.5C0.5 0.5 0.5 0.5 0.5 0.5L15.5 0.5C15.5 0.5 15.5 0.5 15.5 0.5L15.5 15.5C15.5 15.5 15.5 15.5 15.5 15.5L0.5 15.5C0.5 15.5 0.5 15.5 0.5 15.5Z" fill-opacity="0" stroke-opacity="0" stroke="#979797" fill="none" stroke-width="1"/></g><g><g><path d="M5.373828125,11.463425234375L9.364458124999999,11.463425234375C9.914448125,11.463425234375,10.361328125,11.910315234375,10.361328125,12.463415234375C10.361328125,13.013415234375,9.914448125,13.458715234375,9.364458124999999,13.458715234375L4.384768125,13.458715234375C3.284765125,13.458715234375,2.392578125,12.566515234375,2.392578125,11.466555234375L2.392578125,3.497805234375C2.392578125,2.397802234375,3.284765125,1.505615234375,4.384768125,1.505615234375L9.364458124999999,1.505615234375C9.916018125,1.505615234375,10.361328125,1.950927234375,10.361328125,2.500928234375C10.361328125,3.0509252343749997,9.914448125,3.496245234375,9.364458124999999,3.496245234375L5.381638125,3.496245234375C4.830078125,3.496245234375,4.384768125,3.938425234375,4.384768125,4.483745234375L4.384768125,10.468115234375C4.384768125,11.018115234375,4.828518125,11.463425234375,5.373828125,11.463425234375ZM12.244528125,4.678955234375L14.236678125,7.168015234375C14.382078125,7.350825234375,14.382078125,7.608645234375,14.235178125,7.788325234375L12.242968125,10.278955234375C12.146098125,10.399265234375,12.002348125,10.466455234375,11.853908125,10.466455234375Q11.771878125,10.466455234375,11.689848125,10.438325234375C11.491408125,10.368015234375,11.357028125,10.178955234375,11.357028125,9.968015234375L11.357028125,8.474265234375L7.374218125,8.474265234375C6.824218125,8.474265234375,6.378908125000001,8.028955234375001,6.378908125000001,7.478955234375C6.378908125000001,6.928955234375,6.824218125,6.483645234375,7.374218125,6.483645234375L11.358598125,6.483645234375L11.358598125,4.989895234375C11.358598125,4.778955234375,11.491408125,4.589895234375,11.691408125,4.519575234375C11.744528125,4.500825234375,11.800778125,4.491455234375,11.855468125,4.491455234375C12.003908125,4.491455234375,12.147658125,4.558645234375,12.244528125,4.678955234375Z" fill-rule="evenodd" fill="#45A2FF" fill-opacity="1"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><mask id="master_svg0_117_7222" style="mask-type:alpha" maskUnits="objectBoundingBox"><g><path d="M0 0C0 0 0 0 0 0L16 0C16 0 16 0 16 0L16 16C16 16 16 16 16 16L0 16C0 16 0 16 0 16Z" fill="#FFFFFF" fill-opacity="1"/></g></mask></defs><g mask="url(#master_svg0_117_7222)"><g><g><path d="M4.646874904632568,3.9093748554587364C4.999999904632569,3.9093748554587364,5.287499904632568,4.195311855458736,5.287499904632568,4.549999855458736L5.287499904632568,7.218754855458736C5.287499904632568,7.571874855458736,5.001562904632569,7.859374855458737,4.646874904632568,7.859374855458737C4.293749904632568,7.859374855458737,4.006249904632568,7.5734348554587365,4.006249904632568,7.218754855458736L4.006249904632568,4.549999855458736C4.006249904632568,4.196874855458736,4.293749904632568,3.9093748554587364,4.646874904632568,3.9093748554587364Z" fill-rule="evenodd" fill="#0C90FF" fill-opacity="1"/></g><g><path d="M10.9609375,3.846874952316284C11.3140625,3.846874952316284,11.6015625,4.132811952316284,11.6015625,4.487499952316284L11.6015625,7.129684952316284C11.6015625,7.482814952316284,11.3156255,7.7703149523162836,10.9609375,7.7703149523162836C10.6078125,7.7703149523162836,10.3203125,7.484374952316284,10.3203125,7.129684952316284L10.3203125,4.487499952316284C10.3203125,4.134374952316284,10.6078125,3.846874952316284,10.9609375,3.846874952316284Z" fill-rule="evenodd" fill="#0C90FF" fill-opacity="1"/></g></g><g><path d="M10.315862738418579,4.492187428474426L11.59586273841858,4.492187428474426C11.59586273841858,2.3953074284744265,9.902742738418578,0.6921874284744263,7.805862738418579,0.6921874284744263C5.7089827384185785,0.6921874284744263,4.005862738418579,2.3953074284744265,4.005862738418579,4.492187428474426L5.2858627384185795,4.492187428474426C5.2858627384185795,3.1015574284744263,6.416802738418579,1.9721874284744263,7.805862738418579,1.9721874284744263C9.19648273841858,1.9721874284744263,10.315862738418579,3.1015574284744263,10.315862738418579,4.492187428474426ZM13.520312738418578,7.529637428474426C13.46721273841858,7.007757428474426,13.040012738418579,6.599947428474426,12.510312738418579,6.579637428474427L3.130312738418579,6.579637428474427C2.5443757384185792,6.579637428474427,2.070312738418579,7.053697428474426,2.070312738418579,7.639637428474426L2.070312738418579,14.579687428474426C2.132812738418579,15.051587428474427,2.500000738418579,15.432487428474426,2.970312738418579,15.499687428474425L12.690312738418578,15.499687428474425C13.11221273841858,15.415287428474427,13.43901273841858,15.081587428474426,13.520312738418578,14.659687428474426L13.520312738418578,7.529637428474426Z" fill-rule="evenodd" fill="#0C90FF" fill-opacity="1"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/images/login-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

@ -11,3 +11,9 @@ export const getSegmentStatic = () => `${baseUrl}/api/bigScreenData/bigScreenSta
export const getBigScreenRanking = () => `${baseUrl}/api/bigScreenData/bigScreenRanks`;
export const getSixStatisticsUrl = () => `${baseUrl}/api/bigScreenData/wechatData`
export const postLoginUrl=() => `${baseUrl}/api/sysAuth/BigScreenLogin`
export const getUserInfoUrl = ()=> `${baseUrl}/api/sysAuth/userInfo`
export const getLogoutUrl = () => `${baseUrl}/api/sysAuth/logout`

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="480" height="48" viewBox="0 0 480 48"><g><path d="M0,48L480,48L480,0L10.1181,0L0,12.5L0,48L0,48Z" fill="#061E3A" fill-opacity="1"/><path d="M480,48L480,0L10.1181,0L0,12.5L0,48L480,48ZM1,47L479,47L479,1L10.5952,1L1,12.854L1,47Z" fill-rule="evenodd" fill="#2A8EFE" fill-opacity="1"/></g></svg>

After

Width:  |  Height:  |  Size: 407 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,29 @@
<template>
<div class="error-container flex flex-col items-center justify-center w-full h-full">
<SvgIcon name="error" class="w-[48px] h-[48px] text-red-500 mb-2"/>
<div class="text-red-500 text-sm">加载失败</div>
<button
class="mt-2 px-4 py-1 bg-[#45A2FF] text-white rounded hover:bg-[#3d8fe0] transition-colors"
@click="retry"
>
重试
</button>
</div>
</template>
<script lang="ts" setup>
import SvgIcon from "@/components/svg-icon/SvgIcon.vue";
const emit = defineEmits(['retry']);
const retry = () => {
emit('retry');
};
</script>
<style scoped lang="scss">
.error-container {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<div class="loading-container flex items-center justify-center w-full h-full">
<div class="loading-spinner"></div>
</div>
</template>
<style scoped lang="scss">
.loading-container {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #45A2FF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@ -1,6 +1,8 @@
import { createRouter, createWebHistory } from "vue-router";
import { publicRoutes } from "./publicRoutes";
import { privateRoutes } from "./privateRoutes";
import { useUserStore } from "@/store/user";
function getRoutes() {
const routes = [
// 私有路由,请在这里添加
@ -16,6 +18,7 @@ function getRoutes() {
}
const router = createRouter({
history: createWebHistory(),
routes: getRoutes(),
@ -23,19 +26,26 @@ const router = createRouter({
// 全局前置守卫,这边可以对身份进行验证
router.beforeEach((to, _from, next) => {
const userStore = useUserStore()
let userRole = "";
let hasLogin = userStore.getAccessToken;
console.log(hasLogin);
let userRole = "admin";
// 如果目标路由没有角色限制
if (!to.meta.role) {
next();
}
console.log(to.meta.role);
// 判断当前用户角色是否在目标路由的允许角色列表中
if ((to.meta.role as string[]).includes(userRole)) {
// 如果角色匹配,允许进入目标路由
next();
}else if(hasLogin){
next();
} else {
// 如果角色不匹配,跳转到 unauthorized 页面
next({ path: "/unauthorized" });
next({ path: "/login" });
}
});

View File

@ -1 +1,11 @@
export const privateRoutes = [];
export const privateRoutes = [
{
path:"/",
name:'home',
meta:{
title:'主页',
role:['admin']
},
component:()=>import('../views/home.vue')
},
];

View File

@ -8,12 +8,13 @@ export const publicRoutes = [
},
component:()=>import('../views/unauthorized.vue')
},
{
path:"/",
name:'home',
path:"/login",
name:'login',
meta:{
title:'home'
title:'登陆'
},
component:()=>import('../views/home.vue')
component:()=>import('../views/login.vue')
}
]

View File

@ -2,16 +2,23 @@ import { defineStore } from "pinia";
export const useUserStore = defineStore("user", {
persist:true,
state:()=>({
token:""
accessToken:"",
refreshToken:""
}),
getters:{
getToken(state){
return state.token;
getAccessToken(state){
return state.accessToken;
},
getRefreshToken(state){
return state.refreshToken
}
},
actions:{
setToken(token:string){
this.token = token;
setAccessToken(token:string){
this.accessToken = token;
},
setRefreshToken(token:string){
this.refreshToken = token
}
}
});

View File

@ -1,5 +1,5 @@
<template>
<div class="w-[926px] h-[358px] relative bg-[#082059]">
<div class="h-[358px] relative bg-[#082059]">
<div class="flex h-full custom-border absolute top-0 left-0">
<div class="relative h-[36px]">
<div class="absolute top-[50%] translate-y-[-50%] left-[15px] flex items-center">

View File

@ -55,13 +55,16 @@
watch(
() => chargingRankingData.value,
() => {
if(chargingRankingData.value.paymentRanks){
products.value = chargingRankingData.value.paymentRanks
}
initData()
},
);
const initData = () => {
if(chargingRankingData.value.paymentRanks){
products.value = chargingRankingData.value.paymentRanks
}
}
const headerLeftSvg = ref("");
const headerRightSvg = ref("");
@ -115,6 +118,8 @@
getGoldMedalSvg();
getSilverMedalSvg();
getBronzeMedalSvg();
initData();
});
</script>

View File

@ -77,21 +77,26 @@
watch(
() => askSectionData.value,
() => {
if (askSectionData.value.offline.length > 0) {
initData()
},
);
const initData = () => {
if (askSectionData.value.offline && askSectionData.value.offline.length > 0) {
chartData.value = askSectionData.value.offline.map((item, index) => ({
name: item.tag,
value: item.total,
color: colorList.value[index % colorList.value.length],
}));
}
},
);
}
onBeforeMount(() => {
getHeaderLeftSvg();
getHeaderRightSvg();
getArrowLeftSvg();
getPaymentChartSvg();
initData();
});
</script>

View File

@ -76,20 +76,26 @@
const chartData = ref<any[]>([]);
watch(() => askSectionData.value, () => {
if (askSectionData.value.online.length > 0) {
initData()
});
const initData = () => {
if (askSectionData.value.online && askSectionData.value.online.length > 0) {
chartData.value = askSectionData.value.online.map((item, index) => ({
name: item.tag,
value: item.total,
color: colorList.value[index % colorList.value.length],
}));
}
});
}
onBeforeMount(() => {
getHeaderLeftSvg();
getHeaderRightSvg();
getArrowLeftSvg();
getPaymentChartSvg();
initData();
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="w-[926px] h-[358px] relative bg-[#082059]">
<div class="h-[358px] relative bg-[#082059]">
<div class="flex h-full custom-border absolute top-0 left-0">
<div class="relative h-[36px]">
<div class="absolute top-[50%] translate-y-[-50%] left-[15px] flex items-center">
@ -34,6 +34,7 @@
};
addRequest("getAcqTrendData", getAcqTrendData, [DateType]);
runImmediatelyByKey("getAcqTrendData");
const handleDateChange = (type: string) => {
DateType = type === "week" ? 0 : 1;

View File

@ -11,19 +11,13 @@
<SvgComponent :content="headerRightSvg" class="w-[108px] h-[36px]" />
</div>
<div class="w-full h-[calc(100%-36px)] mt-[36px] flex flex-col">
<StudentSourceChart
class="w-full h-full"
:chartData="[
{ name: '线下', value: offlineTotal, itemStyle: { color: 'rgba(147, 219, 255, 1)' } },
{ name: '线上', value: onlineTotal, itemStyle: { color: 'rgb(79, 214, 169)' } },
]"
:ringSize="0.8" />
<StudentSourceChart class="w-full h-full" :chartData="chartData" :ringSize="0.8" />
<div class="flex items-center justify-between mx-[20px] my-[33px]">
<div class="flex items-center">
<SvgComponent :content="onlineSvg" class="w-[53px] h-[38px]" />
<div class="flex flex-col ml-[9px] items-center">
<div class="bg-gradient-to-b from-[#8FC8FF] to-white bg-clip-text text-transparent font-700">
<span class="text-[18px]">{{onlineTotal }}</span>
<span class="text-[18px]">{{ onlineTotal }}</span>
<span class="text-[14px]"></span>
</div>
<span class="text-[#C7F0FF] text-[12px]">线上来源</span>
@ -67,30 +61,42 @@
arrowLeftSvg.value = svg;
};
const onlineSvg = ref("")
const offlineSvg = ref("")
const onlineSvg = ref("");
const offlineSvg = ref("");
const getOfflineSvg = async () => {
const {default: svg} = await import('/src/assets/svg-img/offline.svg?raw')
offlineSvg.value = svg
}
const { default: svg } = await import("/src/assets/svg-img/offline.svg?raw");
offlineSvg.value = svg;
};
const getOnlineSvg = async () => {
const {default: svg} = await import('/src/assets/svg-img/online.svg?raw')
onlineSvg.value = svg
}
const { default: svg } = await import("/src/assets/svg-img/online.svg?raw");
onlineSvg.value = svg;
};
const askSectionData = inject("askSectionData",ref<{online:any[],offline:any[]}>({online:[],offline:[]}))
const askSectionData = inject("askSectionData", ref<{ online: any[]; offline: any[] }>({ online: [], offline: [] }));
const onlineTotal = ref(0)
const offlineTotal = ref(0)
watch(() =>askSectionData.value,() => {
if(askSectionData.value.online.length > 0){
onlineTotal.value = askSectionData.value.online.reduce((acc, curr) => acc + curr.total, 0)
const onlineTotal = ref(0);
const offlineTotal = ref(0);
const chartData = ref<any[]>([]);
watch(
() => askSectionData.value,
() => {
initData()
},
);
const initData = () => {
if (askSectionData.value.online && askSectionData.value.online.length > 0) {
onlineTotal.value = askSectionData.value.online.reduce((acc, curr) => acc + curr.total, 0);
}
if(askSectionData.value.offline.length > 0){
offlineTotal.value = askSectionData.value.offline.reduce((acc, curr) => acc + curr.total, 0)
if (askSectionData.value.offline && askSectionData.value.offline.length > 0) {
offlineTotal.value = askSectionData.value.offline.reduce((acc, curr) => acc + curr.total, 0);
}
chartData.value = [
{ name: "线下", value: offlineTotal.value, itemStyle: { color: "rgba(147, 219, 255, 1)" } },
{ name: "线上", value: onlineTotal.value, itemStyle: { color: "rgb(79, 214, 169)" } },
];
}
})
onBeforeMount(() => {
getHeaderLeftSvg();
@ -99,6 +105,7 @@
getOfflineSvg();
getOnlineSvg();
initData();
});
</script>

View File

@ -57,11 +57,15 @@
watch(
() => chargingRankingData.value,
() => {PageTransitionEvent
initData()
},
);
const initData = () => {
if(chargingRankingData.value.acqRanks){
products.value = chargingRankingData.value.acqRanks
}
},
);
}
const headerLeftSvg = ref("");
const headerRightSvg = ref("");
@ -116,6 +120,8 @@
getGoldMedalSvg();
getSilverMedalSvg();
getBronzeMedalSvg();
initData();
});
</script>

View File

@ -317,12 +317,14 @@
updateChartData();
}
},
{ deep: true }
{ deep: true,immediate:true }
);
onMounted(() => {
onMounted(() => {
if(!myChart){
initChart();
});
}
})
onUnmounted(() => {
window.removeEventListener("resize", handleResize);

View File

@ -33,8 +33,20 @@ const props = defineProps({
})
watch(() => props.chartData,() => {
initChart();
})
if (chart) {
const options = {
xAxis: {
data: props.chartData.map((item:any) => item.date)
},
series: [{
data: props.chartData.map((item:any) => item.total)
}]
};
chart.setOption(options);
}
}, { deep: true })
const initChart = () => {
if(!chartRef.value) return;
@ -81,7 +93,7 @@ const initChart = () => {
},
series: [
{
name: '据1',
name: '获客数',
type: 'line',
smooth: true,
data: props.chartData.map((item:any) => item.total),
@ -114,7 +126,9 @@ const initChart = () => {
}
onMounted(() => {
if(!chart){
initChart();
}
window.addEventListener('resize', () => {
chart?.resize();
});

View File

@ -17,7 +17,7 @@
import { CanvasRenderer } from "echarts/renderers";
import { SurfaceChart } from "echarts-gl/charts";
import { Grid3DComponent } from "echarts-gl/components";
import { ref, onMounted, watch } from "vue";
import { ref, onMounted, watch, onBeforeUnmount } from "vue";
echarts.use([TooltipComponent, Grid3DComponent, SurfaceChart, CanvasRenderer]);
@ -35,7 +35,7 @@
});
const chartRef = ref<HTMLElement | null>(null);
let chart: echarts.ECharts | null = null;
let chartInstance: echarts.ECharts | null = null;
interface SeriesItem {
[key: string]: any;
@ -206,7 +206,7 @@
height: 106,
},
z: 0,
cursor:"point",
cursor: "point",
origin: [127, 53],
keyframeAnimation: [
{
@ -241,173 +241,14 @@
//
const initChart = () => {
if (!chartRef.value) return;
chart = echarts.init(chartRef.value);
const option = getPie3D(props.chartData, props.ringSize);
option.series.push({
name: "pie2d",
type: "pie",
labelLine: {
length: 18,
length2: 18,
smooth: true,
minTurnAngle: 20,
},
label: {
show: true,
position: "outside",
color: "inherit",
rich: {
a: {
width: 5,
height: 5,
borderRadius: 50,
backgroundColor: "inherit",
},
b: {
fontSize: 12,
},
c: {
fontSize: 12,
},
},
formatter: "{a|} {b|{b}}{d|{d}}%",
},
startAngle: -30,
clockwise: false,
radius: ["30%", "60%"],
padAngle: 360,
center: ["50%", "50%"],
data: props.chartData,
itemStyle: {
opacity: 1,
borderWidth: 0,
},
z: 1,
});
chart.setOption(option);
bindEvents();
chartInstance = echarts.init(chartRef.value);
updateChart();
};
//
const bindEvents = () => {
if (!chart) return;
// let selectedIndex = "";
// let hoveredIndex = "";
// chart.on("click", (params: any) => {
// if (!chart) return;
// const option = chart.getOption() as any;
// const isSelected = !option.series[params.seriesIndex].pieStatus.selected;
// const isHovered = option.series[params.seriesIndex].pieStatus.hovered;
// const k = option.series[params.seriesIndex].pieStatus.k;
// const startRatio = option.series[params.seriesIndex].pieData.startRatio;
// const endRatio = option.series[params.seriesIndex].pieData.endRatio;
// if (selectedIndex !== "" && selectedIndex !== params.seriesIndex) {
// option.series[selectedIndex].parametricEquation = getParametricEquation(
// option.series[selectedIndex].pieData.startRatio,
// option.series[selectedIndex].pieData.endRatio,
// false,
// false,
// k,
// option.series[selectedIndex].pieData.value,
// );
// option.series[selectedIndex].pieStatus.selected = false;
// }
// option.series[params.seriesIndex].parametricEquation = getParametricEquation(
// startRatio,
// endRatio,
// isSelected,
// isHovered,
// k,
// option.series[params.seriesIndex].pieData.value,
// );
// option.series[params.seriesIndex].pieStatus.selected = isSelected;
// isSelected ? (selectedIndex = params.seriesIndex) : null;
// chart.setOption(option);
// });
//
// chart.on("mouseover", (params: any) => {
// if (!chart) return;
// const option = chart.getOption() as any;
// if (hoveredIndex === params.seriesIndex) return;
// if (hoveredIndex !== "") {
// const isSelected = option.series[hoveredIndex].pieStatus.selected;
// const isHovered = false;
// const k = option.series[hoveredIndex].pieStatus.k;
// const startRatio = option.series[hoveredIndex].pieData.startRatio;
// const endRatio = option.series[hoveredIndex].pieData.endRatio;
// option.series[hoveredIndex].parametricEquation = getParametricEquation(
// startRatio,
// endRatio,
// isSelected,
// isHovered,
// k,
// option.series[hoveredIndex].pieData.value,
// );
// option.series[hoveredIndex].pieStatus.hovered = isHovered;
// hoveredIndex = "";
// }
// if (params.seriesName !== "mouseoutSeries" && params.seriesName !== "pie2d") {
// const isSelected = option.series[params.seriesIndex].pieStatus.selected;
// const isHovered = true;
// const k = option.series[params.seriesIndex].pieStatus.k;
// const startRatio = option.series[params.seriesIndex].pieData.startRatio;
// const endRatio = option.series[params.seriesIndex].pieData.endRatio;
// option.series[params.seriesIndex].parametricEquation = getParametricEquation(
// startRatio,
// endRatio,
// isSelected,
// isHovered,
// k,
// option.series[params.seriesIndex].pieData.value + 5,
// );
// option.series[params.seriesIndex].pieStatus.hovered = isHovered;
// hoveredIndex = params.seriesIndex;
// }
// chart.setOption(option);
// });
// //
// chart.on("globalout", () => {
// if (!chart) return;
// const option = chart.getOption() as any;
// if (hoveredIndex !== "") {
// const isSelected = option.series[hoveredIndex].pieStatus.selected;
// const isHovered = false;
// const k = option.series[hoveredIndex].pieStatus.k;
// const startRatio = option.series[hoveredIndex].pieData.startRatio;
// const endRatio = option.series[hoveredIndex].pieData.endRatio;
// option.series[hoveredIndex].parametricEquation = getParametricEquation(
// startRatio,
// endRatio,
// isSelected,
// isHovered,
// k,
// option.series[hoveredIndex].pieData.value,
// );
// option.series[hoveredIndex].pieStatus.hovered = isHovered;
// hoveredIndex = "";
// }
// chart.setOption(option);
// });
const updateChart = () => {
if (!chartInstance) return;
const option = getPie3D(props.chartData, props.ringSize);
chartInstance.setOption(option, true);
};
const total = computed(() => props.chartData.reduce((sum: number, item: any) => sum + item.value, 0));
@ -416,15 +257,32 @@
watch(
() => props.chartData,
() => {
initChart();
if (chartInstance) {
updateChart();
}
},
{ deep: true },
);
//
onMounted(() => {
if (!chartInstance) {
initChart();
}
window.addEventListener("resize", handleResize);
});
onBeforeUnmount(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
window.removeEventListener("resize", handleResize);
});
const handleResize = () => {
chartInstance?.resize();
};
</script>
<style scoped></style>

View File

@ -6,6 +6,11 @@
<div class="absolute top-[25%] right-[24px] translate-y-[-25%] z-1 flex items-center justify-center w-max">
<div class="text-[#45A2FF] text-[14px]">{{ year }}-{{ month }}-{{ day }}&nbsp;{{ weekday }}</div>
<DigitalWatch class="ml-[10px]" />
<div class="text-[#45A2FF] flex items-center ml-[10px]">
<SvgIcon name="avatar" class="w-[16px] h-[16px]"/>
<div class="mx-[5px]">{{ username }}</div>
<SvgIcon name="logout" class="w-[16px] h-[16px] cursor-pointer" @click="handleLogout"/>
</div>
</div>
</header>
<div class="flex items-center justify-end pr-[24px] cursor-pointer mb-[13px]" @click="updateAllData">
@ -15,21 +20,21 @@
<div class="grid grid-rows-[210px_358px_320px] gap-y-5 w-full min-h-[928px] gap-[20px] flex-1 overflow-auto mb-[27px]">
<div class="flex items-center px-[24px] justify-start">
<PaymentTotal />
<TodayPayment class="ml-[20px]" />
<GainTotal class="ml-[20px]" />
<GainToday class="ml-[20px]" />
<LossStatic class="ml-[20px]" />
<PaymentTotal class="flex-1"/>
<TodayPayment class="ml-[20px] flex-1" />
<GainTotal class="ml-[20px] flex-1" />
<GainToday class="ml-[20px] flex-1" />
<LossStatic class="ml-[20px] flex-1" />
</div>
<div class="flex items-center px-[24px] justify-start">
<OperatingTrends class="" />
<AskSection class="ml-[20px]" />
<div class="grid grid-cols-2 items-center px-[24px] gap-x-[20px]">
<OperatingTrends class="flex-1" />
<AskSection class="flex-1" />
</div>
<div class="flex items-center px-[24px] overflow-x-auto">
<StudentSource />
<OnLineStatus class="ml-[20px]" />
<OfflineStatus class="ml-[20px]" />
<SixStatistics class="ml-[20px]" />
<StudentSource class="min-w-[296px] max-w-[296px]" />
<OnLineStatus class="ml-[20px] min-w-[296px] max-w-[296px]" />
<OfflineStatus class="ml-[20px] min-w-[296px] max-w-[296px]" />
<SixStatistics class="ml-[20px] min-w-[296px] max-w-[296px]" />
<ChargingRanking class="ml-[20px]" />
<WinCustomer class="ml-[20px]" />
</div>
@ -40,28 +45,87 @@
<script lang="ts" setup>
import SvgComponent from "@/components/SvgComponent.vue";
import SvgIcon from "@/components/svg-icon/SvgIcon.vue";
import LoadingComponent from "@/components/LoadingComponent.vue";
import ErrorComponent from "@/components/ErrorComponent.vue";
import DigitalWatch from "@/components/watch/DigitalWatch.vue";
import PaymentTotal from "@/views/components/PaymentTotal.vue";
import TodayPayment from "@/views/components/TodayPayment.vue";
import GainTotal from "@/views/components/GainTotal.vue";
import GainToday from "@/views/components/GainToday.vue";
import LossStatic from "@/views/components/LossStatic.vue";
import OperatingTrends from "@/views/components/OperatingTrends.vue";
import AskSection from "@/views/components/AskSection.vue";
import StudentSource from "@/views/components/StudentSource.vue";
import OnLineStatus from "@/views/components/OnlineStatus.vue";
import OfflineStatus from "@/views/components/OfflineStatus.vue";
import SixStatistics from "@/views/components/SixStatistics.vue";
import ChargingRanking from "./components/ChargingRanking.vue";
import WinCustomer from "./components/WinCustomer.vue";
import { defineAsyncComponent } from 'vue';
const asyncComponentConfig = {
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
onError: (error: Error, retry: () => void, fail: () => void, attempts: number) => {
if (attempts <= 3) {
retry();
} else {
fail();
}
}
};
const PaymentTotal = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/PaymentTotal.vue")
});
const TodayPayment = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/TodayPayment.vue")
});
const GainTotal = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/GainTotal.vue")
});
const GainToday = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/GainToday.vue")
});
const LossStatic = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/LossStatic.vue")
});
const OperatingTrends = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/OperatingTrends.vue")
});
const AskSection = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/AskSection.vue")
});
const StudentSource = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/StudentSource.vue")
});
const OnLineStatus = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/OnlineStatus.vue")
});
const OfflineStatus = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/OfflineStatus.vue")
});
const SixStatistics = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("@/views/components/SixStatistics.vue")
});
const ChargingRanking = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("./components/ChargingRanking.vue")
});
const WinCustomer = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("./components/WinCustomer.vue")
});
import { useDate } from "@/composables/useDate";
import {useFetchAllData} from "./composables/useFetchData"
import { useFetchAllData } from "./composables/useFetchData"
import { runImmediatelyAll, updateTime } from "@/composables/usePolling";
import { formatDatetime } from "@/utils/date";
import {getUserInfoUrl,getLogoutUrl} from "@/api/fetchUrl";
import { getRequest, postRequest } from "@/api/customFetch";
import { useUserStore } from "@/store/user";
const { year, month, day, weekday, formateTime } = useDate();
const router = useRouter();
const headerSvg = ref("");
@ -78,15 +142,39 @@
useFetchAllData()
const updateAllData = () => {
runImmediatelyAll()
}
const username = ref("")
const userStore = useUserStore()
const getUserInfo = () => {
const accessToken = userStore.getAccessToken
getRequest(getUserInfoUrl(),{},{headers:{ Authorization: `Bearer ${accessToken}`}}).then(resp => {
if(resp.code === 200){
username.value = (resp.result as {account:string}).account
}
})
}
const handleLogout = () => {
const accessToken = userStore.getAccessToken
postRequest(getLogoutUrl(),{},{headers:{"content-type": "application/json; charset=utf-8",Authorization: `Bearer ${accessToken}`}}).then(resp => {
if(resp.code === 200){
userStore.setAccessToken('')
userStore.setRefreshToken('')
}
router.push("/login")
})
}
onBeforeMount(() => {
formateTime();
headerBackgroundSvg();
headerTitleSvg();
getUserInfo();
});
</script>

131
src/views/login.vue Normal file
View File

@ -0,0 +1,131 @@
<template>
<div class="login-bg">
<SvgComponent :content="titleSvg" class="h-[156px] mt-[141px]" />
<div class="login-form-wrapper w-[622px] h-[419px] mx-auto mt-[87px]">
<form class="w-full h-full flex flex-col items-center pt-[126px]" @submit="handleSubmit">
<div class="form-item px-[71px] w-full">
<div class="input-bg w-full h-[48px] flex items-center px-[18px]">
<SvgIcon name="avatar" class="text-[#00D5FF] mr-[13px]" />
<input
type="text"
v-model="formData.username"
placeholder="请输入账号"
class="flex-1 h-[48px] text-white placeholder:text-[#666666] focus:outline-none focus:border-[#29F1FA] bg-transparent" />
</div>
<div class="input-bg w-full h-[48px] flex items-center px-[18px] mt-[20px]">
<SvgIcon :name="showPassword ? 'eye' : 'password'" class="text-[#00D5FF] mr-[13px] cursor-pointer" @click="togglePasswordVisibility" />
<input
:type="showPassword ? 'text' : 'password'"
v-model="formData.password"
placeholder="请输入密码"
class="flex-1 h-[48px] text-white placeholder:text-[#666666] focus:outline-none focus:border-[#29F1FA] bg-transparent" />
</div>
<button type="submit" class="w-full h-[48px] bg-[#2A8EFE] text-[#fff] text-[18px] font-700 border-submit mt-[48px]">登录</button>
<p class="text-[#657295] mt-[10px] text-center text-[14px]">如忘记账号密码请联系管理员18845000222</p>
</div>
</form>
</div>
</div>
</template>
<script lang="ts" setup>
import SvgComponent from "@/components/SvgComponent.vue";
import SvgIcon from "@/components/svg-icon/SvgIcon.vue";
import { postLoginUrl } from "@/api/fetchUrl";
import { useRouter } from "vue-router";
import { postRequest } from "@/api/customFetch";
import { useUserStore } from "@/store/user";
const router = useRouter();
const userStore = useUserStore()
const titleSvg = ref("");
const getTitleSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/login-title.svg?raw");
titleSvg.value = svg;
};
//
const showPassword = ref(false);
const togglePasswordVisibility = () => {
showPassword.value = !showPassword.value;
};
//
const formData = ref({
username: "",
password: "",
});
//
const handleSubmit = async (e: Event) => {
e.preventDefault();
//
if (!formData.value.username || !formData.value.password) {
//
alert("请输入账号和密码");
return;
}
try {
postRequest(
postLoginUrl(),
{ account: formData.value.username, password: formData.value.password },
{ headers: { "content-type": "application/json; charset=utf-8" } },
).then((resp) => {
if (resp.code === 200) {
// token
const {accessToken,refreshToken} = resp.result as {refreshToken:string,accessToken:string}
userStore.setAccessToken(accessToken)
userStore.setRefreshToken(refreshToken)
//
router.push('/')
} else {
//
alert(resp.message || "登录失败");
}
});
} catch (error) {
console.error("登录请求失败:", error);
alert("登录失败,请稍后重试");
}
};
onBeforeMount(() => {
getTitleSvg();
});
</script>
<style lang="scss" scoped>
.login-bg {
width: 100vw;
height: 100vh;
background-image: url("/images/login-bg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
overflow-y: auto;
}
.login-form-wrapper {
background-image: url("@/assets/svg-img/login-border.svg");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
.input-bg {
background-image: url("@/assets/svg-img/login-input.svg");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
.border-submit {
clip-path: polygon(0 15px, 10px 0, 100% 0, 100% 100%, 0 100%, 0 15px);
}
</style>