feat: 手机端样式编写,PC端查看更多
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"upload": "bash ./upload.sh",
|
||||
"build-and-upload": "pnpm run build && pnpm run upload",
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
"@unocss/preset-wind": "^0.65.2",
|
||||
"@vitejs/plugin-basic-ssl": "^1.2.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vueuse/core": "^13.4.0",
|
||||
"prettier": "3.4.2",
|
||||
"sass-embedded": "^1.86.0",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ importers:
|
|||
'@vitejs/plugin-vue':
|
||||
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))
|
||||
'@vueuse/core':
|
||||
specifier: ^13.4.0
|
||||
version: 13.4.0(vue@3.5.13(typescript@5.6.3))
|
||||
prettier:
|
||||
specifier: 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))
|
||||
unplugin-auto-import:
|
||||
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:
|
||||
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)
|
||||
|
|
@ -653,6 +656,9 @@ packages:
|
|||
'@types/qs@6.9.17':
|
||||
resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==}
|
||||
|
||||
'@types/web-bluetooth@0.0.21':
|
||||
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||
|
||||
'@unocss/astro@0.65.2':
|
||||
resolution: {integrity: sha512-lpGoleJToxaYeN5LTGrNbvbXATNWswgoQwlljIJ9kWOjx4NbGC71pXRvDQSb9yRFDTCr5S2hMtupna4ulrHisA==}
|
||||
peerDependencies:
|
||||
|
|
@ -801,6 +807,19 @@ packages:
|
|||
'@vue/shared@3.5.13':
|
||||
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:
|
||||
resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
|
@ -3058,6 +3077,8 @@ snapshots:
|
|||
|
||||
'@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))':
|
||||
dependencies:
|
||||
'@unocss/core': 0.65.2
|
||||
|
|
@ -3306,6 +3327,19 @@ snapshots:
|
|||
|
||||
'@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: {}
|
||||
|
||||
alien-signals@0.2.2: {}
|
||||
|
|
@ -5161,7 +5195,7 @@ snapshots:
|
|||
- supports-color
|
||||
- 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:
|
||||
'@antfu/utils': 0.7.10
|
||||
'@rollup/pluginutils': 5.1.3(rollup@4.28.1)
|
||||
|
|
@ -5172,6 +5206,7 @@ snapshots:
|
|||
unplugin: 2.1.0
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.14.1592(rollup@4.28.1)
|
||||
'@vueuse/core': 13.4.0(vue@3.5.13(typescript@5.6.3))
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createApp } from 'vue'
|
||||
import '@unocss/reset/tailwind-compat.css'
|
||||
// import './style.css'
|
||||
import './style.css'
|
||||
import 'uno.css';
|
||||
import App from './App.vue'
|
||||
// Pinia 持久化
|
||||
|
|
|
|||
|
|
@ -8,4 +8,13 @@ export const privateRoutes = [
|
|||
},
|
||||
component:()=>import('../views/home.vue')
|
||||
},
|
||||
{
|
||||
path:"/app",
|
||||
name:'app',
|
||||
meta:{
|
||||
title:`${new Date().getFullYear()}深泉教育招生数据可视化`,
|
||||
role:['admin']
|
||||
},
|
||||
component:()=>import('../views/app.vue')
|
||||
}
|
||||
];
|
||||
|
|
|
|||
155
src/style.css
|
|
@ -1,156 +1,5 @@
|
|||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
@media (max-width: 1919px){
|
||||
html{
|
||||
line-height: 1;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-size: calc(100vw / 93.75);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
left: 0;
|
||||
z-index: auto;
|
||||
color: #fff;
|
||||
-webkit-mask: linear-gradient(to bottom, transparent, #000);
|
||||
-webkit-mask: var(--mask-gradient, linear-gradient(to bottom, transparent, #000));
|
||||
white-space: nowrap;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -9,12 +9,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<SvgComponent :content="moreArrowSvg" class="w-[14px] h-[22px]" />
|
||||
</div>
|
||||
<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 }">
|
||||
<SvgComponent :content="goldMedalSvg" class="w-[34px] h-[20px]" v-if="index === 0" />
|
||||
<SvgComponent :content="silverMedalSvg" class="w-[34px] h-[20px]" v-if="index === 1" />
|
||||
|
|
@ -28,11 +28,59 @@
|
|||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SvgComponent from "@/components/SvgComponent.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 = [
|
||||
{ field: "rank", header: "名次", align: "justify-center", width: "68px" },
|
||||
|
|
@ -40,6 +88,8 @@
|
|||
{ field: "total", header: "收费人数", align: "justify-center", width: "96px" },
|
||||
];
|
||||
|
||||
const show = ref(false);
|
||||
|
||||
let products = ref<any[]>([]);
|
||||
const chargingRankingData = inject(
|
||||
"chargingRankingData",
|
||||
|
|
@ -140,6 +190,7 @@
|
|||
color: #44c1ef;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -149,7 +200,48 @@
|
|||
td {
|
||||
padding: 10px;
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -11,32 +11,35 @@
|
|||
<div class="w-full h-[calc(100%-36px)] mt-[36px] flex flex-col">
|
||||
<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">
|
||||
<div class="flex items-center flex-col">
|
||||
<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-[14px] text-color" data-text="人">人</span>
|
||||
</div>
|
||||
<span class="text-[#C7F0FF] text-[12px]">线上来源</span>
|
||||
</div>
|
||||
<div class="border-image w-full mt-[6px]"></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="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-[14px] text-color" data-text="人">人</span>
|
||||
</div>
|
||||
<span class="text-[#C7F0FF] text-[12px]">线下来源</span>
|
||||
</div>
|
||||
<div class="border-image w-full mt-[6px]"></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="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-[14px] text-color" data-text="人">人</span>
|
||||
</div>
|
||||
<span class="text-[#C7F0FF] text-[12px]">未标记</span>
|
||||
</div>
|
||||
<div class="border-image w-full mt-[6px]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -129,4 +132,10 @@
|
|||
border-image-source: url("src/assets/svg-img/border-image.png");
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.border-image {
|
||||
border: 1px solid;
|
||||
border-image: linear-gradient(90deg, #217ac600, #227cc8, #217ac600) 1 1;
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<SvgComponent :content="moreArrowSvg" class="w-[14px] h-[22px]" />
|
||||
</div>
|
||||
<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 }">
|
||||
<SvgComponent :content="goldMedalSvg" class="w-[34px] h-[20px]" v-if="index === 0" />
|
||||
<SvgComponent :content="silverMedalSvg" class="w-[34px] h-[20px]" v-if="index === 1" />
|
||||
|
|
@ -28,12 +28,62 @@
|
|||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SvgComponent from "@/components/SvgComponent.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 = [
|
||||
{ field: "rank", header: "名次", align: "justify-center", width: "68px" },
|
||||
{ field: "name", header: "姓名", align: "justify-left", width: "100px" },
|
||||
|
|
@ -141,6 +191,7 @@
|
|||
color: #44c1ef;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -150,7 +201,48 @@
|
|||
td {
|
||||
padding: 10px;
|
||||
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>
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { convertNumber } from "@/utils/convertNumber";
|
||||
import { usePlatformType } from "@/utils/device";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
interface DataItem {
|
||||
|
|
@ -10,9 +12,16 @@
|
|||
total: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
chartData: DataItem[];
|
||||
}>();
|
||||
const props = defineProps({
|
||||
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 }) => {
|
||||
// 柱子索引值
|
||||
|
|
@ -96,10 +105,11 @@
|
|||
if (!chartDom.value) return;
|
||||
|
||||
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({
|
||||
buildPath(ctx, shape) {
|
||||
const { topBasicsYAxis, bottomYAxis, basicsXAxis } = shape;
|
||||
|
|
@ -171,25 +181,29 @@
|
|||
// 初始化时设置空数据,后续通过updateChartData更新
|
||||
const options = {
|
||||
grid: {
|
||||
top: "12%",
|
||||
left: "5%",
|
||||
right: "3%",
|
||||
bottom: "15%",
|
||||
top: props.shorthand ? 30:40,
|
||||
left: props.shorthand ? 40 : 60,
|
||||
right: props.shorthand ?20 : 30,
|
||||
bottom: props.shorthand ?30:45,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
axisLabel: {
|
||||
formatter: (value: any) => {
|
||||
if(props.shorthand){
|
||||
return value.slice(0,2)
|
||||
}else{
|
||||
if (value.length <= 4) return value;
|
||||
const arr = [];
|
||||
for (let i = 0; i < value.length; i += 4) {
|
||||
arr.push(value.slice(i, i + 4));
|
||||
}
|
||||
return arr.join('\n');
|
||||
}
|
||||
},
|
||||
color: "#C0EEFF",
|
||||
fontSize: 14,
|
||||
fontSize: props.shorthand ? 10 : 14,
|
||||
interval: 0,
|
||||
},
|
||||
nameTextStyle: {
|
||||
|
|
@ -206,7 +220,7 @@
|
|||
nameLocation: "end",
|
||||
nameTextStyle: {
|
||||
color: "#C0EEFF",
|
||||
fontSize: 14,
|
||||
fontSize: props.shorthand ? 10 : 14,
|
||||
padding: [0, 5, 0, 0],
|
||||
align: "right",
|
||||
},
|
||||
|
|
@ -216,7 +230,14 @@
|
|||
interval: 20,
|
||||
axisLabel: {
|
||||
color: "#C0EEFF",
|
||||
fontSize: 14,
|
||||
fontSize: props.shorthand ? 10 : 14,
|
||||
formatter: (value: any) => {
|
||||
if(props.shorthand){
|
||||
return convertNumber(value)
|
||||
}else{
|
||||
return value
|
||||
}
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
|
|
@ -245,7 +266,7 @@
|
|||
label: {
|
||||
show: true,
|
||||
position: "top",
|
||||
fontSize: 12,
|
||||
fontSize: props.shorthand ? 10 : 12,
|
||||
color: "rgba(192, 238, 255, 1)",
|
||||
},
|
||||
tooltip: {
|
||||
|
|
@ -258,7 +279,6 @@
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
myChart.setOption(options);
|
||||
|
||||
// 如果有数据,立即更新
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="chartDom" class="w-full h-full"></div>
|
||||
<div ref="chartDom" class="w-full h-full" :style="`--tip-width:${tooltipWidth}`"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -12,8 +12,33 @@
|
|||
const props = defineProps({
|
||||
chartDataArray: {
|
||||
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"]);
|
||||
|
|
@ -58,7 +83,8 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
}
|
||||
// 处理图表数据
|
||||
const processChartData = (dataArray: ChartTypeData[], mode: string): ChartDataFormat => {
|
||||
if (!dataArray || dataArray.length === 0) return {
|
||||
if (!dataArray || dataArray.length === 0)
|
||||
return {
|
||||
dates: [],
|
||||
seriesData: [
|
||||
{ name: "获客", data: [], color: "#20E6A4" },
|
||||
|
|
@ -82,8 +108,8 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
// 将日期转换为数组并排序
|
||||
let sortedDates = Array.from(allDates).sort((a: string, b: string) => {
|
||||
// 假设日期格式为 MM-DD
|
||||
const [monthA, dayA] = a.split('-').map(Number);
|
||||
const [monthB, dayB] = b.split('-').map(Number);
|
||||
const [monthA, dayA] = a.split("-").map(Number);
|
||||
const [monthB, dayB] = b.split("-").map(Number);
|
||||
if (monthA !== monthB) return monthA - monthB;
|
||||
return dayA - dayB;
|
||||
});
|
||||
|
|
@ -106,7 +132,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
|
||||
// 格式化日期显示
|
||||
const formattedDates = sortedDates.map((date: string) => {
|
||||
const [month, day] = date.split('-');
|
||||
const [month, day] = date.split("-");
|
||||
return `${month}.${day}`;
|
||||
});
|
||||
|
||||
|
|
@ -122,7 +148,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
// 对每个系列填充当前日期的值
|
||||
dataArray.forEach((typeData: ChartTypeData, typeIndex: number) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
|
@ -155,7 +181,6 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
|
||||
const legendIcons = ref<{ [key: string]: string }>({});
|
||||
|
||||
|
||||
// 初始化图例图标
|
||||
const initLegendIcons = () => {
|
||||
chartData.value.seriesData.forEach((item) => {
|
||||
|
|
@ -236,7 +261,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
// 计算y轴最大值
|
||||
const calculateMaxValue = () => {
|
||||
let maxValue = 0;
|
||||
chartData.value.seriesData.forEach(series => {
|
||||
chartData.value.seriesData.forEach((series) => {
|
||||
const seriesMax = Math.max(...series.data);
|
||||
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>
|
||||
`);
|
||||
|
||||
const option = {
|
||||
const option: { [key: string]: any } = {
|
||||
grid: {
|
||||
top: "14%",
|
||||
left: "5%",
|
||||
right: "3%",
|
||||
bottom: "15%",
|
||||
...props.grid,
|
||||
},
|
||||
legend: {
|
||||
type: "plain",
|
||||
|
|
@ -269,7 +291,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
itemHeight: 16,
|
||||
top: 3,
|
||||
left: "center",
|
||||
itemGap: 40,
|
||||
itemGap: props.ledgeGap,
|
||||
textStyle: {
|
||||
color: "#C0EEFF",
|
||||
fontSize: 14,
|
||||
|
|
@ -290,84 +312,12 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
},
|
||||
},
|
||||
// 使用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: {
|
||||
trigger: "axis",
|
||||
renderMode: "html",
|
||||
padding: 0,
|
||||
borderWidth: 0,
|
||||
extraCssText: "background-color: transparent;",
|
||||
extraCssText: `background-color: transparent;box-shadow:unset;${props.tooltipFixed ? 'scale:0.8;':''}`,
|
||||
formatter: (params: any) => {
|
||||
const date = params[0].name;
|
||||
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);
|
||||
|
||||
// 跟踪鼠标指示器状态
|
||||
|
|
@ -597,12 +626,26 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
};
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", handleResize);
|
||||
initChart();
|
||||
});
|
||||
|
||||
// 监听图表数据变化,更新图表
|
||||
watch(() => props.chartDataArray, () => {
|
||||
watch(
|
||||
() => props.chartDataArray,
|
||||
() => {
|
||||
initChart();
|
||||
}, { deep: true });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.grid,
|
||||
(newGrid) => {
|
||||
console.log(newGrid);
|
||||
|
||||
myChart?.setOption({ grid: { ...newGrid } });
|
||||
},
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
|
|
@ -614,7 +657,7 @@ function svgToPercentEncodedDataUrl(svg: string) {
|
|||
:deep(.tool-tip-wrapper) {
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
width: 184px;
|
||||
width: var(--tip-width);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 4px 10px 0px #0d3472;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
|
@ -19,6 +19,14 @@
|
|||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 143,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 143,
|
||||
},
|
||||
});
|
||||
|
||||
const generatedFormattedData = () => {
|
||||
|
|
@ -49,6 +57,10 @@
|
|||
|
||||
// 配置项
|
||||
const option = {
|
||||
grid:{
|
||||
width:'auto',
|
||||
height:'auto'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||
|
|
@ -58,6 +70,7 @@
|
|||
fontSize: 10,
|
||||
},
|
||||
formatter: "{b}: {d}%",
|
||||
position: ['25%', '40%']
|
||||
},
|
||||
series: [
|
||||
{
|
||||
|
|
@ -98,9 +111,9 @@
|
|||
top: "center",
|
||||
style: {
|
||||
image: "/images/rotate-circle.webp",
|
||||
width: 143,
|
||||
height: 143,
|
||||
fill:"transparent"
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
fill: "transparent",
|
||||
},
|
||||
z: 2,
|
||||
cursor: "point",
|
||||
|
|
@ -111,7 +124,6 @@
|
|||
animation: true,
|
||||
animationDuration: 500,
|
||||
};
|
||||
|
||||
// 设置配置项
|
||||
chartInstance.setOption(option);
|
||||
|
||||
|
|
@ -148,6 +160,36 @@
|
|||
{ 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(() => {
|
||||
// 移除事件监听
|
||||
window.removeEventListener("resize", handleResize);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
<div ref="chartRef" class="w-full h-[400px]"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -143,8 +143,4 @@ onUnmounted(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -161,6 +161,7 @@
|
|||
|
||||
return {
|
||||
tooltip: {
|
||||
triggerOn:"mousemove|click",
|
||||
formatter: (params: any) => {
|
||||
if (params.seriesName !== "mouseoutSeries" && params.seriesName !== "pie2d") {
|
||||
const bfb = ((series[params.seriesIndex].pieData.endRatio - series[params.seriesIndex].pieData.startRatio) * 100).toFixed(2);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const useFetchAllData = () => {
|
|||
|
||||
const chargingRankingData = ref<any>({});
|
||||
const getChargingRankingData = () => {
|
||||
getRequest(getBigScreenRanking(), {}, {}).then((resp) => {
|
||||
getRequest(getBigScreenRanking(), {take:9999}, {}).then((resp) => {
|
||||
if (resp.code === 200) {
|
||||
chargingRankingData.value = resp.result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,88 +44,74 @@
|
|||
<script lang="ts" setup>
|
||||
import SvgComponent from "@/components/SvgComponent.vue";
|
||||
import SvgIcon from "@/components/svg-icon/SvgIcon.vue";
|
||||
import LoadingComponent from "@/components/LoadingComponent.vue";
|
||||
import ErrorComponent from "@/components/ErrorComponent.vue";
|
||||
|
||||
import DigitalWatch from "@/components/watch/DigitalWatch.vue";
|
||||
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
const asyncComponentConfig = {
|
||||
loadingComponent: LoadingComponent,
|
||||
errorComponent: ErrorComponent,
|
||||
onError: (_error: Error, retry: () => void, fail: () => void, attempts: number) => {
|
||||
if (attempts <= 3) {
|
||||
retry();
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const PaymentTotal = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/PaymentTotal.vue")
|
||||
});
|
||||
const TodayPayment = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/TodayPayment.vue")
|
||||
});
|
||||
const GainTotal = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/GainTotal.vue")
|
||||
});
|
||||
const GainToday = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/GainToday.vue")
|
||||
});
|
||||
const LossStatic = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/LossStatic.vue")
|
||||
});
|
||||
const OperatingTrends = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/OperatingTrends.vue")
|
||||
});
|
||||
const AskSection = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/AskSection.vue")
|
||||
});
|
||||
const StudentSource = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/StudentSource.vue")
|
||||
});
|
||||
const OnLineStatus = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/OnlineStatus.vue")
|
||||
});
|
||||
const OfflineStatus = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/OfflineStatus.vue")
|
||||
});
|
||||
const SixStatistics = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("@/views/components/SixStatistics.vue")
|
||||
});
|
||||
const ChargingRanking = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("./components/ChargingRanking.vue")
|
||||
});
|
||||
const WinCustomer = defineAsyncComponent({
|
||||
...asyncComponentConfig,
|
||||
loader: () => import("./components/WinCustomer.vue")
|
||||
});
|
||||
|
||||
|
||||
import { useFetchAllData } from "./composables/useFetchData"
|
||||
import { defineAsyncComponent } from "vue";
|
||||
import { useFetchAllData } from "./composables/useFetchData";
|
||||
import { runImmediatelyAll, updateTime } from "@/composables/usePolling";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { getUserInfoUrl, getLogoutUrl } from "@/api/fetchUrl";
|
||||
import { getRequest, postRequest } from "@/api/customFetch";
|
||||
import { useUserStore } from "@/store/user";
|
||||
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 headerSvg = ref("");
|
||||
const headerBackgroundSvg = async () => {
|
||||
const { default: svg } = await import("/src/assets/svg-img/header-background.svg?raw");
|
||||
|
|
@ -138,42 +124,45 @@
|
|||
titleSvg.value = svg;
|
||||
};
|
||||
|
||||
|
||||
useFetchAllData()
|
||||
useFetchAllData();
|
||||
const updateAllData = () => {
|
||||
runImmediatelyAll()
|
||||
}
|
||||
runImmediatelyAll();
|
||||
};
|
||||
|
||||
const username = ref("")
|
||||
const username = ref("");
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userStore = useUserStore();
|
||||
const getUserInfo = () => {
|
||||
const accessToken = userStore.getAccessToken
|
||||
getRequest(getUserInfoUrl(),{},{headers:{ Authorization: `Bearer ${accessToken}`}}).then(resp => {
|
||||
const accessToken = userStore.getAccessToken;
|
||||
getRequest(getUserInfoUrl(), {}, { headers: { Authorization: `Bearer ${accessToken}` } }).then((resp) => {
|
||||
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) {
|
||||
router.push("/login")
|
||||
}
|
||||
})
|
||||
router.push("/login");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
const accessToken = userStore.getAccessToken
|
||||
postRequest(getLogoutUrl(),{},{headers:{"content-type": "application/json; charset=utf-8",Authorization: `Bearer ${accessToken}`}}).then(resp => {
|
||||
const accessToken = userStore.getAccessToken;
|
||||
postRequest(getLogoutUrl(), {}, { headers: { "content-type": "application/json; charset=utf-8", Authorization: `Bearer ${accessToken}` } }).then((resp) => {
|
||||
if (resp.code === 200) {
|
||||
userStore.setAccessToken('')
|
||||
userStore.setRefreshToken('')
|
||||
|
||||
}
|
||||
router.push("/login")
|
||||
})
|
||||
userStore.setAccessToken("");
|
||||
userStore.setRefreshToken("");
|
||||
}
|
||||
router.push("/login");
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
headerBackgroundSvg();
|
||||
headerTitleSvg();
|
||||
getUserInfo();
|
||||
const { isMobile } = usePlatformType();
|
||||
// 跳转到首页
|
||||
if (isMobile) {
|
||||
router.push("/app");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
<template>
|
||||
<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="" />
|
||||
</div>
|
||||
<SvgComponent :content="titleSvg" class="h-[156px] mt-[95px]" />
|
||||
<div class="login-form-wrapper w-[622px] h-[419px] mx-auto mt-[87px]">
|
||||
<form class="w-full h-full flex flex-col items-center pt-[126px]" @submit="handleSubmit">
|
||||
<SvgComponent :content="titleSvg" class="md:h-[156px] mt-[95px] w-full" />
|
||||
<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 md:pt-[126px] pt-[21.5rem]" @submit="handleSubmit">
|
||||
<div class="form-item px-[71px] w-full">
|
||||
<div class="input-bg w-full h-[48px] flex items-center px-[18px]">
|
||||
<SvgIcon name="avatar" class="text-[#00D5FF] mr-[13px]" />
|
||||
<div class="input-bg w-full md:h-[48px] h-[6rem] flex items-center px-[18px]">
|
||||
<SvgIcon name="avatar" class="text-[#00D5FF] mr-[13px] w-[3.5rem]! h-[3.5rem]! md:w-[16px]! md:h-[16px]!" />
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.username"
|
||||
placeholder="请输入账号"
|
||||
class="flex-1 h-[48px] text-white placeholder:text-[#666666] focus:outline-none focus:border-[#29F1FA] bg-transparent" />
|
||||
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 class="input-bg w-full h-[48px] flex items-center px-[18px] mt-[20px]">
|
||||
<SvgIcon :name="showPassword ? 'eye' : 'password'" class="text-[#00D5FF] mr-[13px] cursor-pointer" @click="togglePasswordVisibility" />
|
||||
<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 w-[3.5rem]! h-[3.5rem]! md:w-[16px]! md:h-[16px]!" @click="togglePasswordVisibility" />
|
||||
<input
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
v-model="formData.password"
|
||||
placeholder="请输入密码"
|
||||
class="flex-1 h-[48px] text-white placeholder:text-[#666666] focus:outline-none focus:border-[#29F1FA] bg-transparent" />
|
||||
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>
|
||||
<button type="submit" class="w-full h-[48px] bg-[#2A8EFE] text-[#fff] text-[18px] font-700 border-submit mt-[48px]">登录</button>
|
||||
<p class="text-[#657295] mt-[10px] text-center text-[14px]">如忘记账号密码,请联系管理员:18845000222</p>
|
||||
<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 md:text-[14px] text-[2.5rem]">如忘记账号密码,请联系管理员:18845000222</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
import { postRequest } from "@/api/customFetch";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import DigitalWatch from "@/components/watch/DigitalWatch.vue";
|
||||
import { usePlatformType } from "@/utils/device";
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
|
|
@ -87,9 +88,14 @@
|
|||
const { accessToken, refreshToken } = resp.result as { refreshToken: string; accessToken: string };
|
||||
userStore.setAccessToken(accessToken);
|
||||
userStore.setRefreshToken(refreshToken);
|
||||
const {isMobile} = usePlatformType()
|
||||
// 跳转到首页
|
||||
if(isMobile){
|
||||
router.push('/app')
|
||||
}else{
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 登录失败提示
|
||||
alert(resp.message || "登录失败");
|
||||
|
|
|
|||