feat: 手机端样式编写,PC端查看更多

master
xjs 2025-06-23 13:28:38 +08:00
parent 2d0faf099a
commit 1037562c5f
41 changed files with 1896 additions and 396 deletions

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icons/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/icons/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title></title> <title></title>
</head> </head>
<body> <body>

View File

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host 0.0.0.0",
"build": "vue-tsc -b && vite build", "build": "vue-tsc -b && vite build",
"upload": "bash ./upload.sh", "upload": "bash ./upload.sh",
"build-and-upload": "pnpm run build && pnpm run upload", "build-and-upload": "pnpm run build && pnpm run upload",
@ -36,6 +36,7 @@
"@unocss/preset-wind": "^0.65.2", "@unocss/preset-wind": "^0.65.2",
"@vitejs/plugin-basic-ssl": "^1.2.0", "@vitejs/plugin-basic-ssl": "^1.2.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vueuse/core": "^13.4.0",
"prettier": "3.4.2", "prettier": "3.4.2",
"sass-embedded": "^1.86.0", "sass-embedded": "^1.86.0",
"svg-sprite-loader": "^6.0.11", "svg-sprite-loader": "^6.0.11",

View File

@ -75,6 +75,9 @@ importers:
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(sass-embedded@1.86.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.6.3)) version: 5.2.1(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(sass-embedded@1.86.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.6.3))
'@vueuse/core':
specifier: ^13.4.0
version: 13.4.0(vue@3.5.13(typescript@5.6.3))
prettier: prettier:
specifier: 3.4.2 specifier: 3.4.2
version: 3.4.2 version: 3.4.2
@ -92,7 +95,7 @@ importers:
version: 0.65.2(postcss@5.2.18)(rollup@4.28.1)(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(sass-embedded@1.86.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.6.3)) version: 0.65.2(postcss@5.2.18)(rollup@4.28.1)(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(sass-embedded@1.86.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.6.3))
unplugin-auto-import: unplugin-auto-import:
specifier: ^0.19.0 specifier: ^0.19.0
version: 0.19.0(@nuxt/kit@3.14.1592(rollup@4.28.1))(rollup@4.28.1) version: 0.19.0(@nuxt/kit@3.14.1592(rollup@4.28.1))(@vueuse/core@13.4.0(vue@3.5.13(typescript@5.6.3)))(rollup@4.28.1)
vite: vite:
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.3(@types/node@22.10.1)(jiti@2.4.1)(sass-embedded@1.86.0)(tsx@4.19.2) version: 6.0.3(@types/node@22.10.1)(jiti@2.4.1)(sass-embedded@1.86.0)(tsx@4.19.2)
@ -653,6 +656,9 @@ packages:
'@types/qs@6.9.17': '@types/qs@6.9.17':
resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@unocss/astro@0.65.2': '@unocss/astro@0.65.2':
resolution: {integrity: sha512-lpGoleJToxaYeN5LTGrNbvbXATNWswgoQwlljIJ9kWOjx4NbGC71pXRvDQSb9yRFDTCr5S2hMtupna4ulrHisA==} resolution: {integrity: sha512-lpGoleJToxaYeN5LTGrNbvbXATNWswgoQwlljIJ9kWOjx4NbGC71pXRvDQSb9yRFDTCr5S2hMtupna4ulrHisA==}
peerDependencies: peerDependencies:
@ -801,6 +807,19 @@ packages:
'@vue/shared@3.5.13': '@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vueuse/core@13.4.0':
resolution: {integrity: sha512-OnK7zW3bTq/QclEk17+vDFN3tuAm8ONb9zQUIHrYQkkFesu3WeGUx/3YzpEp+ly53IfDAT9rsYXgGW6piNZC5w==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@13.4.0':
resolution: {integrity: sha512-CPDQ/IgOeWbqItg1c/pS+Ulum63MNbpJ4eecjFJqgD/JUCJ822zLfpw6M9HzSvL6wbzMieOtIAW/H8deQASKHg==}
'@vueuse/shared@13.4.0':
resolution: {integrity: sha512-+AxuKbw8R1gYy5T21V5yhadeNM7rJqb4cPaRI9DdGnnNl3uqXh+unvQ3uCaA2DjYLbNr1+l7ht/B4qEsRegX6A==}
peerDependencies:
vue: ^3.5.0
acorn@8.14.0: acorn@8.14.0:
resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
@ -3058,6 +3077,8 @@ snapshots:
'@types/qs@6.9.17': {} '@types/qs@6.9.17': {}
'@types/web-bluetooth@0.0.21': {}
'@unocss/astro@0.65.2(rollup@4.28.1)(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(sass-embedded@1.86.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.6.3))': '@unocss/astro@0.65.2(rollup@4.28.1)(vite@6.0.3(@types/node@22.10.1)(jiti@2.4.1)(sass-embedded@1.86.0)(tsx@4.19.2))(vue@3.5.13(typescript@5.6.3))':
dependencies: dependencies:
'@unocss/core': 0.65.2 '@unocss/core': 0.65.2
@ -3306,6 +3327,19 @@ snapshots:
'@vue/shared@3.5.13': {} '@vue/shared@3.5.13': {}
'@vueuse/core@13.4.0(vue@3.5.13(typescript@5.6.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 13.4.0
'@vueuse/shared': 13.4.0(vue@3.5.13(typescript@5.6.3))
vue: 3.5.13(typescript@5.6.3)
'@vueuse/metadata@13.4.0': {}
'@vueuse/shared@13.4.0(vue@3.5.13(typescript@5.6.3))':
dependencies:
vue: 3.5.13(typescript@5.6.3)
acorn@8.14.0: {} acorn@8.14.0: {}
alien-signals@0.2.2: {} alien-signals@0.2.2: {}
@ -5161,7 +5195,7 @@ snapshots:
- supports-color - supports-color
- vue - vue
unplugin-auto-import@0.19.0(@nuxt/kit@3.14.1592(rollup@4.28.1))(rollup@4.28.1): unplugin-auto-import@0.19.0(@nuxt/kit@3.14.1592(rollup@4.28.1))(@vueuse/core@13.4.0(vue@3.5.13(typescript@5.6.3)))(rollup@4.28.1):
dependencies: dependencies:
'@antfu/utils': 0.7.10 '@antfu/utils': 0.7.10
'@rollup/pluginutils': 5.1.3(rollup@4.28.1) '@rollup/pluginutils': 5.1.3(rollup@4.28.1)
@ -5172,6 +5206,7 @@ snapshots:
unplugin: 2.1.0 unplugin: 2.1.0
optionalDependencies: optionalDependencies:
'@nuxt/kit': 3.14.1592(rollup@4.28.1) '@nuxt/kit': 3.14.1592(rollup@4.28.1)
'@vueuse/core': 13.4.0(vue@3.5.13(typescript@5.6.3))
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/images/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -0,0 +1,45 @@
<template>
<teleport to="body">
<Transition name="fade">
<div v-if="modelValue" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60" @click.self="close">
<div :class="`bg-white ${modalClass}`">
<header class="">
<slot name="title"></slot>
</header>
<main class="">
<slot></slot>
</main>
</div>
</div>
</Transition>
</teleport>
</template>
<script lang="ts" setup>
defineProps({
modelValue:{
type:Boolean,
default: false
},
modalClass:{
type:String,
default: ""
}
});
const emit = defineEmits(['update:modelValue']);
function close() {
emit('update:modelValue', false);
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<div class="flex items-center justify-center w-full py-2 leading-[1]">
<!-- 分页数量选择 -->
<div class="flex items-center space-x-2 text-[#68A1FF] mr-[26px]">
<span>每页</span>
<select v-model="currentPageSize" class="border rounded pl-[8px] py-[2px] border-[#68A1FF] bg-[#001C38] w-[65px]">
<option v-for="size in pageSizes" :key="size" :value="size">{{ size }}</option>
</select>
<span></span>
</div>
<!-- 页码 -->
<div class="flex items-center text-[#68A1FF]">
<button
class="px-2 bg-transparent text-[28px] leading-[1.8]"
:disabled="isFirstPage"
@click="prev"
></button>
<template v-for="page in pageList" :key="page">
<button
class="px-2 rounded text-[17px] bg-transparent h-[28px]"
:class="page === currentPage ? ' text-white' : ''"
@click="changePage(page)"
>{{ page }}</button>
</template>
<button
class="px-2 bg-transparent text-[28px] leading-[1.8]"
:disabled="isLastPage"
@click="next"
></button>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch, toRef } from 'vue';
import { useOffsetPagination } from '@vueuse/core';
const props = defineProps<{
total: number,
modelValue: number,
pageSize?: number,
pageSizes?: number[]
}>();
const emit = defineEmits(['update:modelValue', 'update:pageSize', 'change']);
const fetchData = ({ currentPage, currentPageSize }: { currentPage: number, currentPageSize: number }) => {
emit('update:modelValue', currentPage);
emit('update:pageSize', currentPageSize);
emit('change', currentPage, currentPageSize);
};
// Create local, writable refs for page and pageSize
const page = ref(props.modelValue);
const pageSize = ref(props.pageSize || 10);
// Sync local state with props when they are changed from outside
watch(() => props.modelValue, (v) => page.value = v);
watch(() => props.pageSize, (v) => pageSize.value = v || 10);
const {
currentPage,
currentPageSize,
pageCount,
isFirstPage,
isLastPage,
prev,
next,
} = useOffsetPagination({
total: toRef(props, 'total'),
page: page,
pageSize: pageSize,
onPageChange: fetchData,
onPageSizeChange: fetchData,
});
const pageSizes = computed(() => props.pageSizes || [10, 20, 50, 100]);
const PAGE_BLOCK_SIZE = 5;
const pageList = computed(() => {
const pages = [];
if (pageCount.value <= PAGE_BLOCK_SIZE) {
for (let i = 1; i <= pageCount.value; i++) pages.push(i);
return pages;
}
let start = currentPage.value - Math.floor(PAGE_BLOCK_SIZE / 2);
start = Math.max(1, start);
start = Math.min(start, pageCount.value - PAGE_BLOCK_SIZE + 1);
for (let i = start; i < start + PAGE_BLOCK_SIZE; i++) {
pages.push(i);
}
return pages;
});
function changePage(p: number) {
currentPage.value = p;
}
</script>

View File

@ -0,0 +1,15 @@
import LoadingComponent from "@/components/LoadingComponent.vue";
import ErrorComponent from "@/components/ErrorComponent.vue";
export const asyncComponentConfig = {
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
onError: (_error: Error, retry: () => void, fail: () => void, attempts: number) => {
if (attempts <= 3) {
retry();
} else {
fail();
}
}
};

View File

@ -1,6 +1,6 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import '@unocss/reset/tailwind-compat.css' import '@unocss/reset/tailwind-compat.css'
// import './style.css' import './style.css'
import 'uno.css'; import 'uno.css';
import App from './App.vue' import App from './App.vue'
// Pinia 持久化 // Pinia 持久化

View File

@ -8,4 +8,13 @@ export const privateRoutes = [
}, },
component:()=>import('../views/home.vue') component:()=>import('../views/home.vue')
}, },
{
path:"/app",
name:'app',
meta:{
title:`${new Date().getFullYear()}深泉教育招生数据可视化`,
role:['admin']
},
component:()=>import('../views/app.vue')
}
]; ];

View File

@ -1,156 +1,5 @@
* { @media (max-width: 1919px){
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
touch-action: manipulation;
-webkit-touch-callout: none;
box-sizing: border-box;
}
html{ html{
line-height: 1; font-size: calc(100vw / 93.75);
-webkit-text-size-adjust: 100%;
} }
body {
margin: 0;
}
main {
display: block;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
pre {
font-family: monospace, monospace;
font-size: 1em;
}
a {
background-color: transparent;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
img {
border-style: none;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
[type="button"],
[type="reset"],
[type="submit"],
button {
-webkit-appearance: button;
}
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner,
button::-moz-focus-inner {
border-style: none;
padding: 0;
}
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring,
button:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
padding: 0.35em 0.75em 0.625em;
}
legend {
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
progress {
vertical-align: baseline;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
details {
display: block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none;
} }

View File

@ -8,7 +8,7 @@
left: 0; left: 0;
z-index: auto; z-index: auto;
color: #fff; color: #fff;
-webkit-mask: linear-gradient(to bottom, transparent, #000); -webkit-mask: var(--mask-gradient, linear-gradient(to bottom, transparent, #000));
white-space: nowrap; white-space: nowrap;
padding-right: 4px; padding-right: 4px;
} }

15
src/utils/device.ts Normal file
View File

@ -0,0 +1,15 @@
export const usePlatformType = () => {
const userAgent = navigator.userAgent.toLowerCase();
// 判断是否为移动设备
const isMobile = /mobile|android|iphone|ipad|ipod|windows phone/i.test(userAgent);
// 判断是否为 APP 环境(这里假设 APP 环境会在 userAgent 中包含特定标识)
const isApp = /myapp|appname/i.test(userAgent); // 请根据实际 APP 的标识修改
return {
isMobile,
isApp,
isPC: !isMobile
};
}

63
src/views/app.vue Normal file
View File

@ -0,0 +1,63 @@
<template>
<div class="h-screen px-[3rem] custom-bg overflow-auto leading-[1]">
<HeaderStatic />
<TabStatic class="mt-[3rem]" />
<SixStatistics class="mt-[3rem]" />
<div class="mt-[3rem] grid grid-cols-2 gap-[2.75rem]">
<ChargingRanking />
<WinCustomer />
</div>
</div>
</template>
<script lang="ts" setup>
import { asyncComponentConfig } from "@/composables/useLazyLoad";
import { useFetchAllData } from "./composables/useFetchData";
import SixStatistics from "./appComponents/SixStatistics.vue";
import ChargingRanking from "./appComponents/ChargingRanking.vue";
import WinCustomer from "./appComponents/WinCustomer.vue";
import { usePlatformType } from "@/utils/device";
import { useUserStore } from "@/store/user";
import { getRequest } from "@/api/customFetch";
import { getUserInfoUrl } from "@/api/fetchUrl";
useFetchAllData();
const router = useRouter();
const HeaderStatic = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("./appComponents/HeaderStatic.vue"),
});
const TabStatic = defineAsyncComponent({
...asyncComponentConfig,
loader: () => import("./appComponents/TabStatic.vue"),
});
const userStore = useUserStore();
const getUserInfo = () => {
const accessToken = userStore.getAccessToken;
getRequest(getUserInfoUrl(), {}, { headers: { Authorization: `Bearer ${accessToken}` } }).then((resp) => {
if (resp.code === 401) {
router.push("/login");
}
});
};
onBeforeMount(() => {
const { isMobile } = usePlatformType();
//
if (!isMobile) {
router.push("/");
}
getUserInfo()
});
</script>
<style lang="scss" scoped>
.custom-bg {
background-image: url("/images/main-bg.png");
background-position: center;
background-size: cover;
}
</style>

View File

@ -0,0 +1,174 @@
<template>
<div class="ranking-bg aspect-[0.8/1] px-[2.5rem] pt-[2.5rem] pb-[1.5rem]">
<header class="flex items-center justify-between">
<div class="flex items-center">
<SvgComponent :content="arrowLeftSvg" class="w-[3.75rem] h-[3.75rem]" />
<div class="text-color text-[3.5rem] ml-[1.25rem] font-700" data-text="">缴费排行榜</div>
</div>
<div class="flex items-center" @click="isDrawerVisible = true">
<div class="text-[3rem] text-[#84E8FF]">更多</div>
<SvgComponent :content="moreArrowSvg" class="w-[2.2rem] h-[3.75rem]" />
</div>
</header>
<main>
<RankingTable :value="products.slice(0,5)" :columns="columns" header-class="custom-table-header" body-class="custom-table-body">
<template #rank="{ index }">
<SvgComponent :content="goldMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 0" />
<SvgComponent :content="silverMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 1" />
<SvgComponent :content="bronzeMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 2" />
<span class="text-[2.75rem] font-600" v-if="index > 2">{{ index + 1 }}</span>
</template>
<template #name="{ data }">
<span class="text-[#C0EEFF] text-[2.75rem]">{{ data.name }}</span>
</template>
<template #total="{data}">
<span class="text-[#C0EEFF] text-[2.75rem]">{{ data.total }}</span>
</template>
</RankingTable>
</main>
</div>
<MobileDrawer v-model="isDrawerVisible" :transition-duration="300" panelStyle="border-radius:8rem 8rem 0 0;background-color:#072362;">
<!-- 自定义头部 -->
<template #header>
<div class="drawer-header aspect-[10.7/1] w-full bg-[#072362] flex items-center justify-center relative rounded-[8rem_8rem_0_0]">
<div class="text-color text-[4.5rem] font-700 italic" style="--mask-gradient: linear-gradient(to top, transparent, #000)" data-text="">
缴费排行榜
</div>
<img src="/images/close.png" class="w-[3rem] h-[3rem] absolute right-[5rem] top-[3.75rem]" alt="" @click="isDrawerVisible = false" />
</div>
</template>
<!-- 自定义内容 -->
<RankingTable :value="products" :columns="columns" header-class="custom-table-header" body-class="custom-table-body">
<template #rank="{ index }">
<SvgComponent :content="goldMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 0" />
<SvgComponent :content="silverMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 1" />
<SvgComponent :content="bronzeMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 2" />
<span class="text-[2.75rem] font-600" v-if="index > 2">{{ index + 1 }}</span>
</template>
<template #name="{ data }">
<span class="text-[#C0EEFF] text-[2.75rem]">{{ data.name }}</span>
</template>
<template #total="{data}">
<span class="text-[#C0EEFF] text-[2.75rem]">{{ data.total }}</span>
</template>
</RankingTable>
</MobileDrawer>
</template>
<script lang="ts" setup>
import SvgComponent from "@/components/SvgComponent.vue";
import RankingTable from "@/components/table/RankingTable.vue";
import MobileDrawer from "./MobileDrawer.vue";
const columns = [
{ field: "rank", header: "名次", align: "justify-center", width: "6rem" },
{ field: "name", header: "姓名", align: "justify-left", width: "13.5rem" },
{ field: "total", header: "收费人数", align: "justify-center", width: "15rem" },
];
const isDrawerVisible = ref(false)
let products = ref<any[]>([]);
const chargingRankingData = inject(
"chargingRankingData",
ref<{
paymentRanks: any[];
}>({
paymentRanks: [],
}),
);
watch(
() => chargingRankingData.value,
() => {
initData();
},
);
const initData = () => {
if (chargingRankingData.value.paymentRanks) {
products.value = chargingRankingData.value.paymentRanks;
}
};
const arrowLeftSvg = ref("");
const getArrowLeftSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/arrow-left.svg?raw");
arrowLeftSvg.value = svg;
};
const goldMedalSvg = ref("");
const getGoldMedalSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/gold-medal.svg?raw");
goldMedalSvg.value = svg;
};
const silverMedalSvg = ref("");
const getSilverMedalSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/silver-medal.svg?raw");
silverMedalSvg.value = svg;
};
const bronzeMedalSvg = ref("");
const getBronzeMedalSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/bronze-medal.svg?raw");
bronzeMedalSvg.value = svg;
};
const moreArrowSvg = ref("");
const getMoreArrowSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/more-arrow.svg?raw");
moreArrowSvg.value = svg;
};
onBeforeMount(() => {
getArrowLeftSvg();
getGoldMedalSvg();
getSilverMedalSvg();
getBronzeMedalSvg();
getMoreArrowSvg();
});
</script>
<style lang="scss" scoped>
@use "@/styles/text-color.scss";
.ranking-bg {
background-image: url("/images/ranking-border.png");
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
:deep(.custom-table-header) {
tr {
background-color: rgb(14, 39, 97);
th {
padding: 2rem 1.5rem;
color: #44c1ef;
line-height: 1;
font-size: 2.75rem;
}
}
}
:deep(.custom-table-body) {
tr {
td {
padding: 2rem;
line-height: 1;
}
}
tr:nth-child(even) {
background: linear-gradient( 270deg, rgba(62,118,171,0) 0%, rgba(62, 118, 171, 0.2) 45%, rgba(62,118,171,0) 100%);
}
}
.drawer-header {
background-image: url("/images/drawer-header.png");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<div class="custom-bg mt-[5rem] aspect-[4/1] flex items-center justify-center">
<div class="mt-[3.5rem] custom-border px-[2.5rem] pt-[1.5rem] h-full">
<div class="text-[#44C1EF] italic text-[3.5rem] font-700 mb-[2.5rem]">总缴费</div>
<div class="mb-[3.75rem] z-10 font-700 leading-[1] flex items-baseline">
<span class="text-[5.5rem] italic text-color" :data-text="`${paymentData.chargeTotal || 0}`">{{ paymentData.chargeTotal || 0 }}</span>
<span class="text-[3rem] text-color" data-text=""></span>
</div>
</div>
<div class="mt-[3.5rem] custom-border px-[3.5rem] justify-items-center pt-[1.5rem] h-full">
<div class="text-[#44C1EF] italic text-[3.5rem] font-700">今日缴费</div>
<div class="font-700 leading-[1] flex items-baseline justify-center mt-[1rem] mb-[1.5rem]">
<span class="text-[5.5rem] italic text-color" :data-text="paymentData.toDayTotal">{{ paymentData.toDayTotal || 0 }}</span>
<span class="text-[3rem] text-color" data-text=""></span>
</div>
<div :class="`${paymentDifferent > 0 ? 'text-[#4AFFA2]' : 'text-[#FF4E4E]'} text-[2.5rem] flex items-center flex-wrap justify-center`">
<span class="">较昨日</span>
<SvgIcon :name="paymentDifferent > 0 ? 'arrow-up' : 'arrow-down'" class="text-[1.5rem]" />
<span>{{ Math.abs(paymentDifferent) }}</span>
</div>
</div>
<div class="mt-[3.5rem] custom-border pl-[2.25rem] pr-[2rem] justify-items-center pt-[1.5rem] h-full">
<div class="text-[#44C1EF] italic text-[3.5rem] font-700 mb-[3.5rem]">企微获客</div>
<div class="leading-[1] flex items-baseline mb-[1.5rem] z-10 font-700">
<span class="text-[4.5rem] italic text-color" :data-text="gainData.acqTotal || 0">{{ gainData.acqTotal || 0 }}</span>
<span class="text-[3rem] text-color" data-text=""></span>
</div>
</div>
<div class="mt-[3.5rem] px-[2.5rem] justify-items-center pt-[1.5rem] h-full">
<div class="text-[#44C1EF] italic text-[3.5rem] font-700">今日获客</div>
<div class="leading-[1] flex items-baseline justify-center mt-[1rem] mb-[1.5rem] z-10 font-700">
<span class="text-[4.5rem] italic text-color" :data-text="gainData.toDayAcq || 0">{{ gainData.toDayAcq || 0 }}</span>
<span class="text-[3rem] text-color" data-text=""></span>
</div>
<div :class="`${gainDifferentTody > 0 ? 'text-[#4AFFA2]' : 'text-[#FF4E4E]'} text-[2.5rem] flex items-center flex-wrap justify-center`">
<span class="">较昨日</span>
<SvgIcon :name="gainDifferentTody > 0 ? 'arrow-up' : 'arrow-down'" class="text-[1.5rem]" />
<span>{{ Math.abs(gainDifferentTody) }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import SvgIcon from "@/components/svg-icon/SvgIcon.vue";
const paymentData = inject("paymentData", ref({ chargeTotal: 0, estimatedTotal: 0, items: [], toDayTotal: 0, yesterdayTotal: 0 }));
const gainData = inject("gainData", ref({ acqTotal: 0, goalTotal: 0, toDayAcq: 0, yestertDayAcq: 0 }));
const paymentDifferent = computed(() => {
if (
Object.prototype.toString.call(paymentData.value.toDayTotal) !== "[object Undefined]" &&
Object.prototype.toString.call(paymentData.value.yesterdayTotal) !== "[object Undefined]"
) {
return paymentData.value.toDayTotal - paymentData.value.yesterdayTotal;
} else {
return 0;
}
});
const gainDifferentTody = computed(() => {
if (
Object.prototype.toString.call(gainData.value.toDayAcq) !== "[object Undefined]" &&
Object.prototype.toString.call(gainData.value.yestertDayAcq) !== "[object Undefined]"
) {
return gainData.value.toDayAcq - gainData.value.yestertDayAcq;
} else {
return 0;
}
});
</script>
<style lang="scss" scoped>
@use "@/styles/text-color.scss";
.custom-bg {
background-image: url("/images/top-bg.png");
background-position: center;
background-size: cover;
}
.custom-border {
border-right: 1px solid;
border-image: linear-gradient(180deg, #217ac600, #227cc8, #217ac600) 1 1;
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<teleport to="body">
<!-- 遮罩层 -->
<Transition name="drawer-fade">
<div
v-if="modelValue"
class="fixed top-0 left-0 w-screen h-screen bg-black/50 z-[1]"
:style="`--transition-duration: ${props.transitionDuration}ms;`"
@click="closeDrawer"
></div>
</Transition>
<!-- 抽屉面板 -->
<Transition name="drawer-slide" @after-leave="onTransitionEnd">
<div
v-if="modelValue"
class="fixed bottom-0 left-0 z-[2] flex w-screen flex-col rounded-t-lg bg-white shadow-lg"
:style="`--transition-duration: ${props.transitionDuration}ms;${panelStyle}`"
>
<header class="flex-shrink-0">
<slot name="header"></slot>
</header>
<main class="flex-grow overflow-y-auto max-h-[55vh]">
<slot></slot>
</main>
</div>
</Transition>
</teleport>
</template>
<script lang="ts" setup>
const props = defineProps({
modelValue: {
type: Boolean,
required: true,
},
transitionDuration: {
type: Number,
default: 250,
},
panelStyle:{
type:String,
default: ''
}
});
const emit = defineEmits(['update:modelValue']);
const closeDrawer = () => {
emit('update:modelValue', false);
};
//
watch(
() => props.modelValue,
(isOpen) => {
document.body.style.overflow = isOpen ? 'hidden' : '';
}
);
//
onUnmounted(() => {
document.body.style.overflow = '';
});
//
const onTransitionEnd = () => {
// console.log('Transition ended');
};
</script>
<style lang="scss" scoped>
/* Transitions are defined here to use the dynamic --transition-duration variable */
.drawer-fade-enter-active,
.drawer-fade-leave-active {
transition: opacity var(--transition-duration) ease;
}
.drawer-fade-enter-from,
.drawer-fade-leave-to {
opacity: 0;
}
.drawer-slide-enter-active,
.drawer-slide-leave-active {
transition: transform var(--transition-duration) ease;
}
.drawer-slide-enter-from,
.drawer-slide-leave-to {
transform: translateY(100%);
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="relative mt-[6.25rem] flex items-center justify-center">
<ProportionCharts :chart-data="chartData" class="w-[41.25rem] h-[41.25rem]" :width="canvasWidth" :height="canvasWidth" />
<div class="leading-[1] absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] z-3 flex items-center flex-col font-700 italic">
<div class="flex items-baseline">
<div class="text-[7rem] text-color" :data-text="offlineTotal">{{ offlineTotal }}</div>
<div class="text-[5.75rem] text-color" data-text=""></div>
</div>
<div class="text-[#FFFFFF] text-[4.5rem] text-shadow-[0px_2px_2px_rgba(12,32,72,0.42)] mt-[2rem]">线上</div>
</div>
</div>
<ul class="px-[3.75rem] grid grid-cols-2 gap-x-[2.5rem] gap-y-[2.25rem] mt-[5rem] leading-[1] pb-[11rem]">
<li class="flex items-center flex-col" v-for="item in chartData" :key="item.name">
<div class="flex items-center justify-between w-full">
<div class="w-[1.5rem] h-[1.5rem] rounded-full" :style="{ backgroundColor: item.color }"></div>
<div class="flex-1 flex items-center text-[#C0EEFF] text-[3.5rem] justify-between">
<span class="ml-[1.5rem] mr-[2rem]">{{ item.name }}</span>
<span>{{ item.value }}</span>
</div>
</div>
<div class="border-image w-full mt-[1.5rem]"></div>
</li>
</ul>
</template>
<script lang="ts" setup>
import ProportionCharts from "@/views/components/chartsComponents/ProportionCharts.vue";
const canvasWidth = ref(0);
const handleResize = () => {
canvasWidth.value = 41.25 * (document.documentElement.clientWidth / 93.75);
};
const colorList = ref(["#0783FA", "#07D1FA", "#20E6A4", "#FFD15C"]);
const askSectionData = inject("askSectionData", ref<{ offline: any[]; scource: any[] }>({ offline: [], scource: [] }));
const chartData = ref<any[]>([]);
watch(
() => askSectionData.value,
() => {
initData();
},
);
const offlineTotal = ref<any>(0);
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],
}));
offlineTotal.value = askSectionData.value.scource.filter((item) => item.tag === "线下")[0].total;
}
};
onMounted(() => {
initData();
handleResize();
window.addEventListener("resize", handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
});
</script>
<style lang="scss" scoped>
@use "@/styles/text-color.scss";
.border-image {
border: 1px solid;
border-image: linear-gradient(90deg, #217ac600, #227cc8, #217ac600) 1 1;
opacity: 0.3;
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<div class="relative mt-[6.25rem] flex items-center justify-center">
<ProportionCharts :chart-data="chartData" class="w-[41.25rem] h-[41.25rem]" :width="canvasWidth" :height="canvasWidth" />
<div class="leading-[1] absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] z-3 flex items-center flex-col font-700 italic">
<div class="flex items-baseline">
<div class="text-[7rem] text-color" :data-text="onlineTotal">{{ onlineTotal }}</div>
<div class="text-[5.75rem] text-color" data-text=""></div>
</div>
<div class="text-[#FFFFFF] text-[4.5rem] text-shadow-[0px_2px_2px_rgba(12,32,72,0.42)] mt-[2rem]">线上</div>
</div>
</div>
<ul class="px-[3.75rem] grid grid-cols-2 gap-x-[2.5rem] gap-y-[2.25rem] mt-[5rem] leading-[1] pb-[11rem]">
<li class="flex items-center flex-col" v-for="item in chartData" :key="item.name">
<div class="flex items-center justify-between w-full">
<div class="w-[1.5rem] h-[1.5rem] rounded-full" :style="{ backgroundColor: item.color }"></div>
<div class="flex-1 flex items-center text-[#C0EEFF] text-[3.5rem] justify-between">
<span class="ml-[1.5rem] mr-[2rem]">{{ item.name }}</span>
<span>{{ item.value }}</span>
</div>
</div>
<div class="border-image w-full mt-[1.5rem]"></div>
</li>
</ul>
</template>
<script lang="ts" setup>
import ProportionCharts from "@/views/components/chartsComponents/ProportionCharts.vue";
const canvasWidth = ref(0);
const handleResize = () => {
canvasWidth.value = 41.25 * (document.documentElement.clientWidth / 93.75);
};
const colorList = ref(["#0783FA", "#07D1FA", "#20E6A4", "#FFD15C", "#9A68FF"]);
const askSectionData = inject("askSectionData", ref<{ online: any[]; scource: any[] }>({ online: [], scource: [] }));
const chartData = ref<any[]>([]);
const onlineTotal = ref<any>(0);
watch(
() => askSectionData.value,
() => {
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],
}));
onlineTotal.value = askSectionData.value.scource.filter((item) => item.tag === "线上")[0].total;
}
};
onMounted(() => {
initData();
handleResize();
window.addEventListener("resize", handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
});
</script>
<style lang="scss" scoped>
@use "@/styles/text-color.scss";
.border-image {
border: 1px solid;
border-image: linear-gradient(90deg, #217ac600, #227cc8, #217ac600) 1 1;
opacity: 0.3;
}
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="aspect-[4.875/1] custom-bg p-[3.5rem] flex">
<div class="flex flex-col justify-between">
<div class="text-[#44C1EF] text-[3.75rem] italic font-700">六纬志愿获客</div>
<div class="mt-[2.25rem] flex items-center">
<span class="mr-[1rem] text-[6.5rem] text-color font-500 italic" :data-text="sixStatisticsData.total">{{ sixStatisticsData.total }}</span>
<span class="text-[3.5rem] font-500 text-color" data-text=""></span>
<SvgIcon name="arrow-up" class="text-[9px] text-[#4AFFA2] ml-[1rem]" />
</div>
</div>
<SixStatisticsChart :chartData="sixStatisticsData.items || []" class="flex-1 h-full ml-[6rem]"/>
</div>
</template>
<script setup lang="ts">
import SvgIcon from "@/components/svg-icon/SvgIcon.vue";
import SixStatisticsChart from "@/views/components/chartsComponents/SixStatisticsChart.vue";
const headerLeftSvg = ref("");
const headerRightSvg = ref("");
const getHeaderLeftSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/header-bg-left-sort.svg?raw");
headerLeftSvg.value = svg;
};
const getHeaderRightSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/header-bg-right-sort.svg?raw");
headerRightSvg.value = svg;
};
const arrowLeftSvg = ref("");
const getArrowLeftSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/arrow-left.svg?raw");
arrowLeftSvg.value = svg;
};
const sixStatisticsData = inject("sixStatisticsData", ref<{ total: number; items: { data: string; total: number }[] }>({ total: 0, items: [] }));
onBeforeMount(() => {
getHeaderLeftSvg();
getHeaderRightSvg();
getArrowLeftSvg();
});
</script>
<style scoped lang="scss">
@use "@/styles/text-color.scss";
.custom-bg {
background-image: url("/images/top-bg.png");
background-position: center;
background-size: cover;
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<div class="custom-bg aspect-[1.8/1] overflow-hidden flex flex-col">
<ul class="grid grid-cols-4 items-center mt-[5px]">
<li
@click="handleChoose(index)"
:class="`parallelogram aspect-[8/3] text-[3.25rem] flex items-center justify-center text-[#fff] font-500 ${chosenItem === index ? 'choose' : ''}`"
v-for="(item, index) in tabList"
:key="index">
<span v-if="chosenItem !== index">{{ item }}</span>
<span v-else class="text-[3.75rem] text-color font-700" :data-text="item">{{ item }}</span>
</li>
</ul>
<div class="flex-1">
<PaymentChart class="mt-[4rem]" v-if="chosenItem === 0" />
<OperatingTrendsChart :chartDataArray="acqTrend" v-if="chosenItem === 1" :need-date="false" :ledge-gap="ledgeGap" :grid="grid" :tooltipFixed="true"/>
<AskSectionChart :chart-data="askSectionData.stages || []" v-if="chosenItem === 2" :shorthand="true"/>
<StudentStatus v-if="chosenItem === 3"/>
</div>
</div>
</template>
<script lang="ts" setup>
import { getRequest } from "@/api/customFetch";
import PaymentChart from "./appEchart/PaymentChart.vue";
import OperatingTrendsChart from "@/views/components/chartsComponents/OperatingTrendsChart.vue";
import AskSectionChart from "@/views/components/chartsComponents/AskSectionChart.vue";
import StudentStatus from "./appEchart/StudentStatus.vue";
import { getAcqTrend } from "@/api/fetchUrl";
import { addRequest, runImmediatelyByKey } from "@/composables/usePolling";
const tabList = ["缴费占比", "经营趋势", "咨询学段", "学生来源"];
const askSectionData = inject("askSectionData", ref({ stages: [],}));
const chosenItem = ref(0);
const handleChoose = (index: number) => {
chosenItem.value = index;
};
const ledgeGap = ref(0);
const grid = ref({
top: 0,
left: 0,
right: 0,
bottom: 0,
});
const handleResize = () => {
let draftsSize = document.documentElement.clientWidth / 93.75;
ledgeGap.value = draftsSize * 3.75;
grid.value = { top: 50, left: 50, right: 10, bottom: 30 };
};
let DateType = 0;
const acqTrend = ref<any>([]);
const getAcqTrendData = (type: number) => {
getRequest(getAcqTrend(), { Type: type }, {}).then((resp) => {
if (resp.code === 200) {
acqTrend.value = resp.result;
}
});
};
addRequest("getAcqTrendData", getAcqTrendData, [DateType]);
runImmediatelyByKey("getAcqTrendData");
onMounted(() => {
window.addEventListener("resize", handleResize);
});
onBeforeMount(() => {
handleResize();
window.removeEventListener("resize", handleResize);
});
</script>
<style lang="scss" scoped>
@use "@/styles/text-color.scss";
.custom-bg {
background-image: url("/images/top-bg.png");
background-position: center;
background-size: cover;
}
.parallelogram {
width: 100%;
height: 7.5rem;
position: relative;
background-image: url("/images/tab-border.png");
background-position: center center;
background-size: 100%;
background-repeat: no-repeat;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
scale: 0.8;
transform: skewX(-45deg);
// background-color: red;
}
}
.parallelogram.choose {
width: calc(100% + 2.5rem);
height: 9rem;
background-image: url("/images/tab-border-choose.png");
}
</style>

View File

@ -0,0 +1,193 @@
<template>
<div class="ranking-bg aspect-[0.8/1] px-[2.5rem] pt-[2.5rem] pb-[1.5rem]">
<header class="flex items-center justify-between">
<div class="flex items-center">
<SvgComponent :content="arrowLeftSvg" class="w-[3.75rem] h-[3.75rem]" />
<div class="text-color text-[3.5rem] ml-[1.25rem] font-700" data-text="">获客排行榜</div>
</div>
<div class="flex items-center" @click="isDrawerVisible = true">
<div class="text-[3rem] text-[#84E8FF]">更多</div>
<SvgComponent :content="moreArrowSvg" class="w-[2.2rem] h-[3.75rem]" />
</div>
</header>
<main>
<RankingTable :value="products.slice(0, 5)" :columns="columns" header-class="custom-table-header" body-class="custom-table-body">
<template #rank="{ index }">
<SvgComponent :content="goldMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 0" />
<SvgComponent :content="silverMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 1" />
<SvgComponent :content="bronzeMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 2" />
<span class="text-[2.75rem] font-600" v-if="index > 2">{{ index + 1 }}</span>
</template>
<template #name="{ data }">
<span class="text-[#C0EEFF] text-[2.75rem]">{{ data.name }}</span>
</template>
<template #total="{ data }">
<span class="text-[#C0EEFF] text-[2.75rem]">{{ data.total }}</span>
</template>
</RankingTable>
</main>
</div>
<MobileDrawer v-model="isDrawerVisible" :transition-duration="300" panelStyle="border-radius:8rem 8rem 0 0;background-color:#072362;">
<!-- 自定义头部 -->
<template #header>
<div class="drawer-header aspect-[10.7/1] w-full bg-[#072362] flex items-center justify-center relative rounded-[8rem_8rem_0_0]">
<div class="text-color text-[4.5rem] font-700 italic" style="--mask-gradient: linear-gradient(to top, transparent, #000)" data-text="">
获客排行榜
</div>
<img src="/images/close.png" class="w-[3rem] h-[3rem] absolute right-[5rem] top-[3.75rem]" alt="" @click="isDrawerVisible = false" />
</div>
</template>
<!-- 自定义内容 -->
<RankingTable :value="products" :columns="columns" header-class="custom-table-header" body-class="custom-table-body">
<template #rank="{ index }">
<SvgComponent :content="goldMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 0" />
<SvgComponent :content="silverMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 1" />
<SvgComponent :content="bronzeMedalSvg" class="w-[6rem] h-[3.5rem]" v-if="index === 2" />
<span class="text-[2.75rem] font-600" v-if="index > 2">{{ index + 1 }}</span>
</template>
<template #name="{ data }">
<span class="text-[#C0EEFF] text-[2.75rem]">{{ data.name }}</span>
</template>
<template #total="{ data }">
<span class="text-[#C0EEFF] text-[2.75rem]">{{ data.total }}</span>
</template>
</RankingTable>
</MobileDrawer>
</template>
<script lang="ts" setup>
import SvgComponent from "@/components/SvgComponent.vue";
import RankingTable from "@/components/table/RankingTable.vue";
import MobileDrawer from "./MobileDrawer.vue";
const columns = [
{ field: "rank", header: "名次", align: "justify-center", width: "6rem" },
{ field: "name", header: "姓名", align: "justify-left", width: "13.5rem" },
{ field: "total", header: "获客人数", align: "justify-center", width: "15rem" },
];
const isDrawerVisible = ref(false);
let products = ref<any[]>([]);
const chargingRankingData = inject(
"chargingRankingData",
ref<{
acqRanks: any[];
}>({
acqRanks: [],
}),
);
watch(
() => chargingRankingData.value,
() => {
PageTransitionEvent;
initData();
},
);
const initData = () => {
if (chargingRankingData.value.acqRanks) {
products.value = chargingRankingData.value.acqRanks;
}
};
const headerLeftSvg = ref("");
const headerRightSvg = ref("");
const getHeaderLeftSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/header-bg-left-sort.svg?raw");
headerLeftSvg.value = svg;
};
const getHeaderRightSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/header-bg-right-sort.svg?raw");
headerRightSvg.value = svg;
};
const arrowLeftSvg = ref("");
const getArrowLeftSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/arrow-left.svg?raw");
arrowLeftSvg.value = svg;
};
const moreArrowSvg = ref("");
const getMoreArrowSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/more-arrow.svg?raw");
moreArrowSvg.value = svg;
};
const goldMedalSvg = ref("");
const getGoldMedalSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/gold-medal.svg?raw");
goldMedalSvg.value = svg;
};
const silverMedalSvg = ref("");
const getSilverMedalSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/silver-medal.svg?raw");
silverMedalSvg.value = svg;
};
const bronzeMedalSvg = ref("");
const getBronzeMedalSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/bronze-medal.svg?raw");
bronzeMedalSvg.value = svg;
};
onBeforeMount(() => {
getHeaderLeftSvg();
getHeaderRightSvg();
getArrowLeftSvg();
getMoreArrowSvg();
getGoldMedalSvg();
getSilverMedalSvg();
getBronzeMedalSvg();
initData();
});
</script>
<style lang="scss" scoped>
@use "@/styles/text-color.scss";
.ranking-bg {
background-image: url("/images/ranking-border.png");
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
:deep(.custom-table-header) {
tr {
background-color: rgb(14, 39, 97);
th {
padding: 2rem 1.5rem;
color: #44c1ef;
line-height: 1;
font-size: 2.75rem;
}
}
}
:deep(.custom-table-body) {
tr {
td {
padding: 2rem;
line-height: 1;
}
}
tr:nth-child(even) {
background: linear-gradient(270deg, rgba(62, 118, 171, 0) 0%, rgba(62, 118, 171, 0.2) 45%, rgba(62, 118, 171, 0) 100%);
}
}
.drawer-header {
background-image: url("/images/drawer-header.png");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<div class="flex items-stretch pl-[6rem]">
<div class="relative flex items-center justify-center">
<ProportionCharts :chart-data="chartData" class="w-[31rem] h-[31rem]" :width="canvasWidth" :height="canvasWidth" />
<div class="leading-[1] absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] z-3 flex items-center flex-col font-700 italic">
<div class="flex items-baseline">
<div class="text-[4.5rem] text-color" :data-text="total">{{ total }}</div>
<div class="text-[2.75rem] text-color" data-text=""></div>
</div>
</div>
</div>
<ul class="flex flex-col justify-evenly flex-1 h-[31rem] px-[6.5rem]">
<li class="flex items-center flex-wrap" v-for="item in chartData" :key="item.name">
<div class="w-[1.5rem] h-[1.5rem] rounded-full" :style="{ backgroundColor: item.color }"></div>
<div class="flex-1 flex items-center text-[#C0EEFF] text-[3rem] justify-between">
<span class="ml-[1rem] mr-[2.25rem]">{{ item.name }}</span>
<span class="w-max break-keep">{{ item.value }}</span>
</div>
<div class="border-image w-full mt-[1.5rem]"></div>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import ProportionCharts from "@/views/components/chartsComponents/ProportionCharts.vue";
const chartData = ref<any[]>([]);
const paymentData = inject("paymentData", ref({ chargeTotal: 0, estimatedTotal: 0, items: [] }));
const colors = ["#0783FA", "#07D1FA", "#20E6A4", "#FFD15C", "#9A68FF"];
const total = ref(0);
watchEffect(() => {
if (paymentData.value.items) {
chartData.value = paymentData.value.items.map((item: any, index) => {
total.value += item.value;
return { ...item, color: colors[index % colors.length] };
});
}
});
const canvasWidth = ref(0);
const handleResize = () => {
canvasWidth.value = 31 * (document.documentElement.clientWidth / 93.75);
};
onMounted(() => {
handleResize();
window.addEventListener("resize", handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
});
</script>
<style lang="scss" scoped>
@use "@/styles/text-color.scss";
.border-image {
border: 1px solid;
border-image: linear-gradient(90deg, #217ac600, #227cc8, #217ac600) 1 1;
opacity: 0.3;
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<div class="w-full h-full flex">
<StudentSourceChart class="w-full h-full" :chartData="chartData" :ringSize="0.8" />
<ul class="ml-[4rem] pt-[4rem] pr-[3rem]">
<li class="flex flex-col w-[32.5rem]">
<div class="flex items-center justify-between">
<div class="text-[3rem] text-[#C7F0FF]">未标记来源</div>
<span class="text-[3.5rem] font-500 text-color" :data-text="`${unMarkTotal}人`">{{ unMarkTotal }}</span>
</div>
<div class="border-image w-full mt-[3rem]"></div>
</li>
<li class="flex flex-col w-[32.5rem] mt-[3rem]">
<div class="flex items-center justify-between">
<div class="text-[3rem] text-[#C7F0FF]">线上来源</div>
<div class="flex items-center" @click="handleOpenDrawer('线上详情')">
<span class="text-[3.5rem] font-500 text-color" :data-text="`${onlineTotal}人`">{{ onlineTotal }}</span>
<SvgComponent :content="moreArrowSvg" class="w-[14px] h-[22px]" />
</div>
</div>
<div class="border-image w-full mt-[3rem]"></div>
</li>
<li class="flex flex-col w-[32.5rem] mt-[3rem]">
<div class="flex items-center justify-between">
<div class="text-[3rem] text-[#C7F0FF]">线下来源</div>
<div class="flex items-center" @click="handleOpenDrawer('线下详情')">
<span class="text-[3.5rem] font-500 text-color" :data-text="`${offlineTotal}人`">{{ offlineTotal }}</span>
<SvgComponent :content="moreArrowSvg" class="w-[14px] h-[22px]" />
</div>
</div>
</li>
</ul>
</div>
<MobileDrawer v-model="isDrawerVisible" :transition-duration="300" panelStyle="border-radius:8rem 8rem 0 0;background-color:#072362;">
<!-- 自定义头部 -->
<template #header>
<div class="drawer-header aspect-[10.7/1] w-full bg-[#072362] flex items-center justify-center relative rounded-[8rem_8rem_0_0]">
<div class="text-color text-[4.5rem] font-700 italic" style="--mask-gradient: linear-gradient(to top, transparent, #000)" :data-text="drawerTitle">
{{ drawerTitle }}
</div>
<img src="/images/close.png" class="w-[3rem] h-[3rem] absolute right-[5rem] top-[3.75rem]" alt="" @click="isDrawerVisible = false" />
</div>
</template>
<!-- 自定义内容 -->
<OnlineStatic v-if="drawerTitle === '线'" />
<OfflineStatic v-else />
</MobileDrawer>
</template>
<script lang="ts" setup>
import StudentSourceChart from "@/views/components/chartsComponents/StudentSourceChart.vue";
import SvgComponent from "@/components/SvgComponent.vue";
import MobileDrawer from "../MobileDrawer.vue";
import OnlineStatic from "../OnlineStatic.vue";
import OfflineStatic from "../OfflineStatic.vue";
const askSectionData = inject("askSectionData", ref({ online: [], offline: [] }));
const gainData = inject("gainData", ref({ acqTotal: 0 }));
const onlineTotal = ref(0);
const offlineTotal = ref(0);
const unMarkTotal = ref(0);
const chartData = ref<any[]>([]);
const isDrawerVisible = ref(false);
const drawerTitle = ref("线上详情");
const handleOpenDrawer = (title:string) => {
isDrawerVisible.value = true;
drawerTitle.value = title
}
const initData = () => {
if (askSectionData.value.online && askSectionData.value.online.length > 0) {
onlineTotal.value = askSectionData.value.online.reduce((acc, curr: any) => acc + curr.total, 0);
}
if (askSectionData.value.offline && askSectionData.value.offline.length > 0) {
offlineTotal.value = askSectionData.value.offline.reduce((acc, curr: any) => acc + curr.total, 0);
}
if (gainData.value.acqTotal && askSectionData.value.offline && askSectionData.value.online) {
unMarkTotal.value = gainData.value.acqTotal - offlineTotal.value - onlineTotal.value;
}
chartData.value = [
{ name: "线下", value: offlineTotal.value, itemStyle: { color: "rgba(147, 219, 255, 1)" } },
{ name: "线上", value: onlineTotal.value, itemStyle: { color: "rgb(79, 214, 169)" } },
{ name: "未标记", value: unMarkTotal.value, itemStyle: { color: "#FF4E4E" } },
];
};
const moreArrowSvg = ref("");
const getMoreArrowSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/more-arrow.svg?raw");
moreArrowSvg.value = svg;
};
watch(
() => askSectionData.value,
() => {
initData();
},
{ immediate: true },
);
onBeforeMount(() => {
getMoreArrowSvg();
});
</script>
<style lang="scss" scoped>
@use "@/styles/text-color.scss";
.border-image {
border: 1px solid;
border-image: linear-gradient(90deg, #217ac600, #227cc8, #217ac600) 1 1;
opacity: 0.3;
}
:deep(.particle-base) {
width: 50rem;
height: 21rem;
}
.drawer-header {
background-image: url("/images/drawer-header.png");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
</style>

View File

@ -9,12 +9,12 @@
</div> </div>
</div> </div>
<div class="w-[296px] h-[calc(100%-36px)] flex flex-col"> <div class="w-[296px] h-[calc(100%-36px)] flex flex-col">
<div class="flex items-center justify-end mr-[13px] transform-translate-y-[-50%] h-0"> <div class="flex items-center justify-end mr-[13px] transform-translate-y-[-50%] h-0" @click="show = true">
<div class="text-[15px] text-[#84E8FF]">更多</div> <div class="text-[15px] text-[#84E8FF]">更多</div>
<SvgComponent :content="moreArrowSvg" class="w-[14px] h-[22px]" /> <SvgComponent :content="moreArrowSvg" class="w-[14px] h-[22px]" />
</div> </div>
<div class="mt-[26px] mx-[16px]"> <div class="mt-[26px] mx-[16px]">
<RankingTable :value="products" :columns="columns" header-class="custom-table-header" body-class="custom-table-body"> <RankingTable :value="products.slice(0,5)" :columns="columns" header-class="custom-table-header" body-class="custom-table-body">
<template #rank="{ index }"> <template #rank="{ index }">
<SvgComponent :content="goldMedalSvg" class="w-[34px] h-[20px]" v-if="index === 0" /> <SvgComponent :content="goldMedalSvg" class="w-[34px] h-[20px]" v-if="index === 0" />
<SvgComponent :content="silverMedalSvg" class="w-[34px] h-[20px]" v-if="index === 1" /> <SvgComponent :content="silverMedalSvg" class="w-[34px] h-[20px]" v-if="index === 1" />
@ -28,11 +28,59 @@
</div> </div>
</div> </div>
</div> </div>
<Modal v-model="show" modal-class="modal-class">
<template #title>
<div class="modal-header h-[40px] w-full bg-[#051b36] flex items-center justify-center relative">
<div class="text-color text-[24px] font-700 italic" style="--mask-gradient: linear-gradient(to top, transparent, #000)" data-text="">
缴费排行榜
</div>
<img src="/images/close-clip.png" class="w-[32px] h-[32px] absolute right-0 top-0 cursor-pointer" alt="" @click="show = false" />
</div>
</template>
<RankingTable
:value="paginatedProducts"
class="bg-[#051b36] pl-[25px] pr-[35px] pt-[15px] max-h-[70vh] overflow-auto"
:columns="columns"
header-class="modal-table-header"
body-class="modal-table-body">
<template #rank="{ index }">
<img src="@/assets/svg-img/gold-medal.svg" class="w-[34px] h-[20px]" v-if="index === 0" alt="" />
<img src="@/assets/svg-img/silver-medal.svg" class="w-[34px] h-[20px]" v-if="index === 1" alt="" />
<img src="@/assets/svg-img/bronze-medal.svg" class="w-[34px] h-[20px]" v-if="index === 2" alt="" />
<span class="text-[14px] font-600" v-if="index > 2">{{ index + 1 }}</span>
</template>
<template #name="{ data }">
<span class="text-[#C0EEFF] text-[14px]">{{ data.name }}</span>
</template>
</RankingTable>
<Pagination
v-model="page"
:total="products.length"
v-model:pageSize="pageSize"
:pageSizes="[15, 20, 50, 100]"
@change="
(p, s) => {
page = p;
pageSize = s;
}
" />
</Modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import SvgComponent from "@/components/SvgComponent.vue"; import SvgComponent from "@/components/SvgComponent.vue";
import RankingTable from "@/components/table/RankingTable.vue"; import RankingTable from "@/components/table/RankingTable.vue";
import Modal from "@/components/modal/Modal.vue";
import Pagination from "@/components/pagination/index.vue";
const page = ref(1);
const pageSize = ref(15);
const paginatedProducts = computed(() => {
const start = (page.value - 1) * pageSize.value;
const end = start + pageSize.value;
return products.value.slice(start, end);
});
const columns = [ const columns = [
{ field: "rank", header: "名次", align: "justify-center", width: "68px" }, { field: "rank", header: "名次", align: "justify-center", width: "68px" },
@ -40,6 +88,8 @@
{ field: "total", header: "收费人数", align: "justify-center", width: "96px" }, { field: "total", header: "收费人数", align: "justify-center", width: "96px" },
]; ];
const show = ref(false);
let products = ref<any[]>([]); let products = ref<any[]>([]);
const chargingRankingData = inject( const chargingRankingData = inject(
"chargingRankingData", "chargingRankingData",
@ -140,6 +190,7 @@
color: #44c1ef; color: #44c1ef;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
font-size: 14px;
} }
} }
} }
@ -149,7 +200,48 @@
td { td {
padding: 10px; padding: 10px;
line-height: 1; line-height: 1;
font-size: 14px;
} }
} }
} }
.modal-header {
background-image: url("/images/drawer-header.png");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
</style>
<style lang="scss">
.modal-class {
width: 560px;
border: 1px solid #0e2744;
background-color: #051b36;
}
.modal-table-header {
background-color: #0d2744;
.cell-content {
padding-top: 7px;
padding-bottom: 7px;
color: #44c1ef;
font-size: 14px;
}
}
.modal-table-body {
tr {
td {
div {
padding-top: 10px;
padding-bottom: 10px;
}
}
}
tr:nth-child(even) {
background-color: #0d2744;
}
}
</style> </style>

View File

@ -11,32 +11,35 @@
<div class="w-full h-[calc(100%-36px)] mt-[36px] flex flex-col"> <div class="w-full h-[calc(100%-36px)] mt-[36px] flex flex-col">
<StudentSourceChart class="w-full h-full" :chartData="chartData" :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 justify-between mx-[20px] my-[33px]">
<div class="flex items-center"> <div class="flex items-center flex-col">
<div class="flex flex-col ml-[9px] items-center justify-center"> <div class="flex flex-col ml-[9px] items-center justify-center">
<div class="font-700 flex items-baseline"> <div class="font-700 flex items-baseline mb-[2px]">
<span class="text-[18px] text-color" :data-text="onlineTotal">{{ onlineTotal }}</span> <span class="text-[18px] text-color" :data-text="onlineTotal">{{ onlineTotal }}</span>
<span class="text-[14px] text-color" data-text=""></span> <span class="text-[14px] text-color" data-text=""></span>
</div> </div>
<span class="text-[#C7F0FF] text-[12px]">线上来源</span> <span class="text-[#C7F0FF] text-[12px]">线上来源</span>
</div> </div>
<div class="border-image w-full mt-[6px]"></div>
</div> </div>
<div class="flex items-center"> <div class="flex items-center flex-col">
<div class="flex flex-col ml-[9px] items-center justify-center"> <div class="flex flex-col ml-[9px] items-center justify-center">
<div class="font-700 flex items-baseline"> <div class="font-700 flex items-baseline mb-[2px]">
<span class="text-[18px] text-color" :data-text="offlineTotal">{{ offlineTotal }}</span> <span class="text-[18px] text-color" :data-text="offlineTotal">{{ offlineTotal }}</span>
<span class="text-[14px] text-color" data-text=""></span> <span class="text-[14px] text-color" data-text=""></span>
</div> </div>
<span class="text-[#C7F0FF] text-[12px]">线下来源</span> <span class="text-[#C7F0FF] text-[12px]">线下来源</span>
</div> </div>
<div class="border-image w-full mt-[6px]"></div>
</div> </div>
<div class="flex items-center"> <div class="flex items-center flex-col">
<div class="flex flex-col ml-[9px] items-center justify-center"> <div class="flex flex-col ml-[9px] items-center justify-center">
<div class="font-700 flex items-baseline"> <div class="font-700 flex items-baseline mb-[2px]">
<span class="text-[18px] text-color" :data-text="unMarkTotal">{{ unMarkTotal }}</span> <span class="text-[18px] text-color" :data-text="unMarkTotal">{{ unMarkTotal }}</span>
<span class="text-[14px] text-color" data-text=""></span> <span class="text-[14px] text-color" data-text=""></span>
</div> </div>
<span class="text-[#C7F0FF] text-[12px]">未标记</span> <span class="text-[#C7F0FF] text-[12px]">未标记</span>
</div> </div>
<div class="border-image w-full mt-[6px]"></div>
</div> </div>
</div> </div>
</div> </div>
@ -129,4 +132,10 @@
border-image-source: url("src/assets/svg-img/border-image.png"); border-image-source: url("src/assets/svg-img/border-image.png");
border-style: solid; border-style: solid;
} }
.border-image {
border: 1px solid;
border-image: linear-gradient(90deg, #217ac600, #227cc8, #217ac600) 1 1;
opacity: 0.3;
}
</style> </style>

View File

@ -9,12 +9,12 @@
</div> </div>
</div> </div>
<div class="w-[296px] h-[calc(100%-36px)] flex flex-col"> <div class="w-[296px] h-[calc(100%-36px)] flex flex-col">
<div class="flex items-center justify-end mr-[13px] transform-translate-y-[-50%] h-0"> <div class="flex items-center justify-end mr-[13px] transform-translate-y-[-50%] h-0" @click="show = true">
<div class="text-[15px] text-[#84E8FF]">更多</div> <div class="text-[15px] text-[#84E8FF]">更多</div>
<SvgComponent :content="moreArrowSvg" class="w-[14px] h-[22px]" /> <SvgComponent :content="moreArrowSvg" class="w-[14px] h-[22px]" />
</div> </div>
<div class="mt-[26px] mx-[16px]"> <div class="mt-[26px] mx-[16px]">
<RankingTable :value="products" :columns="columns" header-class="custom-table-header" body-class="custom-table-body"> <RankingTable :value="products.slice(0,5)" :columns="columns" header-class="custom-table-header" body-class="custom-table-body">
<template #rank="{ index }"> <template #rank="{ index }">
<SvgComponent :content="goldMedalSvg" class="w-[34px] h-[20px]" v-if="index === 0" /> <SvgComponent :content="goldMedalSvg" class="w-[34px] h-[20px]" v-if="index === 0" />
<SvgComponent :content="silverMedalSvg" class="w-[34px] h-[20px]" v-if="index === 1" /> <SvgComponent :content="silverMedalSvg" class="w-[34px] h-[20px]" v-if="index === 1" />
@ -28,12 +28,62 @@
</div> </div>
</div> </div>
</div> </div>
<Modal v-model="show" modal-class="modal-class">
<template #title>
<div class="modal-header h-[40px] w-full bg-[#051b36] flex items-center justify-center relative">
<div class="text-color text-[24px] font-700 italic" style="--mask-gradient: linear-gradient(to top, transparent, #000)" data-text="">
获客排行榜
</div>
<img src="/images/close-clip.png" class="w-[32px] h-[32px] absolute right-0 top-0 cursor-pointer" alt="" @click="show = false" />
</div>
</template>
<RankingTable
:value="paginatedProducts"
class="bg-[#051b36] pl-[25px] pr-[35px] pt-[15px] max-h-[70vh] overflow-auto"
:columns="columns"
header-class="modal-table-header"
body-class="modal-table-body">
<template #rank="{ index }">
<img src="@/assets/svg-img/gold-medal.svg" class="w-[34px] h-[20px]" v-if="index === 0" alt="" />
<img src="@/assets/svg-img/silver-medal.svg" class="w-[34px] h-[20px]" v-if="index === 1" alt="" />
<img src="@/assets/svg-img/bronze-medal.svg" class="w-[34px] h-[20px]" v-if="index === 2" alt="" />
<span class="text-[14px] font-600" v-if="index > 2">{{ index + 1 }}</span>
</template>
<template #name="{ data }">
<span class="text-[#C0EEFF] text-[14px]">{{ data.name }}</span>
</template>
</RankingTable>
<Pagination
v-model="page"
:total="products.length"
v-model:pageSize="pageSize"
:pageSizes="[15, 20, 50, 100]"
@change="
(p, s) => {
page = p;
pageSize = s;
}
" />
</Modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import SvgComponent from "@/components/SvgComponent.vue"; import SvgComponent from "@/components/SvgComponent.vue";
import RankingTable from "@/components/table/RankingTable.vue"; import RankingTable from "@/components/table/RankingTable.vue";
import Modal from "@/components/modal/Modal.vue";
import Pagination from "@/components/pagination/index.vue";
const page = ref(1);
const pageSize = ref(15);
const show = ref(false);
const paginatedProducts = computed(() => {
const start = (page.value - 1) * pageSize.value;
const end = start + pageSize.value;
return products.value.slice(start, end);
});
const columns = [ const columns = [
{ field: "rank", header: "名次", align: "justify-center", width: "68px" }, { field: "rank", header: "名次", align: "justify-center", width: "68px" },
{ field: "name", header: "姓名", align: "justify-left", width: "100px" }, { field: "name", header: "姓名", align: "justify-left", width: "100px" },
@ -141,6 +191,7 @@
color: #44c1ef; color: #44c1ef;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
font-size: 14px;
} }
} }
} }
@ -150,7 +201,48 @@
td { td {
padding: 10px; padding: 10px;
line-height: 1; line-height: 1;
font-size: 14px;
} }
} }
} }
.modal-header {
background-image: url("/images/drawer-header.png");
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
</style>
<style lang="scss">
.modal-class {
width: 560px;
border: 1px solid #0e2744;
background-color: #051b36;
}
.modal-table-header {
background-color: #0d2744;
.cell-content {
padding-top: 7px;
padding-bottom: 7px;
color: #44c1ef;
font-size: 14px;
}
}
.modal-table-body {
tr {
td {
div {
padding-top: 10px;
padding-bottom: 10px;
}
}
}
tr:nth-child(even) {
background-color: #0d2744;
}
}
</style> </style>

View File

@ -3,6 +3,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { convertNumber } from "@/utils/convertNumber";
import { usePlatformType } from "@/utils/device";
import * as echarts from "echarts"; import * as echarts from "echarts";
interface DataItem { interface DataItem {
@ -10,9 +12,16 @@
total: number; total: number;
} }
const props = defineProps<{ const props = defineProps({
chartData: DataItem[]; chartData:{
}>(); type:Array<DataItem>,
default: () => []
},
shorthand:{
type:Boolean,
default: false
},
});
const renderItem = (params: any, api: { coord: (arg0: any[]) => any[]; value: (arg0: number) => any; style: () => any }) => { const renderItem = (params: any, api: { coord: (arg0: any[]) => any[]; value: (arg0: number) => any; style: () => any }) => {
// //
@ -96,10 +105,11 @@
if (!chartDom.value) return; if (!chartDom.value) return;
myChart = echarts.init(chartDom.value); myChart = echarts.init(chartDom.value);
const {isMobile} = usePlatformType()
// //
const WIDTH = 10; const WIDTH = isMobile ? 6 : 10;
// //
const OBLIQUE_ANGLE_HEIGHT = 3.5; const OBLIQUE_ANGLE_HEIGHT = isMobile ? 2 : 3.5;
const leftShape = echarts.graphic.extendShape({ const leftShape = echarts.graphic.extendShape({
buildPath(ctx, shape) { buildPath(ctx, shape) {
const { topBasicsYAxis, bottomYAxis, basicsXAxis } = shape; const { topBasicsYAxis, bottomYAxis, basicsXAxis } = shape;
@ -171,25 +181,29 @@
// updateChartData // updateChartData
const options = { const options = {
grid: { grid: {
top: "12%", top: props.shorthand ? 30:40,
left: "5%", left: props.shorthand ? 40 : 60,
right: "3%", right: props.shorthand ?20 : 30,
bottom: "15%", bottom: props.shorthand ?30:45,
}, },
xAxis: { xAxis: {
type: "category", type: "category",
data: [], data: [],
axisLabel: { axisLabel: {
formatter: (value: any) => { formatter: (value: any) => {
if(props.shorthand){
return value.slice(0,2)
}else{
if (value.length <= 4) return value; if (value.length <= 4) return value;
const arr = []; const arr = [];
for (let i = 0; i < value.length; i += 4) { for (let i = 0; i < value.length; i += 4) {
arr.push(value.slice(i, i + 4)); arr.push(value.slice(i, i + 4));
} }
return arr.join('\n'); return arr.join('\n');
}
}, },
color: "#C0EEFF", color: "#C0EEFF",
fontSize: 14, fontSize: props.shorthand ? 10 : 14,
interval: 0, interval: 0,
}, },
nameTextStyle: { nameTextStyle: {
@ -206,7 +220,7 @@
nameLocation: "end", nameLocation: "end",
nameTextStyle: { nameTextStyle: {
color: "#C0EEFF", color: "#C0EEFF",
fontSize: 14, fontSize: props.shorthand ? 10 : 14,
padding: [0, 5, 0, 0], padding: [0, 5, 0, 0],
align: "right", align: "right",
}, },
@ -216,7 +230,14 @@
interval: 20, interval: 20,
axisLabel: { axisLabel: {
color: "#C0EEFF", color: "#C0EEFF",
fontSize: 14, fontSize: props.shorthand ? 10 : 14,
formatter: (value: any) => {
if(props.shorthand){
return convertNumber(value)
}else{
return value
}
},
}, },
axisLine: { axisLine: {
show: true, show: true,
@ -245,7 +266,7 @@
label: { label: {
show: true, show: true,
position: "top", position: "top",
fontSize: 12, fontSize: props.shorthand ? 10 : 12,
color: "rgba(192, 238, 255, 1)", color: "rgba(192, 238, 255, 1)",
}, },
tooltip: { tooltip: {
@ -258,7 +279,6 @@
}, },
], ],
}; };
myChart.setOption(options); myChart.setOption(options);
// //

View File

@ -1,5 +1,5 @@
<template> <template>
<div ref="chartDom" class="w-full h-full"></div> <div ref="chartDom" class="w-full h-full" :style="`--tip-width:${tooltipWidth}`"></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -12,8 +12,33 @@
const props = defineProps({ const props = defineProps({
chartDataArray: { chartDataArray: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
needDate: {
type: Boolean,
default: true,
},
grid: {
type: Object,
default: () => ({
top: 40,
left: 60,
right: 30,
bottom: 30,
}),
},
ledgeGap: {
type: Number,
default: 40,
},
tooltipFixed:{
type:Boolean,
default:false
},
tooltipWidth:{
type:String,
default: '184px'
}
}); });
const emits = defineEmits(["dateChange"]); const emits = defineEmits(["dateChange"]);
@ -58,7 +83,8 @@ function svgToPercentEncodedDataUrl(svg: string) {
} }
// //
const processChartData = (dataArray: ChartTypeData[], mode: string): ChartDataFormat => { const processChartData = (dataArray: ChartTypeData[], mode: string): ChartDataFormat => {
if (!dataArray || dataArray.length === 0) return { if (!dataArray || dataArray.length === 0)
return {
dates: [], dates: [],
seriesData: [ seriesData: [
{ name: "获客", data: [], color: "#20E6A4" }, { name: "获客", data: [], color: "#20E6A4" },
@ -82,8 +108,8 @@ function svgToPercentEncodedDataUrl(svg: string) {
// //
let sortedDates = Array.from(allDates).sort((a: string, b: string) => { let sortedDates = Array.from(allDates).sort((a: string, b: string) => {
// MM-DD // MM-DD
const [monthA, dayA] = a.split('-').map(Number); const [monthA, dayA] = a.split("-").map(Number);
const [monthB, dayB] = b.split('-').map(Number); const [monthB, dayB] = b.split("-").map(Number);
if (monthA !== monthB) return monthA - monthB; if (monthA !== monthB) return monthA - monthB;
return dayA - dayB; return dayA - dayB;
}); });
@ -106,7 +132,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
// //
const formattedDates = sortedDates.map((date: string) => { const formattedDates = sortedDates.map((date: string) => {
const [month, day] = date.split('-'); const [month, day] = date.split("-");
return `${month}.${day}`; return `${month}.${day}`;
}); });
@ -122,7 +148,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
// //
dataArray.forEach((typeData: ChartTypeData, typeIndex: number) => { dataArray.forEach((typeData: ChartTypeData, typeIndex: number) => {
if (typeIndex < seriesData.length) { if (typeIndex < seriesData.length) {
const dateItem = typeData.items.find(item => item.date === date); const dateItem = typeData.items.find((item) => item.date === date);
seriesData[typeIndex].data.push(dateItem ? dateItem.value : 0); seriesData[typeIndex].data.push(dateItem ? dateItem.value : 0);
} }
}); });
@ -155,7 +181,6 @@ function svgToPercentEncodedDataUrl(svg: string) {
const legendIcons = ref<{ [key: string]: string }>({}); const legendIcons = ref<{ [key: string]: string }>({});
// //
const initLegendIcons = () => { const initLegendIcons = () => {
chartData.value.seriesData.forEach((item) => { chartData.value.seriesData.forEach((item) => {
@ -236,7 +261,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
// y // y
const calculateMaxValue = () => { const calculateMaxValue = () => {
let maxValue = 0; let maxValue = 0;
chartData.value.seriesData.forEach(series => { chartData.value.seriesData.forEach((series) => {
const seriesMax = Math.max(...series.data); const seriesMax = Math.max(...series.data);
maxValue = Math.max(maxValue, seriesMax); maxValue = Math.max(maxValue, seriesMax);
}); });
@ -256,12 +281,9 @@ function svgToPercentEncodedDataUrl(svg: string) {
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="72" height="28" viewBox="0 0 72 28"><defs><radialGradient cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" id="master_svg0_85_00187" gradientTransform="translate(36 28) rotate(90) scale(16.125258922576904 182.11454980862436)"><stop offset="0%" stop-color="#FFCE4F" stop-opacity="0.5749016404151917"/><stop offset="99.90439414978027%" stop-color="#E0BF00" stop-opacity="0"/></radialGradient><linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="master_svg1_85_00188"><stop offset="0%" stop-color="#E0BF00" stop-opacity="0.4000000059604645"/><stop offset="100%" stop-color="#E0BF00" stop-opacity="0"/></linearGradient><linearGradient x1="0.5" y1="1" x2="0.5" y2="1.9133386611938477" id="master_svg2_85_00190"><stop offset="0%" stop-color="#FFF7A6" stop-opacity="1"/><stop offset="100%" stop-color="#046172" stop-opacity="0"/></linearGradient></defs><g><g><path d="M0,14.5113C0,8.31958,0,6.07937,0.871948,4.36808C1.63894,2.86278,2.86278,1.63893,4.36808,0.871948C6.07937,0,8.31958,0,14.5113,0L72,0L72,13.4887C72,19.6804,72,21.9206,71.1281,23.6319C70.3611,25.1372,69.1372,26.3611,67.6319,27.1281C65.9206,28,63.6804,28,57.4887,28L0,28L0,14.5113Z" fill="url(#master_svg0_85_00187)" fill-opacity="1"/><path d="M0,14.5113C0,8.31958,0,6.07937,0.871948,4.36808C1.63894,2.86278,2.86278,1.63893,4.36808,0.871948C6.07937,0,8.31958,0,14.5113,0L72,0L72,13.4887C72,19.6804,72,21.9206,71.1281,23.6319C70.3611,25.1372,69.1372,26.3611,67.6319,27.1281C65.9206,28,63.6804,28,57.4887,28L0,28L0,14.5113Z" fill="url(#master_svg1_85_00188)" fill-opacity="1"/></g><g><path d="M0,14.5113C0,8.31958,0,6.07937,0.871948,4.36808C1.63894,2.86278,2.86278,1.63893,4.36808,0.871948C6.07937,0,8.31958,0,14.5113,0L72,0L72,13.4887C72,19.6804,72,21.9206,71.1281,23.6319C70.3611,25.1372,69.1372,26.3611,67.6319,27.1281C65.9206,28,63.6804,28,57.4887,28L0,28L0,14.5113ZM14.5113,1L71,1L71,13.4887Q71,18.6574,70.8935,20.1702Q70.753,22.1653,70.237,23.1779Q69.2045,25.2045,67.1779,26.237Q66.1653,26.753,64.1702,26.8935Q62.6574,27,57.4887,27L1,27L1,14.5113Q1,9.34262,1.10652,7.82977Q1.247,5.83468,1.76295,4.82207Q2.79553,2.79553,4.82207,1.76295Q5.83468,1.247,7.82977,1.10652Q9.34262,1,14.5113,1Z" fill-rule="evenodd" fill="url(#master_svg2_85_00190)" fill-opacity="1"/></g></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="72" height="28" viewBox="0 0 72 28"><defs><radialGradient cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" id="master_svg0_85_00187" gradientTransform="translate(36 28) rotate(90) scale(16.125258922576904 182.11454980862436)"><stop offset="0%" stop-color="#FFCE4F" stop-opacity="0.5749016404151917"/><stop offset="99.90439414978027%" stop-color="#E0BF00" stop-opacity="0"/></radialGradient><linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="master_svg1_85_00188"><stop offset="0%" stop-color="#E0BF00" stop-opacity="0.4000000059604645"/><stop offset="100%" stop-color="#E0BF00" stop-opacity="0"/></linearGradient><linearGradient x1="0.5" y1="1" x2="0.5" y2="1.9133386611938477" id="master_svg2_85_00190"><stop offset="0%" stop-color="#FFF7A6" stop-opacity="1"/><stop offset="100%" stop-color="#046172" stop-opacity="0"/></linearGradient></defs><g><g><path d="M0,14.5113C0,8.31958,0,6.07937,0.871948,4.36808C1.63894,2.86278,2.86278,1.63893,4.36808,0.871948C6.07937,0,8.31958,0,14.5113,0L72,0L72,13.4887C72,19.6804,72,21.9206,71.1281,23.6319C70.3611,25.1372,69.1372,26.3611,67.6319,27.1281C65.9206,28,63.6804,28,57.4887,28L0,28L0,14.5113Z" fill="url(#master_svg0_85_00187)" fill-opacity="1"/><path d="M0,14.5113C0,8.31958,0,6.07937,0.871948,4.36808C1.63894,2.86278,2.86278,1.63893,4.36808,0.871948C6.07937,0,8.31958,0,14.5113,0L72,0L72,13.4887C72,19.6804,72,21.9206,71.1281,23.6319C70.3611,25.1372,69.1372,26.3611,67.6319,27.1281C65.9206,28,63.6804,28,57.4887,28L0,28L0,14.5113Z" fill="url(#master_svg1_85_00188)" fill-opacity="1"/></g><g><path d="M0,14.5113C0,8.31958,0,6.07937,0.871948,4.36808C1.63894,2.86278,2.86278,1.63893,4.36808,0.871948C6.07937,0,8.31958,0,14.5113,0L72,0L72,13.4887C72,19.6804,72,21.9206,71.1281,23.6319C70.3611,25.1372,69.1372,26.3611,67.6319,27.1281C65.9206,28,63.6804,28,57.4887,28L0,28L0,14.5113ZM14.5113,1L71,1L71,13.4887Q71,18.6574,70.8935,20.1702Q70.753,22.1653,70.237,23.1779Q69.2045,25.2045,67.1779,26.237Q66.1653,26.753,64.1702,26.8935Q62.6574,27,57.4887,27L1,27L1,14.5113Q1,9.34262,1.10652,7.82977Q1.247,5.83468,1.76295,4.82207Q2.79553,2.79553,4.82207,1.76295Q5.83468,1.247,7.82977,1.10652Q9.34262,1,14.5113,1Z" fill-rule="evenodd" fill="url(#master_svg2_85_00190)" fill-opacity="1"/></g></g></svg>
`); `);
const option = { const option: { [key: string]: any } = {
grid: { grid: {
top: "14%", ...props.grid,
left: "5%",
right: "3%",
bottom: "15%",
}, },
legend: { legend: {
type: "plain", type: "plain",
@ -269,7 +291,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
itemHeight: 16, itemHeight: 16,
top: 3, top: 3,
left: "center", left: "center",
itemGap: 40, itemGap: props.ledgeGap,
textStyle: { textStyle: {
color: "#C0EEFF", color: "#C0EEFF",
fontSize: 14, fontSize: 14,
@ -290,84 +312,12 @@ function svgToPercentEncodedDataUrl(svg: string) {
}, },
}, },
// 使graphic // 使graphic
graphic: [
//
{
type: "group",
right: 97,
top: 0,
// z: 100,
children: [
{
type: "image",
// z: 100,
style: {
image: `data:image/svg+xml;charset=utf-8,${currentMode.value === "week" ? activeButtonSvgBase64 : buttonSvgBase64}`,
width: 72,
height: 28,
},
cursor: "pointer",
onclick: () => switchDisplayMode("week"),
},
{
type: "text",
// z: 100,
style: {
text: "按周",
x: 36,
y: 14,
textAlign: "center",
textVerticalAlign: "middle",
fill: `${currentMode.value === "week" ? "#F8EA21" : "#DBF7FF"}`,
fontSize: 14,
},
cursor: "pointer",
onclick: () => switchDisplayMode("week"),
},
],
},
//
{
type: "group",
right: 15,
top: 0,
// z: 100,
children: [
{
type: "image",
// z: 100,
style: {
image: `data:image/svg+xml;charset=utf-8,${currentMode.value === "month" ? activeButtonSvgBase64 : buttonSvgBase64}`,
width: 72,
height: 28,
},
cursor: "pointer",
onclick: () => switchDisplayMode("month"),
},
{
type: "text",
// z: 100,
style: {
text: "按月",
x: 36,
y: 14,
textAlign: "center",
textVerticalAlign: "middle",
fill: `${currentMode.value === "month" ? "#F8EA21" : "#DBF7FF"}`,
fontSize: 14,
},
cursor: "pointer",
onclick: () => switchDisplayMode("month"),
},
],
},
],
tooltip: { tooltip: {
trigger: "axis", trigger: "axis",
renderMode: "html", renderMode: "html",
padding: 0, padding: 0,
borderWidth: 0, borderWidth: 0,
extraCssText: "background-color: transparent;", extraCssText: `background-color: transparent;box-shadow:unset;${props.tooltipFixed ? 'scale:0.8;':''}`,
formatter: (params: any) => { formatter: (params: any) => {
const date = params[0].name; const date = params[0].name;
const seriesData = params const seriesData = params
@ -483,6 +433,85 @@ function svgToPercentEncodedDataUrl(svg: string) {
], ],
}; };
if (props.needDate) {
option.graphic = [
//
{
type: "group",
right: 97,
top: 0,
// z: 100,
children: [
{
type: "image",
// z: 100,
style: {
image: `data:image/svg+xml;charset=utf-8,${currentMode.value === "week" ? activeButtonSvgBase64 : buttonSvgBase64}`,
width: 72,
height: 28,
},
cursor: "pointer",
onclick: () => switchDisplayMode("week"),
},
{
type: "text",
// z: 100,
style: {
text: "按周",
x: 36,
y: 14,
textAlign: "center",
textVerticalAlign: "middle",
fill: `${currentMode.value === "week" ? "#F8EA21" : "#DBF7FF"}`,
fontSize: 14,
},
cursor: "pointer",
onclick: () => switchDisplayMode("week"),
},
],
},
//
{
type: "group",
right: 15,
top: 0,
// z: 100,
children: [
{
type: "image",
// z: 100,
style: {
image: `data:image/svg+xml;charset=utf-8,${currentMode.value === "month" ? activeButtonSvgBase64 : buttonSvgBase64}`,
width: 72,
height: 28,
},
cursor: "pointer",
onclick: () => switchDisplayMode("month"),
},
{
type: "text",
// z: 100,
style: {
text: "按月",
x: 36,
y: 14,
textAlign: "center",
textVerticalAlign: "middle",
fill: `${currentMode.value === "month" ? "#F8EA21" : "#DBF7FF"}`,
fontSize: 14,
},
cursor: "pointer",
onclick: () => switchDisplayMode("month"),
},
],
},
];
}
if(props.tooltipFixed){
option.tooltip.position=["center",'0']
}
myChart.setOption(option, true); myChart.setOption(option, true);
// //
@ -597,12 +626,26 @@ function svgToPercentEncodedDataUrl(svg: string) {
}; };
onMounted(() => { onMounted(() => {
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
initChart();
}); });
// //
watch(() => props.chartDataArray, () => { watch(
() => props.chartDataArray,
() => {
initChart(); initChart();
}, { deep: true }); },
{ deep: true },
);
watch(
() => props.grid,
(newGrid) => {
console.log(newGrid);
myChart?.setOption({ grid: { ...newGrid } });
},
);
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
@ -614,7 +657,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
:deep(.tool-tip-wrapper) { :deep(.tool-tip-wrapper) {
background-color: transparent; background-color: transparent;
position: relative; position: relative;
width: 184px; width: var(--tip-width);
border-radius: 4px; border-radius: 4px;
box-shadow: 0px 4px 10px 0px #0d3472; box-shadow: 0px 4px 10px 0px #0d3472;
overflow: hidden; overflow: hidden;

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="w-[143px] h-[143px]"> <div class="w-[143px] h-[143px]">
<div class="w-[143px] h-[143px] bg-transparent" ref="chartRef"></div> <div class="w-full h-inherit bg-transparent" ref="chartRef"></div>
</div> </div>
</template> </template>
@ -19,6 +19,14 @@
required: true, required: true,
default: () => [], default: () => [],
}, },
width: {
type: Number,
default: 143,
},
height: {
type: Number,
default: 143,
},
}); });
const generatedFormattedData = () => { const generatedFormattedData = () => {
@ -49,6 +57,10 @@
// //
const option = { const option = {
grid:{
width:'auto',
height:'auto'
},
tooltip: { tooltip: {
trigger: "item", trigger: "item",
backgroundColor: "rgba(0, 0, 0, 0.7)", backgroundColor: "rgba(0, 0, 0, 0.7)",
@ -58,6 +70,7 @@
fontSize: 10, fontSize: 10,
}, },
formatter: "{b}: {d}%", formatter: "{b}: {d}%",
position: ['25%', '40%']
}, },
series: [ series: [
{ {
@ -98,9 +111,9 @@
top: "center", top: "center",
style: { style: {
image: "/images/rotate-circle.webp", image: "/images/rotate-circle.webp",
width: 143, width: props.width,
height: 143, height: props.height,
fill:"transparent" fill: "transparent",
}, },
z: 2, z: 2,
cursor: "point", cursor: "point",
@ -111,7 +124,6 @@
animation: true, animation: true,
animationDuration: 500, animationDuration: 500,
}; };
// //
chartInstance.setOption(option); chartInstance.setOption(option);
@ -148,6 +160,36 @@
{ deep: true }, { deep: true },
); );
watch(
() => props.width,
(newWidth) => {
if (chartInstance) {
chartInstance.setOption({
graphic: [
{
id: "circle-rotate",
elements: [
{
type: "image",
left: "center",
top: "center",
style: {
image: "/images/rotate-circle.webp",
width: newWidth,
height: props.height,
fill: "transparent",
},
z: 2,
cursor: "point",
},
],
},
],
});
}
},
);
onUnmounted(() => { onUnmounted(() => {
// //
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);

View File

@ -1,5 +1,5 @@
<template> <template>
<div ref="chartRef" class="chart-container"></div> <div ref="chartRef" class="w-full h-[400px]"></div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -143,8 +143,4 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
.chart-container {
width: 100%;
height: 400px;
}
</style> </style>

View File

@ -161,6 +161,7 @@
return { return {
tooltip: { tooltip: {
triggerOn:"mousemove|click",
formatter: (params: any) => { formatter: (params: any) => {
if (params.seriesName !== "mouseoutSeries" && params.seriesName !== "pie2d") { if (params.seriesName !== "mouseoutSeries" && params.seriesName !== "pie2d") {
const bfb = ((series[params.seriesIndex].pieData.endRatio - series[params.seriesIndex].pieData.startRatio) * 100).toFixed(2); const bfb = ((series[params.seriesIndex].pieData.endRatio - series[params.seriesIndex].pieData.startRatio) * 100).toFixed(2);

View File

@ -42,7 +42,7 @@ export const useFetchAllData = () => {
const chargingRankingData = ref<any>({}); const chargingRankingData = ref<any>({});
const getChargingRankingData = () => { const getChargingRankingData = () => {
getRequest(getBigScreenRanking(), {}, {}).then((resp) => { getRequest(getBigScreenRanking(), {take:9999}, {}).then((resp) => {
if (resp.code === 200) { if (resp.code === 200) {
chargingRankingData.value = resp.result; chargingRankingData.value = resp.result;
} }

View File

@ -44,88 +44,74 @@
<script lang="ts" setup> <script lang="ts" setup>
import SvgComponent from "@/components/SvgComponent.vue"; import SvgComponent from "@/components/SvgComponent.vue";
import SvgIcon from "@/components/svg-icon/SvgIcon.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 DigitalWatch from "@/components/watch/DigitalWatch.vue";
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from "vue";
import { useFetchAllData } from "./composables/useFetchData";
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 { useFetchAllData } from "./composables/useFetchData"
import { runImmediatelyAll, updateTime } from "@/composables/usePolling"; import { runImmediatelyAll, updateTime } from "@/composables/usePolling";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { getUserInfoUrl, getLogoutUrl } from "@/api/fetchUrl"; import { getUserInfoUrl, getLogoutUrl } from "@/api/fetchUrl";
import { getRequest, postRequest } from "@/api/customFetch"; import { getRequest, postRequest } from "@/api/customFetch";
import { useUserStore } from "@/store/user"; import { useUserStore } from "@/store/user";
import { asyncComponentConfig } from "@/composables/useLazyLoad";
import { usePlatformType } from "@/utils/device";
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"),
});
const router = useRouter(); const router = useRouter();
const headerSvg = ref(""); const headerSvg = ref("");
const headerBackgroundSvg = async () => { const headerBackgroundSvg = async () => {
const { default: svg } = await import("/src/assets/svg-img/header-background.svg?raw"); const { default: svg } = await import("/src/assets/svg-img/header-background.svg?raw");
@ -138,42 +124,45 @@
titleSvg.value = svg; titleSvg.value = svg;
}; };
useFetchAllData();
useFetchAllData()
const updateAllData = () => { const updateAllData = () => {
runImmediatelyAll() runImmediatelyAll();
} };
const username = ref("") const username = ref("");
const userStore = useUserStore() const userStore = useUserStore();
const getUserInfo = () => { const getUserInfo = () => {
const accessToken = userStore.getAccessToken const accessToken = userStore.getAccessToken;
getRequest(getUserInfoUrl(),{},{headers:{ Authorization: `Bearer ${accessToken}`}}).then(resp => { getRequest(getUserInfoUrl(), {}, { headers: { Authorization: `Bearer ${accessToken}` } }).then((resp) => {
if (resp.code === 200) { if (resp.code === 200) {
username.value = (resp.result as {account:string}).account username.value = (resp.result as { account: string }).account;
} else if (resp.code === 401) { } else if (resp.code === 401) {
router.push("/login") router.push("/login");
}
})
} }
});
};
const handleLogout = () => { const handleLogout = () => {
const accessToken = userStore.getAccessToken const accessToken = userStore.getAccessToken;
postRequest(getLogoutUrl(),{},{headers:{"content-type": "application/json; charset=utf-8",Authorization: `Bearer ${accessToken}`}}).then(resp => { postRequest(getLogoutUrl(), {}, { headers: { "content-type": "application/json; charset=utf-8", Authorization: `Bearer ${accessToken}` } }).then((resp) => {
if (resp.code === 200) { if (resp.code === 200) {
userStore.setAccessToken('') userStore.setAccessToken("");
userStore.setRefreshToken('') userStore.setRefreshToken("");
}
router.push("/login")
})
} }
router.push("/login");
});
};
onBeforeMount(() => { onBeforeMount(() => {
headerBackgroundSvg(); headerBackgroundSvg();
headerTitleSvg(); headerTitleSvg();
getUserInfo(); getUserInfo();
const { isMobile } = usePlatformType();
//
if (isMobile) {
router.push("/app");
}
}); });
</script> </script>

View File

@ -1,30 +1,30 @@
<template> <template>
<div class="login-bg"> <div class="login-bg">
<div class="flex items-center justify-end mt-[17px] mr-[24px]"> <div class="flex items-center md:justify-end justify-center mt-[17px] mr-[24px]">
<DigitalWatch class="" /> <DigitalWatch class="" />
</div> </div>
<SvgComponent :content="titleSvg" class="h-[156px] mt-[95px]" /> <SvgComponent :content="titleSvg" class="md:h-[156px] mt-[95px] w-full" />
<div class="login-form-wrapper w-[622px] h-[419px] mx-auto mt-[87px]"> <div class="login-form-wrapper md:w-[622px] md:h-[419px] aspect-[1.48/1] w-full mx-auto mt-[87px]">
<form class="w-full h-full flex flex-col items-center pt-[126px]" @submit="handleSubmit"> <form class="w-full h-full flex flex-col items-center md:pt-[126px] pt-[21.5rem]" @submit="handleSubmit">
<div class="form-item px-[71px] w-full"> <div class="form-item px-[71px] w-full">
<div class="input-bg w-full h-[48px] flex items-center px-[18px]"> <div class="input-bg w-full md:h-[48px] h-[6rem] flex items-center px-[18px]">
<SvgIcon name="avatar" class="text-[#00D5FF] mr-[13px]" /> <SvgIcon name="avatar" class="text-[#00D5FF] mr-[13px] w-[3.5rem]! h-[3.5rem]! md:w-[16px]! md:h-[16px]!" />
<input <input
type="text" type="text"
v-model="formData.username" v-model="formData.username"
placeholder="请输入账号" placeholder="请输入账号"
class="flex-1 h-[48px] text-white placeholder:text-[#666666] focus:outline-none focus:border-[#29F1FA] bg-transparent" /> class="flex-1 md:h-[48px] h-[6rem] text-white placeholder:text-[#666666] focus:outline-none focus:border-[#29F1FA] bg-transparent text-[3rem] md:text-[100%]" />
</div> </div>
<div class="input-bg w-full h-[48px] flex items-center px-[18px] mt-[20px]"> <div class="input-bg w-full md:h-[48px] h-[6rem] flex items-center px-[18px] md:mt-[20px] mt-[5rem]">
<SvgIcon :name="showPassword ? 'eye' : 'password'" class="text-[#00D5FF] mr-[13px] cursor-pointer" @click="togglePasswordVisibility" /> <SvgIcon :name="showPassword ? 'eye' : 'password'" class="text-[#00D5FF] mr-[13px] cursor-pointer w-[3.5rem]! h-[3.5rem]! md:w-[16px]! md:h-[16px]!" @click="togglePasswordVisibility" />
<input <input
:type="showPassword ? 'text' : 'password'" :type="showPassword ? 'text' : 'password'"
v-model="formData.password" v-model="formData.password"
placeholder="请输入密码" placeholder="请输入密码"
class="flex-1 h-[48px] text-white placeholder:text-[#666666] focus:outline-none focus:border-[#29F1FA] bg-transparent" /> class="flex-1 md:h-[48px] h-[6rem] text-white placeholder:text-[#666666] focus:outline-none focus:border-[#29F1FA] bg-transparent text-[3rem] md:text-[100%]" />
</div> </div>
<button type="submit" class="w-full h-[48px] bg-[#2A8EFE] text-[#fff] text-[18px] font-700 border-submit mt-[48px]">登录</button> <button type="submit" class="w-full md:h-[48px] h-[6rem] bg-[#2A8EFE] text-[#fff] md:text-[18px] text-[3rem] font-700 border-submit md:mt-[48px] mt-[6rem]">登录</button>
<p class="text-[#657295] mt-[10px] text-center text-[14px]">如忘记账号密码请联系管理员18845000222</p> <p class="text-[#657295] mt-[10px] text-center md:text-[14px] text-[2.5rem]">如忘记账号密码请联系管理员18845000222</p>
</div> </div>
</form> </form>
</div> </div>
@ -39,6 +39,7 @@
import { postRequest } from "@/api/customFetch"; import { postRequest } from "@/api/customFetch";
import { useUserStore } from "@/store/user"; import { useUserStore } from "@/store/user";
import DigitalWatch from "@/components/watch/DigitalWatch.vue"; import DigitalWatch from "@/components/watch/DigitalWatch.vue";
import { usePlatformType } from "@/utils/device";
const router = useRouter(); const router = useRouter();
@ -87,9 +88,14 @@
const { accessToken, refreshToken } = resp.result as { refreshToken: string; accessToken: string }; const { accessToken, refreshToken } = resp.result as { refreshToken: string; accessToken: string };
userStore.setAccessToken(accessToken); userStore.setAccessToken(accessToken);
userStore.setRefreshToken(refreshToken); userStore.setRefreshToken(refreshToken);
const {isMobile} = usePlatformType()
// //
if(isMobile){
router.push('/app')
}else{
router.push("/"); router.push("/");
} }
}
} else { } else {
// //
alert(resp.message || "登录失败"); alert(resp.message || "登录失败");