init
No files matched your search
@@ -0,0 +1,11 @@
|
||||
# 开发环境配置
|
||||
ENV = 'development'
|
||||
|
||||
# 开发环境
|
||||
VITE_BASE_API = '/api'
|
||||
|
||||
# 本地
|
||||
VITE_PROXY_PATH = 'http://127.0.0.1:9999'
|
||||
|
||||
# 测试
|
||||
# VITE_PROXY_PATH = 'http://36585.asset-cloud.com.cn:36588'
|
||||
@@ -0,0 +1,6 @@
|
||||
# 生产环境配置
|
||||
ENV = 'production'
|
||||
|
||||
# 生产环境
|
||||
VITE_BASE_API = '/api'
|
||||
VITE_PROXY_PATH = 'http://127.0.0.1:9999'
|
||||
@@ -0,0 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment variables
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Testing
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>浦江县智慧后勤数据统计</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "biz-pc-cockpit",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"build:prod": "vite build --mode production",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.12.2",
|
||||
"dayjs": "^1.11.18",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.11.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"less": "^4.4.1",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.main-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
import { service } from "./share/request"
|
||||
|
||||
/**
|
||||
* 用餐数据统计(根据时间段和日期分类统计)
|
||||
*/
|
||||
export const groupByTimeAndHourType = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-canteen/group/by-time-and-hour-type',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 各个食堂的用餐人数
|
||||
*/
|
||||
export const mchPayPersonCount = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-canteen/mch-pay-person-count',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 各个食堂的用餐人数(趋势)
|
||||
*/
|
||||
export const payPersonCountTrend = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-canteen/pay-person-count-trend',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用餐数据统计(根据自定义时间段和日期分类统计)
|
||||
*/
|
||||
export const groupByTimeAndCustomHour = (data) => {
|
||||
return service({
|
||||
url: "hard/panel-canteen/group/by-time-and-custom-hour",
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 排行榜
|
||||
*/
|
||||
export const mchPayCountTop = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-canteen/mch-pay-count-top',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 食堂报表
|
||||
*/
|
||||
export const canteenReport = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-canteen/report',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 最新的吃饭数据
|
||||
*/
|
||||
export const newPage = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-canteen/new-page',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 最近吃饭数据
|
||||
*/
|
||||
export const recentlyCanteen = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-canteen/recently',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
export * as canteenApi from "./canteen"
|
||||
|
||||
export * as shopApi from "./shop"
|
||||
|
||||
export * as nutritionApi from "./nutrition"
|
||||
|
||||
export * as userApi from "./user"
|
||||
@@ -0,0 +1,68 @@
|
||||
import { service } from "./share/request"
|
||||
|
||||
/**
|
||||
* 食堂用餐分类统计
|
||||
*/
|
||||
export const classCount = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-nutrition/class-count',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 食堂用餐分类统计
|
||||
*/
|
||||
export const cuisineTop = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-nutrition/cuisine-top',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 营养分析(按照购买时间统计)
|
||||
*/
|
||||
export const nutritionByTime = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-nutrition/nutrition/by-time',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 营养分析(总数统计)
|
||||
*/
|
||||
export const nutritionTotal = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-nutrition/nutrition/total',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 营养分析(趋势)
|
||||
*/
|
||||
export const nutritionTrend = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-nutrition/nutrition/trend',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复购排行榜(top5)
|
||||
*/
|
||||
export const repurchase = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-nutrition/repurchase',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import axios from "axios";
|
||||
import Cookies from 'js-cookie';
|
||||
import router from '@/router';
|
||||
|
||||
export const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
// 请求超时时间
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
withCredentials: false,
|
||||
});
|
||||
|
||||
// 请求拦截
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// get请求映射params参数
|
||||
if (config.method === "get" && config.params) {
|
||||
let url = config.url + "?" + tansParams(config.params);
|
||||
url = url.slice(0, -1);
|
||||
config.params = {};
|
||||
config.url = url;
|
||||
}
|
||||
// 加token
|
||||
const token = Cookies.get("panel_token") ?? ""
|
||||
if (token) {
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截
|
||||
service.interceptors.response.use(
|
||||
(res) => {
|
||||
|
||||
return res.data;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
// 登录失效
|
||||
if (error.status == 424) {
|
||||
// 清除token
|
||||
// Cookies.remove("panel_token")
|
||||
// Cookies.remove("panel_refresh_token")
|
||||
// router.replace("/no-permission")
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
function tansParams(params) {
|
||||
let result = "";
|
||||
for (const propName of Object.keys(params)) {
|
||||
const value = params[propName];
|
||||
var part = encodeURIComponent(propName) + "=";
|
||||
if (value !== null && value !== "" && typeof value !== "undefined") {
|
||||
if (typeof value === "object") {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (value[key] !== null && value[key] !== "" && typeof value[key] !== "undefined") {
|
||||
let params = propName + "[" + key + "]";
|
||||
var subPart = encodeURIComponent(params) + "=";
|
||||
result += subPart + encodeURIComponent(value[key]) + "&";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result += part + encodeURIComponent(value) + "&";
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { service } from "./share/request"
|
||||
|
||||
/**
|
||||
* 各个超市的购买数量统计
|
||||
*/
|
||||
export const mchPayPersonCount = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-shop/mch-pay-count',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 各个自营超市的统计数据
|
||||
*/
|
||||
export const mchPayCountTrend = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-shop/mch-pay-count-trend',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 超市报表
|
||||
*/
|
||||
export const canteenReport = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-shop/report',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 最新的超市购买数据
|
||||
*/
|
||||
export const newPage = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-shop/new-page',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 最近的超市购买数据
|
||||
*/
|
||||
export const recentlyShop = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-shop/recently',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 排行榜
|
||||
*/
|
||||
export const mchPayCountTop = (data) => {
|
||||
return service({
|
||||
url: 'hard/panel-shop/mch-pay-count-top',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { service } from "./share/request"
|
||||
|
||||
/**
|
||||
* 获取个人信息
|
||||
*/
|
||||
export const getUserInfo = (params) => {
|
||||
return service({
|
||||
url: '/admin/user/info',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
*/
|
||||
export const refreshToken = (refresh_token) => {
|
||||
const basicAuth = 'Basic ' + window.btoa('pig:pig');
|
||||
const grant_type = 'refresh_token';
|
||||
const scope = 'server';
|
||||
return service({
|
||||
url: '/auth/oauth2/token',
|
||||
method: 'post',
|
||||
headers: {
|
||||
skipToken: true,
|
||||
Authorization: basicAuth,
|
||||
'Content-Type': "application/x-www-form-urlencoded",
|
||||
},
|
||||
params: { refresh_token, grant_type, scope },
|
||||
})
|
||||
}
|
||||
|
||||
export const checkToken = (token) => {
|
||||
const basicAuth = 'Basic ' + window.btoa('pig:pig');
|
||||
return service({
|
||||
url: '/auth/token/check_token',
|
||||
method: 'get',
|
||||
headers: {
|
||||
skipToken: true,
|
||||
Authorization: basicAuth,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
params: { token },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-header class="header">
|
||||
<div class="left">
|
||||
<div class="title">智 慧 后 勤</div>
|
||||
<div class="sub-title">Smart logistics platform</div>
|
||||
</div>
|
||||
<div class="nav">
|
||||
<router-link class="nav-item" v-for="(tab, index) in navs" :key="index" :to="tab.path">
|
||||
<span class="nav-name">{{ tab.name }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="date">{{ date }}</span>
|
||||
<span class="time">{{ time }}</span>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import { getDate, getTime } from "@/utils/date";
|
||||
import { getUserInfo } from "@/utils/auth"
|
||||
|
||||
const { replace } = useRouter()
|
||||
const { query } = useRoute()
|
||||
|
||||
const navs = reactive([
|
||||
{ name: "智慧食堂", path: "/canteen" },
|
||||
{ name: "智慧超市", path: "/shop" },
|
||||
// { name: "智慧服务", path: "/service" },
|
||||
// { name: "智慧通行", path: "/through" },
|
||||
{ name: "营养分析", path: "/nutrition" },
|
||||
]);
|
||||
const date = ref(getDate());
|
||||
const time = ref(getTime());
|
||||
|
||||
onMounted(async () => {
|
||||
await getNavs()
|
||||
setInterval(() => {
|
||||
time.value = getTime();
|
||||
date.value = getDate();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// 获取菜单
|
||||
async function getNavs() {
|
||||
try {
|
||||
if (query.token && query.refresh_token) {
|
||||
Cookies.set("panel_token", query.token)
|
||||
Cookies.set("panel_refresh_token", query.refresh_token)
|
||||
}
|
||||
await getUserInfo()
|
||||
|
||||
} catch (error) {
|
||||
console.log("|获取菜单|错误:", error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.main-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
.background("@/assets/images/bg-image.png");
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 10vh;
|
||||
.background();
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)), url("@/assets/images/header-bg.png");
|
||||
background-position: 0 0, bottom;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.left {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-style: italic;
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
font-size: 1.6vw;
|
||||
font-weight: bold;
|
||||
color: @text-color;
|
||||
text-align: left;
|
||||
text-shadow: 0 0 0.5vw #ffffff;
|
||||
letter-spacing: 0.3vw;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 1vw;
|
||||
letter-spacing: 0.1vw;
|
||||
text-shadow: 0 0 0.5vw #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
height: 5vh;
|
||||
font-size: 1vw;
|
||||
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
.nav-name {
|
||||
color: @text-color;
|
||||
white-space: nowrap;
|
||||
padding: 1vh 4vh;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
.nav-name {
|
||||
color: @text-color;
|
||||
.background(@url: "@/assets/images/nav-active-button-bg.png", @size: 100% 90%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 20vw;
|
||||
height: 5vh;
|
||||
font-size: 1vw;
|
||||
|
||||
.weather-icon {
|
||||
width: 1.5vw;
|
||||
}
|
||||
|
||||
.weather {
|
||||
margin-left: 0.5vw;
|
||||
font-size: 0.8vw;
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
.date {
|
||||
margin-left: 1vw;
|
||||
font-size: 0.8vw;
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
.time {
|
||||
margin-left: 0.6vw;
|
||||
color: @text-color;
|
||||
font-weight: bold;
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
position: absolute;
|
||||
top: 10vh;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100vh - 10vh);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 642 B |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 920 B |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="alarm-list">
|
||||
<div class="segmented">
|
||||
<el-segmented v-model="activeSegment" :options="props.segments" :props="{ label: 'label', value: 'value' }"
|
||||
size="small" @change="dateTypeChange" />
|
||||
</div>
|
||||
<div class="list scrollbar-hide">
|
||||
<div v-for="(row, idx) in curList" :key="idx" class="item" @click="doCar(row)">
|
||||
<div class="type" :class="props.type == 'canteen' ? 'type-1' : 'type-2'">
|
||||
<span v-if="row.tag" class="type-text">{{ row.tag }}</span>
|
||||
<span v-else class="type-text">未知</span>
|
||||
</div>
|
||||
<div class="end">
|
||||
<span>{{ row.name || "未知" }}</span>
|
||||
<span>{{ row.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const emit = defineEmits(["changeDateType"]);
|
||||
|
||||
const props = defineProps({
|
||||
// "canteen"
|
||||
type: {
|
||||
type: String,
|
||||
default: () => "canteen",
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => [{ alarmList: [], name: null, time: null }],
|
||||
},
|
||||
segments: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{
|
||||
label: "昨日",
|
||||
value: "YESTERDAY",
|
||||
},
|
||||
{
|
||||
label: "今日",
|
||||
value: "TODAY",
|
||||
},
|
||||
{
|
||||
label: "本月",
|
||||
value: "THIS_MONTH",
|
||||
},
|
||||
{
|
||||
label: "近半年",
|
||||
value: "HALF_YEAR",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const activeSegment = ref(props.segments[0].value || "YESTERDAY");
|
||||
|
||||
if (Array.isArray(props.segments) && props.segments.length > 0) {
|
||||
activeSegment.value = props.segments[0].value;
|
||||
}
|
||||
|
||||
const curList = computed(() => {
|
||||
if (!props.data || !props.data.length) return [];
|
||||
const list = props.data;
|
||||
return Array.isArray(list) && list.length ? list : [];
|
||||
});
|
||||
|
||||
const doCar = (row) => {
|
||||
if (props.type == "person") {
|
||||
const _params = {
|
||||
// 抓拍时间
|
||||
swingTimeList: DateTool.getFormattedDateTimeRange(activeSegment.value),
|
||||
// 用户名
|
||||
personName: row.name ?? "",
|
||||
};
|
||||
console.log(`|点击跳转|-|异常事件实时统计|-|${row.name}|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("door_alarm", _params);
|
||||
}
|
||||
if (props.type == "car") {
|
||||
const _params = {
|
||||
// 抓拍时间
|
||||
capTimeList: DateTool.getFormattedDateTimeRange(activeSegment.value),
|
||||
// 车牌号
|
||||
carNum: row.name ?? "",
|
||||
};
|
||||
console.log(`|点击跳转|-|车辆异常出入实时统计|-|${row.name}|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("car_alarm", _params);
|
||||
}
|
||||
};
|
||||
|
||||
const dateTypeChange = (e) => {
|
||||
emit("changeDateType", e);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.alarm-list {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 4vh;
|
||||
|
||||
.segmented {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
|
||||
.el-segmented {
|
||||
--el-segmented-item-selected-color: var(--el-text-color-primary);
|
||||
--el-segmented-item-selected-bg-color: #095d82;
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2vh;
|
||||
padding-bottom: 4vh;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
padding: 0.8vh 0.5vw;
|
||||
cursor: pointer;
|
||||
background-image: url("@/assets/images/car-alarm-list-item-bg.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
|
||||
.type {
|
||||
width: 7vw;
|
||||
height: 3.6vh;
|
||||
line-height: 3.6vh;
|
||||
text-align: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
|
||||
.type-text {
|
||||
display: inline-block;
|
||||
font-size: 0.65vw;
|
||||
color: #c9e9ff;
|
||||
|
||||
width: 80%;
|
||||
margin: 0 0.5vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.type-1 {
|
||||
background-image: url("@/assets/images/car-alarm-type-blue.png");
|
||||
}
|
||||
|
||||
.type-2 {
|
||||
background-image: url("@/assets/images/car-alarm-type-green.png");
|
||||
}
|
||||
|
||||
.end {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 5vw;
|
||||
font-size: 0.6vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span:nth-child(1) {
|
||||
text-align: center;
|
||||
color: #d6e3ee;
|
||||
|
||||
flex: 1;
|
||||
margin-right: 1vw;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
color: #d6e3ee;
|
||||
width: 6.5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏滚动条 */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="segmented">
|
||||
<el-segmented v-model="activeSegment" :options="props.segments" :props="{ label: 'label', value: 'value' }"
|
||||
size="small" @change="dateTypeChange" />
|
||||
</div>
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
<div class="area-info">
|
||||
<div class="area-info-item" v-for="(item, index) in props.data" :key="item.name" @click="doItem(item)">
|
||||
<span
|
||||
:style="{ width: '1vw', height: '1vw', background: colors[index], borderRadius: '50%', display: 'inline-block' }"></span>
|
||||
<span class="name">{{ item.title }}</span>
|
||||
<span :style="{ marginLeft: '0.2vw', color: colors[index] }">{{ item.total }}次</span>
|
||||
<span :style="{ marginLeft: '0.1vw', color: colors[index], fontSize: '0.6vw' }"> ({{ percent(item.total) }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const emit = defineEmits(["changeDateType"]);
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: "告警总数" },
|
||||
type: {
|
||||
type: String,
|
||||
default: () => "car",
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{
|
||||
title: "未知",
|
||||
total: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
segments: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{
|
||||
label: "昨日",
|
||||
value: "YESTERDAY",
|
||||
},
|
||||
{
|
||||
label: "今日",
|
||||
value: "TODAY",
|
||||
},
|
||||
{
|
||||
label: "本月",
|
||||
value: "THIS_MONTH",
|
||||
},
|
||||
{
|
||||
label: "近半年",
|
||||
value: "HALF_YEAR",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const colors = ["#31D3A4", "#1AD0F9", "#5470C6"];
|
||||
const activeSegment = ref(props.segments[0].value || "TODAY");
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
if (Array.isArray(props.segments) && props.segments.length > 0) {
|
||||
activeSegment.value = props.segments[0].value;
|
||||
}
|
||||
|
||||
const total = computed(() => {
|
||||
if (!props.data || !props.data.length) return 0;
|
||||
return props.data.reduce((sum, i) => sum + (Number(i.total) || 0), 0);
|
||||
});
|
||||
const seriesData = computed(() => {
|
||||
if (!props.data || !props.data.length) return [];
|
||||
return props.data.map((i, idx) => ({ name: i.title, value: i.total, itemStyle: { color: colors[idx] } }));
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.data],
|
||||
() => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({
|
||||
series: [{ data: seriesData.value }],
|
||||
graphic: [{ type: "text", left: "40%", top: "40%", style: { text: String(total.value) } }],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const percent = (v) => {
|
||||
if (!total.value) return "0%";
|
||||
return ((Number(v) / total.value) * 100).toFixed(1) + "%";
|
||||
};
|
||||
|
||||
const dateTypeChange = (e) => {
|
||||
emit("changeDateType", e);
|
||||
};
|
||||
|
||||
const doItem = (item) => {
|
||||
// if (props.type == "person") {
|
||||
// const _params = {
|
||||
// // 报警类型
|
||||
// alarmType: item.type,
|
||||
// // 抓拍时间
|
||||
// swingTimeList: DateTool.getFormattedDateTimeRange(activeSegment.value),
|
||||
// }
|
||||
// console.log(`|点击跳转|-|异常事件类型统计|-|${item.title}|params:|{${JSON.stringify(_params)}}|`);
|
||||
// jumpAdminPath("door_alarm", _params)
|
||||
// }
|
||||
// if (props.type == "car") {
|
||||
// const _params = {
|
||||
// // 报警类型
|
||||
// alarmType: item.type,
|
||||
// // 抓拍时间
|
||||
// capTimeList: DateTool.getFormattedDateTimeRange(activeSegment.value)
|
||||
// }
|
||||
// console.log(`|点击跳转|-|车辆异常出入类型统计|-|${item.title}|params:|{${JSON.stringify(_params)}}|`);
|
||||
// jumpAdminPath("car_alarm", _params)
|
||||
// }
|
||||
};
|
||||
|
||||
const options = {
|
||||
backgroundColor: "transparent",
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
// formatter: (p) => `${p.name}:${p.value} (${p.percent}%)`,
|
||||
confine: true,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "pie",
|
||||
radius: ["60%", "78%"],
|
||||
center: ["50%", "50%"],
|
||||
avoidLabelOverlap: true,
|
||||
label: { show: false },
|
||||
labelLine: { show: false },
|
||||
itemStyle: { borderWidth: 0 },
|
||||
data: seriesData.value,
|
||||
},
|
||||
],
|
||||
graphic: [
|
||||
{
|
||||
type: "text",
|
||||
left: "40%",
|
||||
top: "40%",
|
||||
style: {
|
||||
text: String(total.value),
|
||||
fill: "#fff",
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
textAlign: "center",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
left: "39%",
|
||||
top: "55%",
|
||||
style: {
|
||||
text: props.title,
|
||||
fill: "#B8C7D9",
|
||||
fontSize: 12,
|
||||
textAlign: "center",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(options);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
.segmented {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
|
||||
.el-segmented {
|
||||
--el-segmented-item-selected-color: var(--el-text-color-primary);
|
||||
--el-segmented-item-selected-bg-color: #095d82;
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.area-info {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.area-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.4vw 0;
|
||||
font-size: 0.7vw;
|
||||
border-bottom: 1px dashed #eeeeee99;
|
||||
cursor: pointer;
|
||||
|
||||
.name {
|
||||
width: 5vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
margin-left: 0.2vw;
|
||||
color: #b8c7d9;
|
||||
font-size: 0.7vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="aside-block" :style="blockStyle">
|
||||
<div class="aside-block-title" :style="titleStyle" v-bind="$attrs">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="aside-block-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
// position: {
|
||||
// type: String,
|
||||
// default: "left",
|
||||
// },
|
||||
// bgImageShow: {
|
||||
// type: Boolean,
|
||||
// default: true,
|
||||
// },
|
||||
height: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
titleWidth: {
|
||||
type: String,
|
||||
default: "100%",
|
||||
},
|
||||
titleBgWidth: {
|
||||
type: String,
|
||||
default: "100%",
|
||||
},
|
||||
// mainStyle: {
|
||||
// type: String,
|
||||
// default: "",
|
||||
// },
|
||||
});
|
||||
|
||||
// const titleClass = computed(() => (props.position === "left" ? "aside-block-title-left" : "aside-block-title-right"));
|
||||
// const contentClass = computed(() => {
|
||||
// if (props.position === "left" && props.bgImageShow) return "bg-image-left";
|
||||
// if (props.position === "right" && props.bgImageShow) return "bg-image-right";
|
||||
// });
|
||||
const blockStyle = computed(() =>
|
||||
props.height ? { height: props.height } : props.height === "auto" ? { height: "auto" } : { height: "calc(100% / 3)" }
|
||||
);
|
||||
|
||||
const titleStyle = computed(() => ({
|
||||
width: props.titleWidth,
|
||||
backgroundSize: `${props.titleBgWidth} 100%`,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.aside-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aside-block-title {
|
||||
height: 4vh;
|
||||
line-height: 4vh;
|
||||
font-size: 0.78vw;
|
||||
font-weight: bold;
|
||||
text-indent: 2.8vw;
|
||||
/* padding: 0 2.8vw; */
|
||||
color: @text-color;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.background("@/assets/images/box-title.png");
|
||||
letter-spacing: 0.1vw;
|
||||
}
|
||||
|
||||
.aside-block-content {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
padding: 0.5vw;
|
||||
}
|
||||
|
||||
/*
|
||||
.bg-image-left {
|
||||
background: linear-gradient(to right, #1a3642, transparent);
|
||||
background: url("@/assets/images/aside_block_bg.png") center center/contain no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.bg-image-right {
|
||||
background: linear-gradient(to left, #1a3642, transparent);
|
||||
background: url("@/assets/images/aside_block_bg.png") center center/contain no-repeat;
|
||||
background-size: 100% 100%;
|
||||
} */
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="tab">
|
||||
<span class="tab-item" v-for="(item, index) in tabList" :key="index" :class="{ active: activeIndex === index }"
|
||||
@click="dateTypeChange(index)">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<keep-alive>
|
||||
<component :is="tabList[activeIndex].component" :data="componentData" :labels="componentLabels" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import YesterdayStatistics from "./yesterday-statistics.vue";
|
||||
import WeekStatistics from "./week-statistics.vue";
|
||||
import MonthStatistics from "./month-statistics.vue";
|
||||
import YearStatistics from "./year-statistics.vue";
|
||||
import { getMonthDays } from "@/utils/date";
|
||||
|
||||
const emit = defineEmits(["changeDateType"]);
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Object,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
YESTERDAY: { morning: 0, noon: 0, evening: 0 },
|
||||
TODAY: { morning: 0, noon: 0, evening: 0 },
|
||||
THIS_WEEK: [
|
||||
{ text: "在岗", name: "morning", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "迟到", name: "noon", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "早退", name: "evening", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
],
|
||||
THIS_MONTH: getMonthDays().map((day) => ({ day, type: -1 })),
|
||||
THIS_YEAR: [
|
||||
{ text: "在岗", name: "morning", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "迟到", name: "noon", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "早退", name: "evening", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const tabList = [
|
||||
{ label: "昨日", value: "YESTERDAY", component: YesterdayStatistics },
|
||||
{ label: "今日", value: "TODAY", component: YesterdayStatistics },
|
||||
{ label: "本周", value: "THIS_WEEK", component: WeekStatistics },
|
||||
{ label: "本月", value: "THIS_MONTH", component: MonthStatistics },
|
||||
{ label: "本年", value: "THIS_YEAR", component: YearStatistics },
|
||||
];
|
||||
const activeIndex = ref(0);
|
||||
const activeKey = computed(() => tabList[activeIndex.value].value);
|
||||
const componentData = computed(() => {
|
||||
if (!props.data) {
|
||||
return activeIndex.value == 0 ? {} : [];
|
||||
}
|
||||
return props.data[activeKey.value];
|
||||
});
|
||||
const componentLabels = computed(() => {
|
||||
if (!props.labels) return [];
|
||||
return props.labels[activeKey.value] ?? [];
|
||||
});
|
||||
|
||||
const dateTypeChange = (index) => {
|
||||
activeIndex.value = index;
|
||||
emit("changeDateType", tabList[index].value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tab {
|
||||
width: 70%;
|
||||
height: 14%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
.tab-item {
|
||||
padding: 1.2vh 1vw;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 0.8vw;
|
||||
white-space: nowrap;
|
||||
.background("@/assets/images/date-dimension-tab.png", @size: 100% 100%, @position: center);
|
||||
color: @text-color-primary;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background-image: url("@/assets/images/date-dimension-tab-select.png");
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 86%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="legend">
|
||||
<span class="legend-item" :style="{ backgroundColor: getBarColor(0) }"></span>
|
||||
<span>正常</span>
|
||||
<span class="legend-item" :style="{ backgroundColor: getBarColor(1) }"></span>
|
||||
<span>休息</span>
|
||||
<span class="legend-item" :style="{ backgroundColor: getBarColor(-1) }"></span>
|
||||
<span>未知</span>
|
||||
</div>
|
||||
<div class="days">
|
||||
<div class="item" v-for="(item, index) in displayData" :key="index" @click="doCar(item)">
|
||||
<div class="day">{{ item.day }}</div>
|
||||
<div class="bar" :style="{ backgroundColor: getBarColor(item.type) }"></div>
|
||||
<div class="tooltip">
|
||||
<div class="tooltip-content">
|
||||
<span class="label">早餐:</span>
|
||||
<span class="value">{{ item.morningCount ?? 0 }}人</span>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<span class="label">午餐:</span>
|
||||
<span class="value">{{ item.noonCount ?? 0 }}人</span>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<span class="label">晚餐:</span>
|
||||
<span class="value">{{ item.eveningCount ?? 0 }}人</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getMonthDays } from "@/utils/date";
|
||||
import { computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => getMonthDays().map((day) => ({ day, type: -1 })),
|
||||
},
|
||||
});
|
||||
|
||||
const displayData = computed(() => {
|
||||
const days = getMonthDays();
|
||||
const data = props.data;
|
||||
if (!Array.isArray(props.data)) return days.map((day) => ({ day, type: -1 }));
|
||||
if (props.data.length !== days.length) {
|
||||
for (let i = data.length; i < days.length; i++) {
|
||||
data.push({
|
||||
day: days[i],
|
||||
type: -1,
|
||||
});
|
||||
}
|
||||
return data.slice(0, days.length);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
const getBarColor = (type) => {
|
||||
if (type === 0) return "#40c9cb";
|
||||
if (type === 1) return "#5470C6";
|
||||
return "#7f7f7f77";
|
||||
};
|
||||
|
||||
// 新增:类型文案
|
||||
const getTypeText = (type) => {
|
||||
if (type === 0) return "正常";
|
||||
if (type === 1) return "休息";
|
||||
return "未知";
|
||||
};
|
||||
|
||||
const doCar = (item) => {
|
||||
if (!item.day) return;
|
||||
if (item.type == "-1") {
|
||||
ElMessage.warning(`还未到${item.day}号,无法跳转`);
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
// 抓拍时间
|
||||
swingTimeList: [
|
||||
dayjs().date(item.day).startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
dayjs().date(item.day).endOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
],
|
||||
};
|
||||
console.log(`|点击跳转|-|打卡统计|-|月|params:|{${JSON.stringify(params)}}|`);
|
||||
jumpAdminPath("door_reign_record", params);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.legend {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
font-size: 0.6vw;
|
||||
gap: 0.4vw;
|
||||
margin: 1vh 0;
|
||||
|
||||
.legend-item {
|
||||
width: 1vw;
|
||||
height: 0.5vw;
|
||||
}
|
||||
}
|
||||
|
||||
.days {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 0.2vw;
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
background-color: #122c40;
|
||||
border: 1px solid #1b3346;
|
||||
cursor: pointer;
|
||||
|
||||
.day {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -60%);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #cfe8f6;
|
||||
font-size: 0.8vw;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 0.3vh;
|
||||
}
|
||||
|
||||
/* Hover 弹框样式 */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #0f1f2e;
|
||||
border: 1px solid #1b3346;
|
||||
color: #cfe8f6;
|
||||
font-size: 0.7vw;
|
||||
padding: 0.4vw 0.6vw;
|
||||
border-radius: 0.3vw;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.18s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&:hover .tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: #0f1f2e transparent transparent transparent;
|
||||
filter: drop-shadow(0 -1px 0 rgba(27, 51, 70, 0.9));
|
||||
display: grid;
|
||||
gap: 0.2vw;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.2vw;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tooltip-row .label {
|
||||
color: #7fb5d6;
|
||||
}
|
||||
|
||||
.tooltip-row .value {
|
||||
color: #cfe8f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Object,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => [
|
||||
{ text: "在岗", name: "onJob", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "迟到", name: "beLate", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "早退", name: "leaveEarly", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
watch(
|
||||
() => [props.data, props.labels],
|
||||
(val) => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({
|
||||
xAxis: {
|
||||
data: props.labels?.map((v) => v.split("|")[1]) ?? [],
|
||||
},
|
||||
series: props.data.map((item, i) => {
|
||||
return {
|
||||
type: "bar",
|
||||
name: item.text,
|
||||
showBackground: false,
|
||||
barGap: "50%",
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
borderColor: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][0] },
|
||||
{ offset: 0.7, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][0] },
|
||||
{ offset: 1, color: linearGradientColors[i][1] },
|
||||
]),
|
||||
},
|
||||
},
|
||||
data: item.values,
|
||||
};
|
||||
}),
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const colors = ["#287CE7", "#F4CF3D", "#30C6DC"];
|
||||
const linearGradientColors = [
|
||||
["#01A5FA", "#086BA5", "#0E385A"],
|
||||
["#E0B60D", "#78752F", "#143750"],
|
||||
["#15D1C4", "#117883", "#0F3654"],
|
||||
];
|
||||
|
||||
const option = {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "14%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "4%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
valueFormatter: (value) => `${value}人`,
|
||||
},
|
||||
xAxis: {
|
||||
data: [],
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#21303C",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#00ffff",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
icon: "rect",
|
||||
right: 0,
|
||||
top: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 8,
|
||||
itemGap: 16,
|
||||
textStyle: { color: "#ffffff", fontSize: 9 },
|
||||
itemStyle: { borderWidth: 0, borderColor: "transparent" },
|
||||
data: props.data.map((item, i) => {
|
||||
return { name: item.text, itemStyle: { color: colors[i], borderWidth: 0, borderColor: "transparent" } };
|
||||
}),
|
||||
},
|
||||
series: props.data.map((item, i) => {
|
||||
return {
|
||||
type: "bar",
|
||||
name: item.text,
|
||||
showBackground: false,
|
||||
barGap: "50%",
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
borderColor: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][0] },
|
||||
{ offset: 0.7, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][0] },
|
||||
{ offset: 1, color: linearGradientColors[i][1] },
|
||||
]),
|
||||
},
|
||||
},
|
||||
data: item.values,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!chartRef.value) return;
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(option);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
chartInstance.on("click", function (params) {
|
||||
const target = props.labels[params.dataIndex];
|
||||
if (!target) return;
|
||||
const [timeStr, _] = target.split("|");
|
||||
const _params = {
|
||||
swingTimeList: [
|
||||
dayjs(timeStr).startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
dayjs(timeStr).endOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
],
|
||||
};
|
||||
console.log(`|点击跳转|-|打卡统计|-|本周|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("door_reign_record", _params);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Object,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => [
|
||||
{ text: "早饭", name: "morning", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "中饭", name: "noon", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "晚饭", name: "evening", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
const colors = ["#287CE7", "#F4CF3D", "#30C6DC"];
|
||||
const linearGradientColors = [
|
||||
["#01A5FA", "#086BA5", "#0E385A"],
|
||||
["#E0B60D", "#78752F", "#143750"],
|
||||
["#15D1C4", "#117883", "#0F3654"],
|
||||
];
|
||||
|
||||
watch(
|
||||
() => [props.labels, props.data],
|
||||
(val) => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({
|
||||
xAxis: {
|
||||
data: props.labels,
|
||||
},
|
||||
legend: {
|
||||
data: props.data.map((item, i) => {
|
||||
return { name: item.text, itemStyle: { color: colors[i], borderWidth: 0, borderColor: "transparent" } };
|
||||
}),
|
||||
},
|
||||
series: props.data.map((item, i) => {
|
||||
return {
|
||||
type: "bar",
|
||||
name: item.text,
|
||||
showBackground: false,
|
||||
barGap: "50%",
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
borderColor: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][0] },
|
||||
{ offset: 0.7, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][0] },
|
||||
{ offset: 1, color: linearGradientColors[i][1] },
|
||||
]),
|
||||
},
|
||||
},
|
||||
data: item.values,
|
||||
};
|
||||
}),
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const option = {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "14%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "4%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
valueFormatter: (value) => `${value}人`,
|
||||
},
|
||||
xAxis: {
|
||||
data: [],
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#21303C",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#00ffff",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
icon: "rect",
|
||||
right: 0,
|
||||
top: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 8,
|
||||
itemGap: 16,
|
||||
textStyle: { color: "#ffffff", fontSize: 9 },
|
||||
itemStyle: { borderWidth: 0, borderColor: "transparent" },
|
||||
data: [],
|
||||
},
|
||||
series: [],
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!chartRef.value) return;
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(option);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
chartInstance.on("click", function (params) {
|
||||
if (!params.name) return;
|
||||
const [year, month] = params.name.split("-");
|
||||
const _params = {
|
||||
// 抓拍时间
|
||||
swingTimeList: [
|
||||
dayjs()
|
||||
.year(year)
|
||||
.month(month - 1)
|
||||
.startOf("month")
|
||||
.format("YYYY-MM-DD HH:mm:ss"),
|
||||
dayjs()
|
||||
.year(year)
|
||||
.month(month - 1)
|
||||
.endOf("month")
|
||||
.format("YYYY-MM-DD HH:mm:ss"),
|
||||
],
|
||||
};
|
||||
console.log(`|点击跳转|-|打卡统计|-|年|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("door_reign_record", _params);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="item" :class="item.bgCls" v-for="(item, index) in displayData" :key="index" @click="handleClick(item)">
|
||||
<span class="value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
morning: 0,
|
||||
noon: 0,
|
||||
evening: 0,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const displayData = computed(() => {
|
||||
const { morning, noon, evening } = props.data;
|
||||
return [
|
||||
{ name: "早饭", type: "11", value: morning, bgCls: "on-job-bg" },
|
||||
{ name: "午饭", type: "100", value: noon, bgCls: "be-late-bg" },
|
||||
{ name: "晚饭", type: "200", value: evening, bgCls: "leave-early-bg" },
|
||||
];
|
||||
});
|
||||
|
||||
const handleClick = (item) => {
|
||||
const params = {
|
||||
// 抓拍时间
|
||||
swingTimeList: [
|
||||
dayjs().subtract(1, "day").startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
dayjs().startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
],
|
||||
// 类型
|
||||
alarmTypeList: [],
|
||||
personIdIsNotNull: true,
|
||||
};
|
||||
const alarmTypeItem = displayData.value.find((v) => v.name == item.name);
|
||||
if (alarmTypeItem) {
|
||||
params.alarmTypeList = [alarmTypeItem.type];
|
||||
}
|
||||
console.log(`|点击跳转|-|打卡统计|-|昨日|params:|{${JSON.stringify(params)}}|`);
|
||||
jumpAdminPath("door_reign_record", params);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
.item {
|
||||
width: 24%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
.value {
|
||||
position: absolute;
|
||||
top: 46%;
|
||||
left: 46%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 1.2vw;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.on-job-bg {
|
||||
background-image: url(@/assets/images/on-job-bg.png) !important;
|
||||
}
|
||||
|
||||
.be-late-bg {
|
||||
background-image: url(@/assets/images/be-late-bg.png) !important;
|
||||
}
|
||||
|
||||
.leave-early-bg {
|
||||
background-image: url(@/assets/images/leave-early-bg.png) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-table :data="state.dataList" :size="TABLE.SIZE" :style="TABLE.STYLE" :cell-style="TABLE.CELL_STYLE"
|
||||
:row-style="TABLE.ROW_STYLE" :header-cell-style="TABLE.HEADER_CELL_STYLE" border
|
||||
:summary-method="getSummaries" show-summary>
|
||||
<el-table-column align="center" type="index" label="#" width="50" />
|
||||
<el-table-column align="center" label="商户" prop="merchantName" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.merchantName || "未知" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流水金额" prop="totalPayPrice">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalPayPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流水次数" prop="totalPayCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">
|
||||
{{ row.totalPayCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="退款金额" prop="totalRefundPrice">
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ displayPrice(row.totalRefundPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="退款次数" prop="totalRefundCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ row.totalRefundCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="消费净金额" prop="totalProfitPrice" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalProfitPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="消费净次数" prop="totalProfitCount" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ row.totalProfitCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, reactive } from "vue";
|
||||
|
||||
import { canteenApi } from "@/api/index"
|
||||
import { displayPrice } from "@/utils/format-money"
|
||||
import { TABLE } from "@/constant/table-constant"
|
||||
|
||||
const state = reactive({
|
||||
dataList: [],
|
||||
queryParams: {
|
||||
createTimeList: []
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
|
||||
// 食堂报表
|
||||
const getDataList = async () => {
|
||||
try {
|
||||
const { code, data } = await canteenApi.canteenReport({ ...state.queryParams })
|
||||
console.log("食堂报表:", data);
|
||||
state.dataList = data
|
||||
} catch (error) {
|
||||
console.error("|食堂报表|意外的错误:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async (createTimeList) => {
|
||||
state.queryParams.createTimeList = createTimeList ?? null
|
||||
await getDataList()
|
||||
}
|
||||
|
||||
const getSummaries = (param) => {
|
||||
const { columns, data } = param
|
||||
const totalColumns = ["totalPayPrice", "totalPayCount", "totalRefundPrice", "totalRefundCount", "totalProfitPrice", "totalProfitCount"]
|
||||
const totalRow = Array.from({ length: columns.length })
|
||||
totalRow[0] = "合计"
|
||||
columns.forEach((i, index) => {
|
||||
if (totalColumns.includes(i.property)) {
|
||||
const total = data
|
||||
.filter((v) => v[i.property] != null)
|
||||
.map((v) => v[i.property])
|
||||
.reduce((pren, next) => pren + next, 0)
|
||||
if (i.property.includes("Price")) {
|
||||
totalRow[index] = displayPrice(total)
|
||||
} else {
|
||||
totalRow[index] = total
|
||||
}
|
||||
}
|
||||
})
|
||||
return totalRow
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
tr {
|
||||
background: #1d283477 !important;
|
||||
}
|
||||
|
||||
.el-table__footer-wrapper {
|
||||
.el-table__cell {
|
||||
background: #1d283477 !important;
|
||||
border-color: #121F33 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { getRecentMonths } from "@/utils/date";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
in: [],
|
||||
out: [],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
onMounted(async () => {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(option);
|
||||
window.addEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
if (!chartInstance) return;
|
||||
const { in: inData, out: outData } = props.data;
|
||||
const inChart = inData.list?.map((v) => {
|
||||
return v.carCount;
|
||||
});
|
||||
const outChart = outData.list?.map((v) => {
|
||||
return v.carCount;
|
||||
});
|
||||
chartInstance.setOption({
|
||||
series: [
|
||||
{ name: outData.typeStr, data: outChart },
|
||||
{ name: inData.typeStr, data: inChart },
|
||||
],
|
||||
xAxis: {
|
||||
data: inData.list?.map((v) => {
|
||||
return v.time;
|
||||
}),
|
||||
},
|
||||
legend: {
|
||||
data: [{ name: outData.typeStr }, { name: inData.typeStr }],
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const option = {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "18%",
|
||||
left: "0%",
|
||||
right: "4%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
// formatter: (params) => {
|
||||
// return `${params[0].axisValue}<br/>${params[0].marker}${params[0].value}辆<br/>
|
||||
// ${params[1].axisValue}<br/>${params[1].marker}${params[1].value}辆<br/>
|
||||
// ${params[2].axisValue}<br/>${params[2].marker}${params[2].value}辆<br/>`;
|
||||
// },
|
||||
valueFormatter: (value) => value,
|
||||
},
|
||||
legend: {
|
||||
icon: "rect",
|
||||
right: 0,
|
||||
top: 0,
|
||||
itemWidth: 14,
|
||||
itemHeight: 8,
|
||||
itemGap: 16,
|
||||
textStyle: { color: "#ffffff", fontSize: 12 },
|
||||
itemStyle: { borderWidth: 0, borderColor: "transparent" },
|
||||
data: [
|
||||
{ name: "车出", itemStyle: { color: "#1DB996", borderWidth: 0, borderColor: "transparent" } },
|
||||
{ name: "车入", itemStyle: { color: "#01C6EF", borderWidth: 0, borderColor: "transparent" } },
|
||||
],
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#21303C",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
// backgroundColor: "#2d5a3d",
|
||||
// padding: [4, 4, 4, 4],
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "车辆数",
|
||||
min: 0,
|
||||
nameTextStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
padding: [0, 0, 0, 0],
|
||||
// align: "right",
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: "#00ffff",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "车出",
|
||||
type: "line",
|
||||
// stack: "Total",
|
||||
data: props.data?.out ?? [],
|
||||
smooth: true,
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
yAxisIndex: 0,
|
||||
lineStyle: {
|
||||
color: "#1DB996",
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#1DB996",
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
formatter: "{c}",
|
||||
color: "#1DB996",
|
||||
fontWeight: "bold",
|
||||
fontSize: 12,
|
||||
},
|
||||
areaStyle: {
|
||||
color: "#1DB996",
|
||||
opacity: 0.8,
|
||||
color: {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
// { offset: 0, color: "rgba(255, 107, 107, 0.3)" },
|
||||
{ offset: 0, color: "#1C7C67" },
|
||||
{ offset: 1, color: "#0D1E25" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "车入",
|
||||
type: "line",
|
||||
// stack: "Total",
|
||||
data: props.data?.in ?? [],
|
||||
smooth: true,
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
yAxisIndex: 0,
|
||||
lineStyle: {
|
||||
color: "#01C6EF",
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#01C6EF",
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
formatter: "{c}",
|
||||
color: "#01C6EF",
|
||||
fontWeight: "bold",
|
||||
fontSize: 12,
|
||||
},
|
||||
areaStyle: {
|
||||
color: "#01C6EF",
|
||||
opacity: 0.8,
|
||||
color: {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
// { offset: 0, color: "rgba(255, 107, 107, 0.3)" },
|
||||
{ offset: 0, color: "#097186" },
|
||||
{ offset: 1, color: "#0D1E25" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="count-total">
|
||||
<div class="total-item in-bg" @click="doTotalCar('enter')">
|
||||
<div class="total-item-container">
|
||||
<div class="text">在位军车数量</div>
|
||||
<div class="count" style="color: #9ce7e8">{{ total?.inCarNum ?? 0 }} 辆</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="total-item not-in-bg" @click="doTotalCar('exit')">
|
||||
<div class="total-item-container">
|
||||
<div class="text">不在位军车数量</div>
|
||||
<div class="count" style="color: #f5d097">{{ total?.exitCarNum ?? 0 }} 辆</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="out-statue">
|
||||
<div class="item" v-for="(item, index) in data.items" :key="index" @click="doCar(item)">
|
||||
<div class="name">
|
||||
<span>{{ item.typeName }}</span>
|
||||
</div>
|
||||
<div class="number">
|
||||
<div>
|
||||
<span>出车次数</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ item.outNumber }} 次</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="duration" v-if="false">
|
||||
<div>出车时长</div>
|
||||
<div>{{ item.outDuration }} 时</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
total: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
items: [],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const doCar = (item) => {
|
||||
// if (item.outNumber == 0) {
|
||||
// ElMessage.warning(`${item.typeName}过车次数为0,无法跳转`);
|
||||
// return;
|
||||
// }
|
||||
// const _params = {
|
||||
// // 抓拍时间
|
||||
// capTimeList: DateTool.getFormattedDateTimeRange(item.type),
|
||||
// // 军车
|
||||
// isArmyCar: "1"
|
||||
// }
|
||||
// console.log(`|点击跳转|-|军车出车情况统计|-|${item.name}|params:|{${JSON.stringify(_params)}}|`);
|
||||
// jumpAdminPath("car_record", _params)
|
||||
};
|
||||
|
||||
const doTotalCar = (type) => {
|
||||
// const _params = {
|
||||
// inTimeList: DateTool.getFormattedDateTimeRange("TODAY"),
|
||||
// isArmyCar: "1"
|
||||
// }
|
||||
// if (type == "enter") {
|
||||
// _params.isEnter = '1'
|
||||
// }
|
||||
// if (type == "exit") {
|
||||
// _params.isEnter = '0'
|
||||
// }
|
||||
// jumpAdminPath("car_park", _params)
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2vh;
|
||||
|
||||
.count-total {
|
||||
width: 100%;
|
||||
height: 26%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.total-item {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 80% 100%;
|
||||
padding-right: 2vw;
|
||||
color: #c7cfd3;
|
||||
cursor: pointer;
|
||||
|
||||
.total-item-container {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: 1vw;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
|
||||
.text {
|
||||
font-size: 0.7vw;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 0.8vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.in-bg {
|
||||
background-image: url("@/assets/images/car-count-in-bg.png");
|
||||
}
|
||||
|
||||
.not-in-bg {
|
||||
background-image: url("@/assets/images/car-count-not-in-bg.png");
|
||||
}
|
||||
}
|
||||
|
||||
.out-statue {
|
||||
width: 100%;
|
||||
height: 74%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.item {
|
||||
height: 100%;
|
||||
width: calc(100% / 5 - 1vw);
|
||||
background-image: url("@/assets/images/car-out-statue-item-bg.png");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #d1eafd;
|
||||
cursor: pointer;
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
height: calc(100% / 3);
|
||||
font-size: 0.7vw;
|
||||
}
|
||||
|
||||
.number {
|
||||
height: calc(100% / 3);
|
||||
font-size: 0.6vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.duration {
|
||||
height: calc(100% / 3);
|
||||
font-size: 0.6vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="menu">
|
||||
<span
|
||||
class="menu-item"
|
||||
v-for="(item, index) in menuList"
|
||||
:key="index"
|
||||
:class="{ active: activeIndex === index }"
|
||||
@click="changeTab(index)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-type">
|
||||
<div v-if="false">按出车次数</div>
|
||||
<div v-if="false" style="margin-left: 1vw">按出车时长</div>
|
||||
</div>
|
||||
<div ref="chartRef" style="width: 100%; height: 74%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, watch, useTemplateRef, ref, computed } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const emit = defineEmits(["changeDateType"]);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
const menuList = [
|
||||
{ label: "昨日", value: "YESTERDAY" },
|
||||
{ label: "今日", value: "TODAY" },
|
||||
{ label: "本周", value: "THIS_WEEK" },
|
||||
{ label: "本月", value: "THIS_MONTH" },
|
||||
{ label: "本年", value: "THIS_YEAR" },
|
||||
];
|
||||
const activeIndex = ref(0);
|
||||
|
||||
const activeKey = computed(() => menuList[activeIndex.value].value);
|
||||
|
||||
watch(
|
||||
() => activeKey.value,
|
||||
() => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({ series: [{ data: props.data[activeKey.value] }] });
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [props.data],
|
||||
() => {
|
||||
if (!chartInstance) return;
|
||||
const labels = props.data.map((item) => item.carNum) ?? [];
|
||||
const seriesData = props.data.map((item) => item.timeCount) ?? [];
|
||||
console.log(props.data);
|
||||
|
||||
chartInstance.setOption({
|
||||
yAxis: { data: labels.reverse() },
|
||||
series: [{ data: seriesData.reverse() }],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const changeTab = (index) => {
|
||||
activeIndex.value = index;
|
||||
emit("changeDateType", menuList[index].value);
|
||||
};
|
||||
|
||||
const options = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
axisPointer: { type: "shadow" },
|
||||
// formatter: (params) => {
|
||||
// const p = Array.isArray(params) ? params[0] : params;
|
||||
// return `${p.name}:${p.value}`;
|
||||
// },
|
||||
confine: true,
|
||||
},
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "0%",
|
||||
left: "0%",
|
||||
right: "4%",
|
||||
bottom: "2%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "value",
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: "rgba(255,255,255,0.6)" },
|
||||
splitLine: { show: true, lineStyle: { color: "rgba(255,255,255,0.08)" } },
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: "#fff" },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "bar",
|
||||
data: [],
|
||||
barWidth: 14,
|
||||
showBackground: true,
|
||||
backgroundStyle: { color: "rgba(255,255,255,0.06)" },
|
||||
itemStyle: {
|
||||
borderRadius: [0, 10, 10, 0],
|
||||
color: (params) => {
|
||||
const palette = [
|
||||
["#112537", "#3081DC"],
|
||||
["#112537", "#3081DC"],
|
||||
["#112537", "#D0F43D"],
|
||||
["#112537", "#3081DC"],
|
||||
["#112537", "#3081DC"],
|
||||
];
|
||||
const [c1, c2] = palette[params.dataIndex % palette.length];
|
||||
return new echarts.graphic.LinearGradient(1, 0, 0, 0, [
|
||||
{ offset: 0, color: c2 },
|
||||
{ offset: 1, color: c1 },
|
||||
]);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!chartRef.value) return;
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(options);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
// chartInstance.on('click', (params) => {
|
||||
// console.log("params:", params);
|
||||
// if (!params.name) return
|
||||
// const _params = {
|
||||
// // 抓拍时间
|
||||
// capTimeList: DateTool.getFormattedDateTimeRange(activeKey.value),
|
||||
// // 车牌号
|
||||
// carNum: params.name
|
||||
// }
|
||||
// console.log(`|点击跳转|-|军车出车情况排行|-|${params.name}|params:|{${JSON.stringify(_params)}}|`);
|
||||
// jumpAdminPath("car_record", _params)
|
||||
// })
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.menu {
|
||||
width: 70%;
|
||||
height: 14%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
.menu-item {
|
||||
padding: 1.2vh 1vw;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 0.8vw;
|
||||
white-space: nowrap;
|
||||
background-image: url("@/assets/images/date-dimension-tab.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
color: #419ce3;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background-image: url("@/assets/images/date-dimension-tab-select.png");
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-type {
|
||||
display: flex;
|
||||
height: 10%;
|
||||
padding: 1vh 0;
|
||||
font-size: 0.8vw;
|
||||
cursor: pointer;
|
||||
|
||||
&.status-type-active {
|
||||
color: #419ce3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="car-status-img">
|
||||
<img class="img" src="@/assets/images/car-status.png" />
|
||||
</div>
|
||||
<div class="car-status-info">
|
||||
<!-- start 在册私家车 start -->
|
||||
<div class="info-item" @click="doCar('carManage')">
|
||||
<div class="info-title">
|
||||
<el-icon style="color: #0494b9; opacity: 0.5">
|
||||
<ArrowRightBold />
|
||||
</el-icon>
|
||||
<el-icon style="color: #0494b9; margin-left: -6px">
|
||||
<ArrowRightBold />
|
||||
</el-icon>
|
||||
<div>当前营区在册私家车数量</div>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<span class="count">{{ data?.carManage?.selfCarTotal ?? 0 }}</span>
|
||||
<span class="unit">辆</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end 在册私家车 end -->
|
||||
|
||||
<!-- start 私家车情况 start -->
|
||||
<div class="info-item" @click="doCar('selfCar')">
|
||||
<div class="info-title">
|
||||
<el-icon style="color: #0494b9; opacity: 0.5">
|
||||
<ArrowRightBold />
|
||||
</el-icon>
|
||||
<el-icon style="color: #0494b9; margin-left: -6px">
|
||||
<ArrowRightBold />
|
||||
</el-icon>
|
||||
<div>当前营区在册私家车进</div>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<span class="count">{{ data?.selfCar?.enterCarNum ?? 0 }}</span>
|
||||
<span class="unit">辆</span>,
|
||||
</div>
|
||||
<div class="info-title">出</div>
|
||||
<div class="info-content">
|
||||
<span class="count">{{ data?.selfCar?.exitCarNum ?? 0 }}</span>
|
||||
<span class="unit">辆</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end 私家车情况 end -->
|
||||
|
||||
<!-- start 外部车情况 start -->
|
||||
<div class="info-item" @click="doCar('foreignCar')">
|
||||
<div class="info-title">
|
||||
<el-icon style="color: #0494b9; opacity: 0.5">
|
||||
<ArrowRightBold />
|
||||
</el-icon>
|
||||
<el-icon style="color: #0494b9; margin-left: -6px">
|
||||
<ArrowRightBold />
|
||||
</el-icon>
|
||||
<div>当前营区外来车进</div>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<span class="count">{{ data?.foreignCar?.enterCarNum ?? 0 }}</span>
|
||||
<span class="unit">辆</span>,
|
||||
</div>
|
||||
<div class="info-title">出</div>
|
||||
<div class="info-content">
|
||||
<span class="count">{{ data?.foreignCar?.exitCarNum ?? 0 }}</span>
|
||||
<span class="unit">辆</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end 外部车情况 end -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowRightBold } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const doCar = (type) => {
|
||||
// let actionType = ""
|
||||
// const _params = {}
|
||||
// if (type == "carManage") {
|
||||
// actionType = "car_manage"
|
||||
// _params.isArmyCar = '0'
|
||||
// }
|
||||
// if (type == "selfCar") {
|
||||
// actionType = "car_park"
|
||||
// _params.inTimeList = DateTool.getFormattedDateTimeRange("TODAY")
|
||||
// _params.isInner = '0'
|
||||
// _params.isArmyCar = '0'
|
||||
// }
|
||||
// if (type == "foreignCar") {
|
||||
// actionType = "car_park"
|
||||
// _params.inTimeList = DateTool.getFormattedDateTimeRange("TODAY")
|
||||
// _params.isInner = '1'
|
||||
// _params.isArmyCar = '0'
|
||||
// }
|
||||
// if (!actionType) return
|
||||
// jumpAdminPath(actionType, _params)
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.car-status-img {
|
||||
width: calc(100% / 3);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.car-status-info {
|
||||
width: calc(100% / 3 * 2 + 5vw);
|
||||
height: 60%;
|
||||
background-image: url(@/assets/images/car-status-info-bg.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding-left: 12%;
|
||||
font-size: 0.6vw;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.info-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.count {
|
||||
font-size: 0.7vw;
|
||||
color: #00d8ff;
|
||||
}
|
||||
|
||||
.unit {
|
||||
margin-left: 0vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(option);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.data, props.labels],
|
||||
() => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({
|
||||
series: [{ data: props.data }],
|
||||
xAxis: {
|
||||
data: props.labels,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const option = {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "18%",
|
||||
left: "0%",
|
||||
right: "2%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
// formatter: (params) => {
|
||||
// return `${params[0].axisValue}<br/>${params[0].marker}${params[0].value}辆<br/>
|
||||
// ${params[1].axisValue}<br/>${params[1].marker}${params[1].value}辆<br/>
|
||||
// ${params[2].axisValue}<br/>${params[2].marker}${params[2].value}辆<br/>`;
|
||||
// },
|
||||
valueFormatter: (value) => value + "次",
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#21303C",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
// backgroundColor: "#2d5a3d",
|
||||
// padding: [4, 4, 4, 4],
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "告警率(次)",
|
||||
min: 0,
|
||||
nameTextStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
padding: [0, 0, 0, 60]
|
||||
// align: "right",
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: "#00ffff",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "设备告警率",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
data: props.data,
|
||||
smooth: true,
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
yAxisIndex: 0,
|
||||
lineStyle: {
|
||||
color: "#1DB996",
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#1DB996",
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
formatter: "{c}",
|
||||
color: "#1DB996",
|
||||
fontWeight: "bold",
|
||||
fontSize: 12,
|
||||
},
|
||||
areaStyle: {
|
||||
color: "#1DB996",
|
||||
opacity: 0.8,
|
||||
color: {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
// { offset: 0, color: "rgba(255, 107, 107, 0.3)" },
|
||||
{ offset: 0, color: "#1C7C67" },
|
||||
{ offset: 1, color: "#0D1E25" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="alarm-list">
|
||||
<div class="totals">
|
||||
<div class="total-card" @click="doTotalCar('today')">
|
||||
<div class="total-title">今日告警数</div>
|
||||
<div class="total-value" style="color: #ef3121">{{ data.todayTotal }}</div>
|
||||
</div>
|
||||
<div class="total-card" @click="doTotalCar('all')">
|
||||
<div class="total-title">告警总数</div>
|
||||
<div class="total-value" style="color: #ece415">{{ data.allTotal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table">
|
||||
<div class="thead">
|
||||
<div class="th">序号</div>
|
||||
<div class="th" style="width: 6vw">告警设备</div>
|
||||
<div class="th">告警区域</div>
|
||||
<div class="th" style="color: rgba(255, 255, 255, 0.7); width: 8vw">告警时间</div>
|
||||
</div>
|
||||
<div class="tbody scrollbar-hide">
|
||||
<div v-for="(row, idx) in data.list" :key="idx" class="tr" @click="doCar(row)">
|
||||
<div class="td">
|
||||
<span class="index-badge">{{ idx + 1 }}</span>
|
||||
</div>
|
||||
<div class="td" style="width: 6vw">
|
||||
<span class="device-dot"></span>
|
||||
<span class="text-overflow">{{ row.deviceName }}</span>
|
||||
</div>
|
||||
<div class="td text-overflow" style="width: 5vw">{{ row.areaName }}</div>
|
||||
<div class="td" style="width: 8vw">{{ row.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
list: [],
|
||||
todayTotal: 0,
|
||||
allTotal: 0,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const doTotalCar = (type) => {
|
||||
// const _params = {}
|
||||
// if (type == "today") {
|
||||
// _params.alarmDateList = DateTool.getFormattedDateTimeRange("TODAY")
|
||||
// }
|
||||
// console.log(`|点击跳转|-|设备告警实时统计统计|params:|{${JSON.stringify(_params)}}|`);
|
||||
// jumpAdminPath("device_alarm", _params)
|
||||
};
|
||||
|
||||
const doCar = (row) => {
|
||||
console.log("row:", row);
|
||||
const _params = {
|
||||
alarmCode: row.uniqueCode,
|
||||
};
|
||||
console.log(`|点击跳转|-|设备告警实时统计列表|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("device_alarm", _params);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.text-overflow {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.totals {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1vw;
|
||||
|
||||
.total-card {
|
||||
height: 7vh;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("@/assets/images/device-alarm-list-total-bg.png");
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1vw;
|
||||
gap: 1vw;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.total-title {
|
||||
color: #fff;
|
||||
font-size: 0.9vw;
|
||||
margin-left: 20%;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
margin-left: auto;
|
||||
font-weight: 700;
|
||||
font-size: 1vw;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top: 1vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
.thead {
|
||||
display: grid;
|
||||
grid-template-columns: 0.6fr 1fr 1fr 1.6fr;
|
||||
gap: 1vh;
|
||||
padding: 1vh;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
|
||||
.th {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 0.8vw;
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tbody {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1vh;
|
||||
padding-top: 1vh;
|
||||
min-height: 0;
|
||||
|
||||
.tr {
|
||||
display: grid;
|
||||
grid-template-columns: 0.6fr 1fr 1fr 1.6fr;
|
||||
gap: 1vh;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #0c1d31;
|
||||
border-radius: 8px;
|
||||
padding: 1vh 0;
|
||||
cursor: pointer;
|
||||
|
||||
.td {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 0.7vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
.index-badge {
|
||||
width: 1.6vw;
|
||||
height: 1vw;
|
||||
line-height: 1vw;
|
||||
text-align: center;
|
||||
color: #cfe8ff;
|
||||
font-weight: 600;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("@/assets/images/device-alarm-list-index-bg.png");
|
||||
background-size: 100% 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.device-dot {
|
||||
width: 0.6vw;
|
||||
height: 0.6vw;
|
||||
border-radius: 50%;
|
||||
background: #3bd2a6;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏滚动条 */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
<div class="area-info">
|
||||
<div class="area-info-item" v-for="(item, i) in props.data" :key="item.name" @click="doCar(item)">
|
||||
<span
|
||||
:style="{ width: '1vw', height: '1vw', background: colors[i], borderRadius: '50%', display: 'inline-block' }"></span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span :style="{ marginLeft: '0.2vw', color: colors[i] }">{{ item.total }}台</span>
|
||||
<span :style="{ marginLeft: '0.1vw', color: colors[i], fontSize: '0.6vw' }">({{ percent(item.total) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, useTemplateRef, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: "监控总数" },
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
const colors = ["#5470C6", "#91CC75", "#FFC83D", "#FF6B6B", "#9B59B6"];
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(options);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.data, props.title],
|
||||
() => {
|
||||
if (!chartInstance) return;
|
||||
console.log(props.data);
|
||||
|
||||
chartInstance.setOption({
|
||||
graphic: [{ style: { text: total.value } }],
|
||||
series: [
|
||||
{
|
||||
data: props.data.map((v, i) => {
|
||||
return {
|
||||
name: v.name,
|
||||
value: Number(v.total),
|
||||
itemStyle: { color: colors[i] },
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const total = computed(() => {
|
||||
if (!props.data || !props.data.length) return [];
|
||||
return props.data.reduce((sum, i) => sum + (Number(i.total) || 0), 0);
|
||||
});
|
||||
|
||||
const doCar = (item) => {
|
||||
if (!item.name) return;
|
||||
if (item.total == 0) {
|
||||
ElMessage.warning(`当前${item.name}设备总数为0,无法跳转`);
|
||||
return;
|
||||
}
|
||||
const _params = {
|
||||
// 空间名
|
||||
areaName: item.name,
|
||||
};
|
||||
console.log(`|点击跳转|-|监控区域统计|-|${item.name}|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("area_house", _params);
|
||||
};
|
||||
|
||||
const options = {
|
||||
backgroundColor: "transparent",
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
// formatter: (p) => `${p.name}:${p.value} (${p.percent}%)`,
|
||||
confine: true,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "pie",
|
||||
radius: ["60%", "78%"],
|
||||
center: ["50%", "50%"],
|
||||
avoidLabelOverlap: true,
|
||||
label: { show: false },
|
||||
labelLine: { show: false },
|
||||
itemStyle: { borderWidth: 0 },
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
graphic: [
|
||||
{
|
||||
type: "text",
|
||||
left: "40%",
|
||||
top: "40%",
|
||||
style: {
|
||||
text: String(total.value),
|
||||
fill: "#fff",
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
textAlign: "center",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
left: "39%",
|
||||
top: "55%",
|
||||
style: {
|
||||
text: props.title,
|
||||
fill: "#B8C7D9",
|
||||
fontSize: 12,
|
||||
textAlign: "center",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
const percent = (v) => {
|
||||
if (!total.value) return "0%";
|
||||
return ((Number(v) / Number(total.value)) * 100).toFixed(1) + "%";
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.area-info {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.area-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.4vw 0;
|
||||
font-size: 0.7vw;
|
||||
cursor: pointer;
|
||||
|
||||
.name {
|
||||
width: 5vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
margin-left: 0.2vw;
|
||||
color: #b8c7d9;
|
||||
font-size: 0.7vw;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dashed #eeeeee99;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, watch, useTemplateRef } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(options);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
chartInstance.on("click", (params) => {
|
||||
if (!params.name) return;
|
||||
if (params.data == 0) {
|
||||
ElMessage.warning(`当前${params.name}设备总数为0,无法跳转`);
|
||||
return;
|
||||
}
|
||||
const _params = {
|
||||
// 空间名
|
||||
areaName: params.name,
|
||||
};
|
||||
console.log(`|点击跳转|-|监控数量排行|-|${params.name}|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("area_house", _params);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!chartInstance) return;
|
||||
console.log(props.data,);
|
||||
|
||||
chartInstance.setOption({
|
||||
yAxis: {
|
||||
data: props.data?.labels?.map((v) => {
|
||||
return v;
|
||||
}),
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: props.data?.values?.map((v) => {
|
||||
return v;
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const options = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
axisPointer: { type: "shadow" },
|
||||
// formatter: (params) => {
|
||||
// const p = Array.isArray(params) ? params[0] : params;
|
||||
// return `${p.name}:${p.value}`;
|
||||
// },
|
||||
confine: true,
|
||||
},
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "0%",
|
||||
left: "0%",
|
||||
right: "4%",
|
||||
bottom: "0%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "value",
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: "rgba(255,255,255,0.6)" },
|
||||
splitLine: { show: true, lineStyle: { color: "rgba(255,255,255,0.08)" } },
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
data: props.labels,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: "#fff" },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "bar",
|
||||
data: props.data,
|
||||
barWidth: 14,
|
||||
showBackground: true,
|
||||
backgroundStyle: { color: "rgba(255,255,255,0.06)" },
|
||||
itemStyle: {
|
||||
borderRadius: [0, 10, 10, 0],
|
||||
color: (params) => {
|
||||
const palette = [
|
||||
["#1F4F68", "#2EE4D9"],
|
||||
["#1F4F68", "#2EE4D9"],
|
||||
["#1F4F68", "#F57F3C"],
|
||||
["#1F4F68", "#2EE4D9"],
|
||||
["#1F4F68", "#2EE4D9"],
|
||||
];
|
||||
const [c1, c2] = palette[params.dataIndex % palette.length];
|
||||
return new echarts.graphic.LinearGradient(1, 0, 0, 0, [
|
||||
{ offset: 0, color: c2 },
|
||||
{ offset: 1, color: c1 },
|
||||
]);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { getRecentDays } from "@/utils/date";
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(option);
|
||||
window.addEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.data, props.labels],
|
||||
(val) => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({
|
||||
xAxis: {
|
||||
data: props.labels,
|
||||
},
|
||||
series: [{ data: props.data }],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const option = {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "18%",
|
||||
left: "0%",
|
||||
right: "4%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
// formatter: (params) => {
|
||||
// return `${params[0].axisValue}<br/>${params[0].marker}${params[0].value}辆<br/>
|
||||
// ${params[1].axisValue}<br/>${params[1].marker}${params[1].value}辆<br/>
|
||||
// ${params[2].axisValue}<br/>${params[2].marker}${params[2].value}辆<br/>`;
|
||||
// },
|
||||
valueFormatter: (value) => value + "%",
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#5598B3",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
// backgroundColor: "#2d5a3d",
|
||||
// padding: [4, 4, 4, 4],
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "购买次数",
|
||||
min: 0,
|
||||
nameTextStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
padding: [0, 0, 0, 60]
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#374D59",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
// formatter: "{value}%",
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "设备在线率",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
data: props.data,
|
||||
smooth: true,
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
yAxisIndex: 0,
|
||||
lineStyle: {
|
||||
color: "#1FE9D5",
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#1FE9D5",
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
formatter: "{c}",
|
||||
color: "#1FE9D5",
|
||||
fontWeight: "bold",
|
||||
fontSize: 12,
|
||||
},
|
||||
areaStyle: {
|
||||
color: "#1FE9D5",
|
||||
opacity: 0.8,
|
||||
color: {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
// { offset: 0, color: "rgba(255, 107, 107, 0.3)" },
|
||||
{ offset: 0, color: "#129F97" },
|
||||
{ offset: 1, color: "#102837" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="device" v-for="(item, index) in displayData" :key="index" @click="doCar(item)">
|
||||
<img :src="item.img" />
|
||||
<div class="info">
|
||||
<div class="title">{{ item.name }}</div>
|
||||
<div class="number">
|
||||
<span>总数</span>
|
||||
<span :style="{ marginLeft: '0.3vw', color: item.color }">{{ item.total }}</span>
|
||||
<span style="margin-left: 12%">在线</span>
|
||||
<span :style="{ marginLeft: '0.3vw', color: item.color }">{{ item.online }}</span>
|
||||
<span style="margin-left: 12%">在线率</span>
|
||||
<span :style="{ marginLeft: '0.3vw', color: item.color }">{{ item.onlineRate.toFixed(2) }}%</span>
|
||||
</div>
|
||||
<div class="percent" :class="item.bgClass" :style="{ backgroundSize: item.bgSize }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
import entranceGuardImg from "@/assets/images/entrance-guard.png";
|
||||
import barrierGateImg from "@/assets/images/barrier-gate.png";
|
||||
import monitoringImg from "@/assets/images/monitoring.png";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{
|
||||
name: "门禁设备",
|
||||
total: 0,
|
||||
online: 0,
|
||||
onlineRate: 0,
|
||||
},
|
||||
{
|
||||
name: "闸道设备",
|
||||
total: 0,
|
||||
online: 0,
|
||||
onlineRate: 0,
|
||||
},
|
||||
{
|
||||
name: "监控设备",
|
||||
total: 0,
|
||||
online: 0,
|
||||
onlineRate: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const styleConfig = [
|
||||
{
|
||||
img: entranceGuardImg,
|
||||
color: "#39f291",
|
||||
bgClass: "guard",
|
||||
},
|
||||
{
|
||||
img: barrierGateImg,
|
||||
color: "#00ebfb",
|
||||
bgClass: "gate",
|
||||
},
|
||||
{
|
||||
img: monitoringImg,
|
||||
color: "#f7bb03",
|
||||
bgClass: "monitoring",
|
||||
},
|
||||
];
|
||||
|
||||
const doCar = (item) => {
|
||||
if (item.total == 0) {
|
||||
ElMessage.warning(`当前${item.name}设备总数为0,无法跳转`);
|
||||
return;
|
||||
}
|
||||
const _params = {
|
||||
// 设备大类
|
||||
deviceCategory: item.type,
|
||||
};
|
||||
console.log(`|点击跳转|-|设备状态统计|-|${item.name}|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("device_manage", _params);
|
||||
};
|
||||
|
||||
const displayData = computed(() => {
|
||||
const data = Array.isArray(props.data) ? props.data : [];
|
||||
return styleConfig.map((style, index) => {
|
||||
const item = data[index] || { name: "", total: 0, online: 0 };
|
||||
const total = Number(item.total) || 0;
|
||||
const online = Number(item.online) || 0;
|
||||
const rate = total > 0 ? (online / total) * 100 : 0;
|
||||
return {
|
||||
name: item.name || "",
|
||||
total,
|
||||
online,
|
||||
type: item.type,
|
||||
onlineRate: rate,
|
||||
img: style.img,
|
||||
color: style.color,
|
||||
bgClass: style.bgClass,
|
||||
bgSize: `${rate}% 100%`,
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
.device {
|
||||
width: 94%;
|
||||
height: calc(90% / 3);
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
|
||||
.info {
|
||||
background-image: url("@/assets/images/device-status-bg.png");
|
||||
background-size: 100% 100%;
|
||||
width: 100%;
|
||||
padding: 0 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.title {
|
||||
font-size: 0.7vw;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
background-image: linear-gradient(to bottom, #ffffff, #367fab);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 0.6vw;
|
||||
color: #ecf7ff;
|
||||
}
|
||||
|
||||
.percent {
|
||||
height: 0.6vh;
|
||||
background-color: #002f51;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.guard {
|
||||
background-image: url("@/assets/images/entrance-guard-percent.png");
|
||||
}
|
||||
|
||||
.gate {
|
||||
background-image: url("@/assets/images/barrier-gate-percent.png");
|
||||
}
|
||||
|
||||
.monitoring {
|
||||
background-image: url("@/assets/images/monitoring-percent.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="video-list">
|
||||
<div class="video-item" :class="{ 'video-item-active': index === curIndex }" @click="curIndex = index"
|
||||
v-for="(item, index) in 4" :key="index"></div>
|
||||
</div>
|
||||
<div class="video-operate">
|
||||
<el-button type="primary" plain size="small">自适应</el-button>
|
||||
<el-button type="primary" plain size="small">单屏</el-button>
|
||||
<el-button type="primary" plain size="small">四分屏</el-button>
|
||||
<el-button type="primary" plain size="small" style="margin-right: 2vw">全屏</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
const curIndex = ref(0);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.video-list {
|
||||
width: 100%;
|
||||
height: 88%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.video-item {
|
||||
width: calc(50% - 0.2vh * 2);
|
||||
height: calc(50% - 0.2vh * 2);
|
||||
background-color: #1d283477;
|
||||
border: 0.2vh solid #000;
|
||||
}
|
||||
|
||||
.video-item-active {
|
||||
border: 0.2vh solid #0b4f82 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.video-operate {
|
||||
width: 100%;
|
||||
height: 12%;
|
||||
background-color: #054861;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
import { h, render } from "vue";
|
||||
import NotificationProvider from "./notificationProvider.vue";
|
||||
|
||||
let vnode;
|
||||
const useNotification = () => {
|
||||
if (!vnode) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
vnode = h(NotificationProvider, {});
|
||||
render(vnode, container);
|
||||
}
|
||||
|
||||
const { add: notificationAdd, remove: notificationRemove, clear: notificationClear } = (vnode.component && vnode.component.exposed) || {};
|
||||
return {
|
||||
notificationAdd,
|
||||
notificationRemove,
|
||||
notificationClear,
|
||||
};
|
||||
};
|
||||
|
||||
export default useNotification;
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="notification" @mouseenter="clearTimer" @mouseleave="createTimer">
|
||||
<div class="top">
|
||||
<span class="title">
|
||||
<ElIcon>
|
||||
<BellFilled />
|
||||
</ElIcon>
|
||||
<span>{{ title }}</span>
|
||||
</span>
|
||||
<a class="btn" @click="handleClose">{{ btnText }}</a>
|
||||
</div>
|
||||
<span class="content">
|
||||
<div v-if="content.tag" class="tag">{{ content.tag }}</div>
|
||||
<img v-if="content.pic" class="pic" :src="content.pic" />
|
||||
<div class="info">
|
||||
<div class="row">
|
||||
<ElIcon style="color: #1ac1ba">
|
||||
<ShoppingBag />
|
||||
</ElIcon>
|
||||
<span>{{ content.channelName }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ElIcon style="color: #c1951a">
|
||||
<Timer />
|
||||
</ElIcon>
|
||||
<span>{{ content.createTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { ElIcon } from "element-plus";
|
||||
import { ShoppingBag, Timer, BellFilled } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题",
|
||||
},
|
||||
content: {
|
||||
type: Object,
|
||||
default: {
|
||||
tag: "",
|
||||
pic: "",
|
||||
channelName: "",
|
||||
createTime: "",
|
||||
},
|
||||
},
|
||||
btnText: {
|
||||
type: String,
|
||||
default: "关闭",
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 5 * 1000,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["onClosed"]);
|
||||
|
||||
const timer = ref(0);
|
||||
|
||||
// 点击关闭按钮
|
||||
const handleClose = (e) => {
|
||||
emit("onClosed");
|
||||
};
|
||||
|
||||
const createTimer = () => {
|
||||
if (props.duration) {
|
||||
timer.value = setTimeout(() => {
|
||||
emit("onClosed");
|
||||
}, props.duration);
|
||||
}
|
||||
};
|
||||
|
||||
const clearTimer = () => {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value);
|
||||
timer.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
createTimer();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notification {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
background-color: transparent;
|
||||
color: #e0e2e2;
|
||||
min-width: 18vw;
|
||||
box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);
|
||||
transition: all 0.3s;
|
||||
border: 1px solid #0c3245;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.top {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #0d4c71;
|
||||
padding: 4px 0;
|
||||
font-size: 0.8vw;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
color: #e0e2e2;
|
||||
padding-right: 20px;
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
background-color: #132227;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8vw;
|
||||
}
|
||||
|
||||
.pic {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #7f7f7f;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="notification-provider">
|
||||
<transition-group name="fade">
|
||||
<notification v-for="item in notificationList" :key="item.id" :title="item.title" :content="item.content"
|
||||
:btnText="item.btnText" :duration="item.duration" @onClosed="remove(item.id)" />
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import notification from "./notification.vue";
|
||||
import useList from "./useList";
|
||||
|
||||
const { notificationList, add, remove, clear } = useList();
|
||||
|
||||
defineExpose({
|
||||
add,
|
||||
remove,
|
||||
clear,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notification-provider {
|
||||
position: fixed;
|
||||
top: 40px;
|
||||
right: 1vw;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.3s;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
let count = 0;
|
||||
|
||||
const useList = () => {
|
||||
const notificationList = ref([]);
|
||||
|
||||
const add = (optionProps) => {
|
||||
notificationList.value.push({ id: count++, ...optionProps });
|
||||
};
|
||||
|
||||
const remove = (id) => {
|
||||
notificationList.value = notificationList.value.filter((item) => item.id !== id);
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
notificationList.value = [];
|
||||
}
|
||||
|
||||
return { notificationList, add, remove, clear };
|
||||
};
|
||||
|
||||
export default useList;
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="tab">
|
||||
<span class="tab-item" v-for="(item, index) in tabList" :key="index" :class="{ active: activeIndex === index }"
|
||||
@click="dateTypeChange(index)">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<keep-alive>
|
||||
<component :is="tabList[activeIndex].component" :data="componentData" :labels="componentLabels" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { getMonthDays } from "@/utils/date";
|
||||
|
||||
import YesterdayStatistics from "./yesterday-statistics.vue";
|
||||
import WeekStatistics from "./week-statistics.vue";
|
||||
import MonthStatistics from "./month-statistics.vue";
|
||||
import YearStatistics from "./year-statistics.vue";
|
||||
|
||||
const emit = defineEmits(["changeDateType"]);
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Object,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
YESTERDAY: {},
|
||||
TODAY: {},
|
||||
THIS_WEEK: [],
|
||||
THIS_MONTH: [],
|
||||
THIS_YEAR: [],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const tabList = [
|
||||
{ label: "昨日", value: "YESTERDAY", component: YesterdayStatistics },
|
||||
{ label: "今日", value: "TODAY", component: YesterdayStatistics },
|
||||
{ label: "本周", value: "THIS_WEEK", component: WeekStatistics },
|
||||
{ label: "本月", value: "THIS_MONTH", component: MonthStatistics },
|
||||
{ label: "本年", value: "THIS_YEAR", component: YearStatistics },
|
||||
];
|
||||
const activeIndex = ref(0);
|
||||
const activeKey = computed(() => tabList[activeIndex.value].value);
|
||||
const componentData = computed(() => {
|
||||
if (!props.data) {
|
||||
return activeIndex.value == 0 ? {} : [];
|
||||
}
|
||||
return props.data[activeKey.value];
|
||||
});
|
||||
const componentLabels = computed(() => {
|
||||
if (!props.labels) return [];
|
||||
return props.labels[activeKey.value] ?? [];
|
||||
});
|
||||
|
||||
const dateTypeChange = (index) => {
|
||||
activeIndex.value = index;
|
||||
emit("changeDateType", tabList[index].value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tab {
|
||||
width: 70%;
|
||||
height: 14%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
.tab-item {
|
||||
padding: 1.2vh 1vw;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 0.8vw;
|
||||
white-space: nowrap;
|
||||
.background("@/assets/images/date-dimension-tab.png", @size: 100% 100%, @position: center);
|
||||
color: @text-color-primary;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background-image: url("@/assets/images/date-dimension-tab-select.png");
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 86%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="legend">
|
||||
<span class="legend-item" :style="{ backgroundColor: getBarColorByTypeStr('hcCount') }"></span>
|
||||
<span>荤菜</span>
|
||||
<span class="legend-item" :style="{ backgroundColor: getBarColorByTypeStr('scCount') }"></span>
|
||||
<span>素菜</span>
|
||||
<span class="legend-item" :style="{ backgroundColor: getBarColorByTypeStr('zsCount') }"></span>
|
||||
<span>主食</span>
|
||||
</div>
|
||||
<div class="days">
|
||||
<div class="item" v-for="(item, index) in displayData" :key="index" @click="doCar(item)">
|
||||
<div class="day">{{ item.day }}</div>
|
||||
<div class="bar">
|
||||
<div class="progress-container">
|
||||
<div v-for="(typeItem, typeIndex) in typeSettings" :key="typeIndex" class="progress-segment"
|
||||
:style="{
|
||||
backgroundColor: getBarColorByTypeStr(typeItem.typeStr),
|
||||
width: getWidth(typeItem, item),
|
||||
}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tooltip">
|
||||
<div class="tooltip-content">
|
||||
<span class="label">荤菜:</span>
|
||||
<span class="value">{{ item.hcCount ?? 0 }}份</span>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<span class="label">素菜:</span>
|
||||
<span class="value">{{ item.scCount ?? 0 }}份</span>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<span class="label">主食:</span>
|
||||
<span class="value">{{ item.zsCount ?? 0 }}份</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getMonthDays } from "@/utils/date";
|
||||
import { computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => getMonthDays().map((day) => ({ day, type: -1 })),
|
||||
},
|
||||
});
|
||||
|
||||
const typeSettings = [
|
||||
{
|
||||
type: "荤菜",
|
||||
typeStr: "hcCount",
|
||||
color: "#287CE7",
|
||||
},
|
||||
{
|
||||
type: "素菜",
|
||||
typeStr: "scCount",
|
||||
color: "#F4CF3D",
|
||||
},
|
||||
{
|
||||
type: "主食",
|
||||
typeStr: "zsCount",
|
||||
color: "#30C6DC",
|
||||
},
|
||||
]
|
||||
|
||||
const displayData = computed(() => {
|
||||
const days = getMonthDays();
|
||||
const data = props.data;
|
||||
if (!Array.isArray(props.data)) return days.map((day) => ({ day, type: -1 }));
|
||||
if (props.data.length !== days.length) {
|
||||
for (let i = data.length; i < days.length; i++) {
|
||||
data.push({
|
||||
day: days[i],
|
||||
type: -1,
|
||||
});
|
||||
}
|
||||
return data.slice(0, days.length);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
const getBarColorByTypeStr = (type) => {
|
||||
const target = typeSettings.find((v) => v.typeStr === type)
|
||||
if (!target) return
|
||||
return target.color
|
||||
}
|
||||
|
||||
const getWidth = (type, item) => {
|
||||
const { typeStr } = type;
|
||||
const total = item.hcCount + item.scCount + item.zsCount;
|
||||
return `${(item[typeStr] / total * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
const doCar = (item) => {
|
||||
if (!item.day) return;
|
||||
if (item.type == "-1") {
|
||||
ElMessage.warning(`还未到${item.day}号,无法跳转`);
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
// 抓拍时间
|
||||
swingTimeList: [
|
||||
dayjs().date(item.day).startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
dayjs().date(item.day).endOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
],
|
||||
};
|
||||
console.log(`|点击跳转|-|打卡统计|-|月|params:|{${JSON.stringify(params)}}|`);
|
||||
jumpAdminPath("door_reign_record", params);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.legend {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
font-size: 0.6vw;
|
||||
gap: 0.4vw;
|
||||
margin: 1vh 0;
|
||||
|
||||
.legend-item {
|
||||
width: 1vw;
|
||||
height: 0.5vw;
|
||||
}
|
||||
}
|
||||
|
||||
.days {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 0.2vw;
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
background-color: #122c40;
|
||||
border: 1px solid #1b3346;
|
||||
cursor: pointer;
|
||||
|
||||
.day {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -60%);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #cfe8f6;
|
||||
font-size: 0.8vw;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 0.3vh;
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.progress-segment {
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover 弹框样式 */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #0f1f2e;
|
||||
border: 1px solid #1b3346;
|
||||
color: #cfe8f6;
|
||||
font-size: 0.7vw;
|
||||
padding: 0.4vw 0.6vw;
|
||||
border-radius: 0.3vw;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.18s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&:hover .tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: #0f1f2e transparent transparent transparent;
|
||||
filter: drop-shadow(0 -1px 0 rgba(27, 51, 70, 0.9));
|
||||
display: grid;
|
||||
gap: 0.2vw;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.2vw;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tooltip-row .label {
|
||||
color: #7fb5d6;
|
||||
}
|
||||
|
||||
.tooltip-row .value {
|
||||
color: #cfe8f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Object,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => [
|
||||
{ text: "荤菜", name: "hc", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "素菜", name: "sc", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "主食", name: "zs", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
watch(
|
||||
() => [props.data, props.labels],
|
||||
(val) => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({
|
||||
xAxis: {
|
||||
data: props.labels?.map((v) => v.split("|")[1]) ?? [],
|
||||
},
|
||||
series: props.data.map((item, i) => {
|
||||
return {
|
||||
type: "bar",
|
||||
stack: 'total',
|
||||
name: item.text,
|
||||
showBackground: false,
|
||||
barGap: "50%",
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
borderColor: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][0] },
|
||||
{ offset: 0.7, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][0] },
|
||||
{ offset: 1, color: linearGradientColors[i][1] },
|
||||
]),
|
||||
},
|
||||
},
|
||||
data: item.values,
|
||||
};
|
||||
}),
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const colors = ["#287CE7", "#F4CF3D", "#30C6DC"];
|
||||
const linearGradientColors = [
|
||||
["#01A5FA", "#086BA5", "#0E385A"],
|
||||
["#E0B60D", "#78752F", "#143750"],
|
||||
["#15D1C4", "#117883", "#0F3654"],
|
||||
];
|
||||
|
||||
const option = {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "14%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "4%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
valueFormatter: (value) => `${value}份`,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#21303C",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#00ffff",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
icon: "rect",
|
||||
right: 0,
|
||||
top: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 8,
|
||||
itemGap: 16,
|
||||
textStyle: { color: "#ffffff", fontSize: 9 },
|
||||
itemStyle: { borderWidth: 0, borderColor: "transparent" },
|
||||
data: props.data.map((item, i) => {
|
||||
return { name: item.text, itemStyle: { color: colors[i], borderWidth: 0, borderColor: "transparent" } };
|
||||
}),
|
||||
},
|
||||
series: props.data.map((item, i) => {
|
||||
return {
|
||||
type: "bar",
|
||||
stack: 'total',
|
||||
name: item.text,
|
||||
showBackground: false,
|
||||
barGap: "50%",
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
borderColor: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][0] },
|
||||
{ offset: 0.7, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][0] },
|
||||
{ offset: 1, color: linearGradientColors[i][1] },
|
||||
]),
|
||||
},
|
||||
},
|
||||
data: item.values,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!chartRef.value) return;
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(option);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
chartInstance.on("click", function (params) {
|
||||
const target = props.labels[params.dataIndex];
|
||||
if (!target) return;
|
||||
const [timeStr, _] = target.split("|");
|
||||
const _params = {
|
||||
swingTimeList: [
|
||||
dayjs(timeStr).startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
dayjs(timeStr).endOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
],
|
||||
};
|
||||
console.log(`|点击跳转|-|打卡统计|-|本周|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("door_reign_record", _params);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Object,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => [
|
||||
{ text: "荤菜", name: "hc", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "素菜", name: "sc", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ text: "主食", name: "zs", values: [0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
watch(
|
||||
() => [props.data, props.labels],
|
||||
(val) => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({
|
||||
xAxis: {
|
||||
data: props.labels?.map((v) => v) ?? [],
|
||||
},
|
||||
series: props.data.map((item, i) => {
|
||||
return {
|
||||
type: "bar",
|
||||
stack: 'total',
|
||||
name: item.text,
|
||||
showBackground: false,
|
||||
barGap: "50%",
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
borderColor: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][0] },
|
||||
{ offset: 0.7, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][0] },
|
||||
{ offset: 1, color: linearGradientColors[i][1] },
|
||||
]),
|
||||
},
|
||||
},
|
||||
data: item.values,
|
||||
};
|
||||
}),
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const colors = ["#287CE7", "#F4CF3D", "#30C6DC"];
|
||||
const linearGradientColors = [
|
||||
["#01A5FA", "#086BA5", "#0E385A"],
|
||||
["#E0B60D", "#78752F", "#143750"],
|
||||
["#15D1C4", "#117883", "#0F3654"],
|
||||
];
|
||||
|
||||
const option = {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "14%",
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
bottom: "4%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
valueFormatter: (value) => `${value}份`,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#21303C",
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#00ffff",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
icon: "rect",
|
||||
right: 0,
|
||||
top: 0,
|
||||
itemWidth: 10,
|
||||
itemHeight: 8,
|
||||
itemGap: 16,
|
||||
textStyle: { color: "#ffffff", fontSize: 9 },
|
||||
itemStyle: { borderWidth: 0, borderColor: "transparent" },
|
||||
data: props.data.map((item, i) => {
|
||||
return { name: item.text, itemStyle: { color: colors[i], borderWidth: 0, borderColor: "transparent" } };
|
||||
}),
|
||||
},
|
||||
series: props.data.map((item, i) => {
|
||||
return {
|
||||
type: "bar",
|
||||
stack: 'total',
|
||||
name: item.text,
|
||||
showBackground: false,
|
||||
barGap: "50%",
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
borderColor: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][0] },
|
||||
{ offset: 0.7, color: linearGradientColors[i][1] },
|
||||
{ offset: 1, color: linearGradientColors[i][0] },
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: linearGradientColors[i][2] },
|
||||
{ offset: 0.5, color: linearGradientColors[i][0] },
|
||||
{ offset: 1, color: linearGradientColors[i][1] },
|
||||
]),
|
||||
},
|
||||
},
|
||||
data: item.values,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!chartRef.value) return;
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(option);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
chartInstance.on("click", function (params) {
|
||||
const target = props.labels[params.dataIndex];
|
||||
if (!target) return;
|
||||
const [timeStr, _] = target.split("|");
|
||||
const _params = {
|
||||
swingTimeList: [
|
||||
dayjs(timeStr).startOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
dayjs(timeStr).endOf("day").format("YYYY-MM-DD HH:mm:ss"),
|
||||
],
|
||||
};
|
||||
console.log(`|点击跳转|-|打卡统计|-|本周|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("door_reign_record", _params);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
<div class="area-info">
|
||||
<div class="area-info-item" v-for="(item, index) in props.data" :key="item.name" @click="doItem(item)">
|
||||
<span
|
||||
:style="{ width: '1vw', height: '1vw', background: colors[index], borderRadius: '50%', display: 'inline-block' }"></span>
|
||||
<span class="name">{{ item.title }}</span>
|
||||
<span :style="{ marginLeft: '0.2vw', color: colors[index] }">
|
||||
{{ item.total }}份
|
||||
</span>
|
||||
<span :style="{ marginLeft: '0.1vw', color: colors[index], fontSize: '0.6vw' }">
|
||||
({{ percent(item.total) }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
const colors = ["#31D3A4", "#1AD0F9", "#5470C6"];
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
const total = computed(() => {
|
||||
if (!props.data || !props.data.length) return 0;
|
||||
return props.data.reduce((sum, i) => sum + (Number(i.total) || 0), 0);
|
||||
});
|
||||
const seriesData = computed(() => {
|
||||
if (!props.data || !props.data.length) return [];
|
||||
return props.data.map((i, idx) => ({ name: i.title, value: i.total, itemStyle: { color: colors[idx] } }));
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.data],
|
||||
() => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({
|
||||
series: [{ data: seriesData.value }],
|
||||
graphic: [{ type: "text", left: "40%", top: "40%", style: { text: String(total.value) } }],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const percent = (v) => {
|
||||
if (!total.value) return "0%";
|
||||
return ((Number(v) / total.value) * 100).toFixed(1) + "%";
|
||||
};
|
||||
|
||||
const options = {
|
||||
backgroundColor: "transparent",
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
// formatter: (p) => `${p.name}:${p.value} (${p.percent}%)`,
|
||||
confine: true,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "pie",
|
||||
radius: ["60%", "78%"],
|
||||
center: ["50%", "50%"],
|
||||
avoidLabelOverlap: true,
|
||||
label: { show: false },
|
||||
labelLine: { show: false },
|
||||
itemStyle: { borderWidth: 0 },
|
||||
data: seriesData.value,
|
||||
},
|
||||
],
|
||||
graphic: [
|
||||
{
|
||||
type: "text",
|
||||
left: "40%",
|
||||
top: "40%",
|
||||
style: {
|
||||
text: String(total.value),
|
||||
fill: "#fff",
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
textAlign: "center",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
left: "39%",
|
||||
top: "55%",
|
||||
style: {
|
||||
text: "购买数量",
|
||||
fill: "#B8C7D9",
|
||||
fontSize: 12,
|
||||
textAlign: "center",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(options);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.area-info {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.area-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.4vw 0;
|
||||
font-size: 0.7vw;
|
||||
border-bottom: 1px dashed #eeeeee99;
|
||||
cursor: pointer;
|
||||
|
||||
.name {
|
||||
width: 5vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
margin-left: 0.2vw;
|
||||
color: #b8c7d9;
|
||||
font-size: 0.7vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-table :data="state.dataList" :size="TABLE.SIZE" :style="TABLE.STYLE" :cell-style="TABLE.CELL_STYLE"
|
||||
:row-style="TABLE.ROW_STYLE" :header-cell-style="TABLE.HEADER_CELL_STYLE" border
|
||||
:summary-method="getSummaries" show-summary>
|
||||
<el-table-column align="center" type="index" label="#" width="50" />
|
||||
<el-table-column align="center" label="商户" prop="merchantName" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.merchantName || "未知" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流水金额" prop="totalPayPrice">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalPayPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流水次数" prop="totalPayCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">
|
||||
{{ row.totalPayCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="退款金额" prop="totalRefundPrice">
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ displayPrice(row.totalRefundPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="退款次数" prop="totalRefundCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ row.totalRefundCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="消费净金额" prop="totalProfitPrice" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalProfitPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="消费净次数" prop="totalProfitCount" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ row.totalProfitCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, reactive } from "vue";
|
||||
|
||||
import { canteenApi } from "@/api/index"
|
||||
import { displayPrice } from "@/utils/format-money"
|
||||
import { TABLE } from "@/constant/table-constant"
|
||||
|
||||
const state = reactive({
|
||||
dataList: [],
|
||||
queryParams: {
|
||||
createTimeList: []
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
|
||||
// 食堂报表
|
||||
const getDataList = async () => {
|
||||
try {
|
||||
const { code, data } = await canteenApi.canteenReport({ ...state.queryParams })
|
||||
console.log("食堂报表:", data);
|
||||
state.dataList = data
|
||||
} catch (error) {
|
||||
console.error("|食堂报表|意外的错误:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async (createTimeList) => {
|
||||
state.queryParams.createTimeList = createTimeList ?? null
|
||||
await getDataList()
|
||||
}
|
||||
|
||||
const getSummaries = (param) => {
|
||||
const { columns, data } = param
|
||||
const totalColumns = ["totalPayPrice", "totalPayCount", "totalRefundPrice", "totalRefundCount", "totalProfitPrice", "totalProfitCount"]
|
||||
const totalRow = Array.from({ length: columns.length })
|
||||
totalRow[0] = "合计"
|
||||
columns.forEach((i, index) => {
|
||||
if (totalColumns.includes(i.property)) {
|
||||
const total = data
|
||||
.filter((v) => v[i.property] != null)
|
||||
.map((v) => v[i.property])
|
||||
.reduce((pren, next) => pren + next, 0)
|
||||
if (i.property.includes("Price")) {
|
||||
totalRow[index] = displayPrice(total)
|
||||
} else {
|
||||
totalRow[index] = total
|
||||
}
|
||||
}
|
||||
})
|
||||
return totalRow
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
tr {
|
||||
background: #1d283477 !important;
|
||||
}
|
||||
|
||||
.el-table__footer-wrapper {
|
||||
.el-table__cell {
|
||||
background: #1d283477 !important;
|
||||
border-color: #121F33 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="segmented">
|
||||
<el-segmented v-model="activeSegment" :options="props.segments" :props="{ label: 'label', value: 'value' }"
|
||||
size="small" @change="dateTypeChange" />
|
||||
</div>
|
||||
<div class="list scrollbar-hide">
|
||||
<div v-for="(row, idx) in curList" :key="idx" class="item" @click="doCar(row)">
|
||||
<div class="type" :class="props.type == 'canteen' ? 'type-1' : 'type-2'">
|
||||
<span class="type-text">{{ idx + 1 }}</span>
|
||||
</div>
|
||||
<div class="end">
|
||||
<span>{{ row.title || "未知" }}</span>
|
||||
<span>{{ row.total }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const emit = defineEmits(["changeDateType"]);
|
||||
|
||||
const props = defineProps({
|
||||
// "canteen"
|
||||
type: {
|
||||
type: String,
|
||||
default: () => "canteen",
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => [{ alarmList: [], name: null, time: null }],
|
||||
},
|
||||
segments: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{
|
||||
label: "昨日",
|
||||
value: "YESTERDAY",
|
||||
},
|
||||
{
|
||||
label: "今日",
|
||||
value: "TODAY",
|
||||
},
|
||||
{
|
||||
label: "本月",
|
||||
value: "THIS_MONTH",
|
||||
},
|
||||
{
|
||||
label: "近半年",
|
||||
value: "HALF_YEAR",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const activeSegment = ref(props.segments[0].value || "YESTERDAY");
|
||||
|
||||
const curList = computed(() => {
|
||||
if (!props.data || !props.data.length) return [];
|
||||
const list = props.data;
|
||||
return Array.isArray(list) && list.length ? list : [];
|
||||
});
|
||||
|
||||
const dateTypeChange = (e) => {
|
||||
emit("changeDateType", e);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 4vh;
|
||||
|
||||
.segmented {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
|
||||
.el-segmented {
|
||||
--el-segmented-item-selected-color: var(--el-text-color-primary);
|
||||
--el-segmented-item-selected-bg-color: #095d82;
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2vh;
|
||||
padding-bottom: 4vh;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
padding: 0.8vh 0.5vw;
|
||||
cursor: pointer;
|
||||
background-image: url("@/assets/images/car-alarm-list-item-bg.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
|
||||
.type {
|
||||
width: 7vw;
|
||||
height: 3.6vh;
|
||||
line-height: 3.6vh;
|
||||
text-align: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
|
||||
.type-text {
|
||||
display: inline-block;
|
||||
font-size: 0.65vw;
|
||||
color: #c9e9ff;
|
||||
|
||||
width: 80%;
|
||||
margin: 0 0.5vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.type-1 {
|
||||
background-image: url("@/assets/images/car-alarm-type-blue.png");
|
||||
}
|
||||
|
||||
.type-2 {
|
||||
background-image: url("@/assets/images/car-alarm-type-green.png");
|
||||
}
|
||||
|
||||
.end {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 5vw;
|
||||
font-size: 0.6vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
span:nth-child(1) {
|
||||
text-align: center;
|
||||
color: #d6e3ee;
|
||||
|
||||
flex: 1;
|
||||
margin-right: 1vw;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
color: #d6e3ee;
|
||||
width: 6.5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏滚动条 */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
// 图表类型 "canteen" | "nutrition" | "shop"
|
||||
type: {
|
||||
type: String,
|
||||
default: "canteen",
|
||||
},
|
||||
yTitle: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
onMounted(async () => {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(option);
|
||||
window.addEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
window.removeEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
if (!chartInstance) return;
|
||||
// console.log(props.type, "type");
|
||||
// console.log(props.data, "data");
|
||||
if (props.type == "nutrition") {
|
||||
const { hc, sc, zs } = props.data;
|
||||
chartInstance.setOption({
|
||||
yAxis: {
|
||||
name: props.yTitle,
|
||||
},
|
||||
xAxis: {
|
||||
data: hc?.list?.map((v) => {
|
||||
return v.time;
|
||||
}),
|
||||
},
|
||||
legend: {
|
||||
data: [{ name: hc.typeStr }, { name: sc.typeStr }, { name: zs.typeStr }],
|
||||
},
|
||||
series: [
|
||||
{ name: hc.typeStr, data: hc?.list?.map((v) => v.personCount) },
|
||||
{ name: sc.typeStr, data: sc?.list?.map((v) => v.personCount) },
|
||||
{ name: zs.typeStr, data: zs?.list?.map((v) => v.personCount) },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (props.type == "canteen") {
|
||||
const { morning, noon, evening } = props.data;
|
||||
chartInstance.setOption({
|
||||
yAxis: {
|
||||
name: props.yTitle,
|
||||
},
|
||||
xAxis: {
|
||||
data: morning?.list?.map((v) => {
|
||||
return v.time;
|
||||
}),
|
||||
},
|
||||
legend: {
|
||||
data: [{ name: morning.typeStr }, { name: noon.typeStr }, { name: evening.typeStr }],
|
||||
},
|
||||
series: [
|
||||
{ name: morning.typeStr, data: morning?.list?.map((v) => v.personCount) },
|
||||
{ name: noon.typeStr, data: noon?.list?.map((v) => v.personCount) },
|
||||
{ name: evening.typeStr, data: evening?.list?.map((v) => v.personCount) },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (props.type == "shop") {
|
||||
chartInstance.setOption({
|
||||
yAxis: {
|
||||
name: props.yTitle,
|
||||
},
|
||||
xAxis: {
|
||||
data: props.data[0]?.list?.map((v) => {
|
||||
return v.time;
|
||||
}),
|
||||
},
|
||||
series: props.data?.map((v) => {
|
||||
return {
|
||||
name: v.typeStr,
|
||||
data: v?.list?.map((v) => v.buyProductCount ?? 0),
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const option = {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "18%",
|
||||
left: "0%",
|
||||
right: "4%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
// formatter: (params) => {
|
||||
// return `${params[0].axisValue}<br/>${params[0].marker}${params[0].value}辆<br/>
|
||||
// ${params[1].axisValue}<br/>${params[1].marker}${params[1].value}辆<br/>
|
||||
// ${params[2].axisValue}<br/>${params[2].marker}${params[2].value}辆<br/>`;
|
||||
// },
|
||||
valueFormatter: (value) => value,
|
||||
},
|
||||
legend: {
|
||||
icon: "rect",
|
||||
right: 0,
|
||||
top: 0,
|
||||
itemWidth: 14,
|
||||
itemHeight: 8,
|
||||
itemGap: 16,
|
||||
textStyle: { color: "#ffffff", fontSize: 12 },
|
||||
itemStyle: { borderWidth: 0, borderColor: "transparent" },
|
||||
data: [
|
||||
{ itemStyle: { color: "#1DB996", borderWidth: 0, borderColor: "transparent" } },
|
||||
{ itemStyle: { color: "#01C6EF", borderWidth: 0, borderColor: "transparent" } },
|
||||
{ itemStyle: { color: "#F7CC08", borderWidth: 0, borderColor: "transparent" } },
|
||||
],
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#21303C",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
// backgroundColor: "#2d5a3d",
|
||||
// padding: [4, 4, 4, 4],
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "",
|
||||
min: 0,
|
||||
nameTextStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
padding: [0, 0, 0, 40]
|
||||
// align: "right",
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: "#00ffff",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "",
|
||||
type: "line",
|
||||
// stack: "Total",
|
||||
data: [],
|
||||
smooth: true,
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
yAxisIndex: 0,
|
||||
lineStyle: {
|
||||
color: "#1DB996",
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#1DB996",
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
formatter: "{c}",
|
||||
color: "#1DB996",
|
||||
fontWeight: "bold",
|
||||
fontSize: 12,
|
||||
},
|
||||
areaStyle: {
|
||||
color: "#1DB996",
|
||||
opacity: 0.8,
|
||||
color: {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
// { offset: 0, color: "rgba(255, 107, 107, 0.3)" },
|
||||
{ offset: 0, color: "#1C7C67" },
|
||||
{ offset: 1, color: "#0D1E25" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "line",
|
||||
// stack: "Total",
|
||||
data: [],
|
||||
smooth: true,
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
yAxisIndex: 0,
|
||||
lineStyle: {
|
||||
color: "#01C6EF",
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#01C6EF",
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
formatter: "{c}",
|
||||
color: "#01C6EF",
|
||||
fontWeight: "bold",
|
||||
fontSize: 12,
|
||||
},
|
||||
areaStyle: {
|
||||
color: "#01C6EF",
|
||||
opacity: 0.8,
|
||||
color: {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
// { offset: 0, color: "rgba(255, 107, 107, 0.3)" },
|
||||
{ offset: 0, color: "#097186" },
|
||||
{ offset: 1, color: "#0D1E25" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "line",
|
||||
// stack: "Total",
|
||||
data: [],
|
||||
smooth: true,
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
yAxisIndex: 0,
|
||||
lineStyle: {
|
||||
color: "#F7CC08",
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#F7CC08",
|
||||
borderColor: "#fff",
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
formatter: "{c}",
|
||||
color: "#F7CC08",
|
||||
fontWeight: "bold",
|
||||
fontSize: 12,
|
||||
},
|
||||
areaStyle: {
|
||||
color: "#F7CC08",
|
||||
opacity: 0.8,
|
||||
color: {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
// { offset: 0, color: "rgba(255, 107, 107, 0.3)" },
|
||||
{ offset: 0, color: "#6B6119" },
|
||||
{ offset: 1, color: "#0D1E25" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, watch, useTemplateRef } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
labels: [],
|
||||
values: [],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = useTemplateRef("chartRef");
|
||||
let chartInstance = null;
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(options);
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
chartInstance.on("click", (params) => {
|
||||
if (!params.name) return;
|
||||
const _params = {
|
||||
swingTimeList: [dayjs().startOf("day").format("YYYY-MM-DD HH:mm:ss"), dayjs().endOf("day").format("YYYY-MM-DD HH:mm:ss")],
|
||||
personName: params.name,
|
||||
isMainFloor: "1",
|
||||
};
|
||||
console.log(`|点击跳转|-|今日出入机关楼次数排行|-|${params.name}|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("door_record", _params);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.setOption({
|
||||
yAxis: { data: props.data?.labels ?? [] },
|
||||
series: [{ data: props.data?.values ?? [] }],
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const options = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
axisPointer: { type: "shadow" },
|
||||
// formatter: (params) => {
|
||||
// return `${params[0].value}次`;
|
||||
// },
|
||||
confine: true,
|
||||
},
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
top: "0%",
|
||||
left: "0%",
|
||||
right: "4%",
|
||||
bottom: "0%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "value",
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: "rgba(255,255,255,0.6)" },
|
||||
splitLine: { show: true, lineStyle: { color: "rgba(255,255,255,0.08)" } },
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: "#fff" },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "bar",
|
||||
data: [],
|
||||
barWidth: 14,
|
||||
showBackground: true,
|
||||
backgroundStyle: { color: "rgba(255,255,255,0.06)" },
|
||||
itemStyle: {
|
||||
borderRadius: [0, 10, 10, 0],
|
||||
color: (params) => {
|
||||
const palette = [
|
||||
["#1F4F68", "#2FC6DC"],
|
||||
["#1F4F68", "#2FC6DC"],
|
||||
["#F47E3D", "#F4C93D"],
|
||||
["#1F4F68", "#2FC6DC"],
|
||||
["#1F4F68", "#2FC6DC"],
|
||||
];
|
||||
const [c1, c2] = palette[params.dataIndex % palette.length];
|
||||
return new echarts.graphic.LinearGradient(1, 0, 0, 0, [
|
||||
{ offset: 0, color: c2 },
|
||||
{ offset: 1, color: c1 },
|
||||
]);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (chartInstance) chartInstance.resize();
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div ref="chartRef" style="width: 100%; height: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = ref(null);
|
||||
let chartInstance = null;
|
||||
|
||||
watch(
|
||||
() => [props.data, props.labels],
|
||||
() => {
|
||||
if (!chartInstance) return;
|
||||
const dataLength = props.data.length
|
||||
chartInstance.setOption({
|
||||
xAxis: {
|
||||
data: props.labels || [],
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data:
|
||||
props.data.map((item) => {
|
||||
if (item >= 50)
|
||||
return {
|
||||
value: item,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#0D3149" },
|
||||
{ offset: 0.5, color: "#039A5D" },
|
||||
{ offset: 1, color: "#00EF7E" },
|
||||
]),
|
||||
},
|
||||
};
|
||||
return item;
|
||||
}) || [],
|
||||
},
|
||||
{
|
||||
data: new Array(dataLength).fill(0)
|
||||
},
|
||||
{
|
||||
data: new Array(dataLength).fill(0)
|
||||
},
|
||||
{
|
||||
data: new Array(dataLength).fill(0)
|
||||
}
|
||||
],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const option = {
|
||||
backgroundColor: "transparent",
|
||||
grid: {
|
||||
left: "0%",
|
||||
right: "0%",
|
||||
top: "14%",
|
||||
bottom: "8%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: [],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#313A42",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "rgba(255,255,255,0.6)",
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "就餐人数",
|
||||
nameTextStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 10,
|
||||
// align: "right",
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#313A42",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: "rgba(255,255,255,0.6)",
|
||||
fontSize: 12,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "#313A42",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "bar",
|
||||
data: [],
|
||||
barWidth: 12,
|
||||
showBackground: true,
|
||||
backgroundStyle: { color: "rgba(255,255,255,0.06)" },
|
||||
itemStyle: {
|
||||
borderRadius: [0, 0, 6, 6],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#0D3149" },
|
||||
{ offset: 0.5, color: "#0D84AC" },
|
||||
{ offset: 1, color: "#01F6C4" },
|
||||
]),
|
||||
},
|
||||
z: 3,
|
||||
},
|
||||
{
|
||||
z: 4,
|
||||
type: "pictorialBar",
|
||||
symbol: "circle",
|
||||
symbolOffset: ["0%", "0%"],
|
||||
symbolSize: [36, 6],
|
||||
toolltip: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#0A2531" },
|
||||
{ offset: 1, color: "#106DA7" },
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
z: 5,
|
||||
type: "pictorialBar",
|
||||
symbol: "circle",
|
||||
symbolOffset: ["0%", "-30%"],
|
||||
symbolSize: [26, 10],
|
||||
toolltip: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#0A2531" },
|
||||
{ offset: 1, color: "#106DA7" },
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
z: 6,
|
||||
type: "pictorialBar",
|
||||
symbol: "circle",
|
||||
symbolPosition: "end",
|
||||
symbolOffset: ["0%", "-240%"],
|
||||
symbolSize: [12, 6],
|
||||
toolltip: {
|
||||
show: false,
|
||||
},
|
||||
data: [],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#07BFB8" },
|
||||
{ offset: 1, color: "#01F6C4" },
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: { type: "shadow" },
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
borderColor: "#000000",
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
fontSize: 12,
|
||||
},
|
||||
padding: 10,
|
||||
formatter: (params) => {
|
||||
const p = Array.isArray(params) ? params[0] : params;
|
||||
return `${p.name}:${p.value}人`;
|
||||
},
|
||||
confine: true,
|
||||
},
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!chartRef.value) return;
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(option);
|
||||
window.addEventListener("resize", handleResize);
|
||||
chartInstance.on("click", (params) => {
|
||||
const hourList = params.name.replace("点", "").split("~");
|
||||
if (!hourList || hourList.length !== 2) return;
|
||||
const startHour = hourList[0];
|
||||
const endHour = hourList[1];
|
||||
const start = dayjs().hour(startHour).minute(0).second(0).format("YYYY-MM-DD HH:mm:ss");
|
||||
const end = dayjs().hour(endHour).minute(0).second(0).format("YYYY-MM-DD HH:mm:ss");
|
||||
const _params = {
|
||||
swingTimeList: [start, end],
|
||||
// 进门
|
||||
alarmTypeList: ["11"],
|
||||
personIdIsNotNull: true,
|
||||
};
|
||||
console.log(`|点击跳转|-|今日人员打卡统计|-|${params.name}|params:|{${JSON.stringify(_params)}}|`);
|
||||
jumpAdminPath("door_reign_record", _params);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
window.removeEventListener("resize", handleResize);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-table :data="state.dataList" :size="TABLE.SIZE" :style="TABLE.STYLE" :cell-style="TABLE.CELL_STYLE"
|
||||
:row-style="TABLE.ROW_STYLE" :header-cell-style="TABLE.HEADER_CELL_STYLE" border
|
||||
:summary-method="getSummaries" show-summary>
|
||||
<el-table-column align="center" type="index" label="#" width="50" />
|
||||
<el-table-column align="center" label="商户" prop="merchantName" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.merchantName || "未知" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流水金额" prop="totalPayPrice">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalPayPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流水次数" prop="totalPayCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-primary">
|
||||
{{ row.totalPayCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="退款金额" prop="totalRefundPrice">
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ displayPrice(row.totalRefundPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="退款次数" prop="totalRefundCount">
|
||||
<template #default="{ row }">
|
||||
<span class="color-danger">
|
||||
{{ row.totalRefundCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="消费净金额" prop="totalProfitPrice" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ displayPrice(row.totalProfitPrice) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="消费净次数" prop="totalProfitCount" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="color-success">
|
||||
{{ row.totalProfitCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, useTemplateRef, reactive } from "vue";
|
||||
|
||||
import { shopApi } from "@/api/index"
|
||||
import { displayPrice } from "@/utils/format-money"
|
||||
import { TABLE } from "@/constant/table-constant"
|
||||
|
||||
const state = reactive({
|
||||
dataList: [],
|
||||
queryParams: {
|
||||
createTimeList: []
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
|
||||
// 食堂报表
|
||||
const getDataList = async () => {
|
||||
try {
|
||||
const { code, data } = await shopApi.canteenReport({ ...state.queryParams })
|
||||
console.log("食堂报表:", data);
|
||||
state.dataList = data
|
||||
} catch (error) {
|
||||
console.error("|食堂报表|意外的错误:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async (createTimeList) => {
|
||||
state.queryParams.createTimeList = createTimeList ?? null
|
||||
await getDataList()
|
||||
}
|
||||
|
||||
const getSummaries = (param) => {
|
||||
const { columns, data } = param
|
||||
const totalColumns = ["totalPayPrice", "totalPayCount", "totalRefundPrice", "totalRefundCount", "totalProfitPrice", "totalProfitCount"]
|
||||
const totalRow = Array.from({ length: columns.length })
|
||||
totalRow[0] = "合计"
|
||||
columns.forEach((i, index) => {
|
||||
if (totalColumns.includes(i.property)) {
|
||||
const total = data
|
||||
.filter((v) => v[i.property] != null)
|
||||
.map((v) => v[i.property])
|
||||
.reduce((pren, next) => pren + next, 0)
|
||||
if (i.property.includes("Price")) {
|
||||
totalRow[index] = displayPrice(total)
|
||||
} else {
|
||||
totalRow[index] = total
|
||||
}
|
||||
}
|
||||
})
|
||||
return totalRow
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
tr {
|
||||
background: #1d283477 !important;
|
||||
}
|
||||
|
||||
.el-table__footer-wrapper {
|
||||
.el-table__cell {
|
||||
background: #1d283477 !important;
|
||||
border-color: #121F33 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
export const TABLE = {
|
||||
SIZE: "small",
|
||||
STYLE: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
CELL_STYLE: {
|
||||
background: "#1d283477",
|
||||
borderColor: "#121F33",
|
||||
},
|
||||
ROW_STYLE: {
|
||||
background: "#1d283477",
|
||||
borderColor: "#121F33",
|
||||
},
|
||||
HEADER_CELL_STYLE: {
|
||||
background: "#1d283477",
|
||||
borderColor: "#121F33",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@background-color: #222222;
|
||||
@text-color: #ffffff;
|
||||
@text-color-primary: #419ce3;
|
||||
|
||||
:root {
|
||||
::selection {
|
||||
background: #45b2fe;
|
||||
}
|
||||
::-moz-selection {
|
||||
background: #45b2fe;
|
||||
}
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
background-color: @background-color;
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
.background(@url: "", @size: auto auto, @position: 0% 0%, @repeat: no-repeat) {
|
||||
background-image: url(@url);
|
||||
background-size: @size;
|
||||
background-position: @position;
|
||||
background-repeat: @repeat;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createApp } from "vue";
|
||||
import ElementPlus from "element-plus";
|
||||
import "element-plus/dist/index.css";
|
||||
import "element-plus/theme-chalk/dark/css-vars.css";
|
||||
import "./global.less";
|
||||
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(ElementPlus, { locale: zhCn });
|
||||
app.use(router);
|
||||
app.mount("#app");
|
||||
@@ -0,0 +1,73 @@
|
||||
import { createWebHashHistory } from "vue-router";
|
||||
import { createRouter } from "vue-router";
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import { getUserInfo, checkToken } from "@/utils/auth"
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
redirect: "/canteen",
|
||||
component: () => import("@/app-panel.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "/canteen",
|
||||
name: "canteen",
|
||||
component: () => import("@/views/canteen.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shop",
|
||||
name: "shop",
|
||||
component: () => import("@/views/shop.vue"),
|
||||
},
|
||||
{
|
||||
path: "/service",
|
||||
name: "service",
|
||||
component: () => import("@/views/service.vue"),
|
||||
},
|
||||
{
|
||||
path: "/through",
|
||||
name: "through",
|
||||
component: () => import("@/views/through.vue"),
|
||||
},
|
||||
{
|
||||
path: "/nutrition",
|
||||
name: "nutrition",
|
||||
component: () => import("@/views/nutrition.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/no-permission",
|
||||
name: "noPermission",
|
||||
component: () => import("@/views/auth-unknown.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes,
|
||||
// 路由切换时滚动到顶部
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
} else {
|
||||
return { top: 0 };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const { query } = to
|
||||
// 处理URL中的token参数
|
||||
if (query.token && query.refresh_token) {
|
||||
Cookies.set("panel_token", query.token)
|
||||
Cookies.set("panel_refresh_token", query.refresh_token)
|
||||
await getUserInfo()
|
||||
await checkToken()
|
||||
next()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,38 @@
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import { userApi } from "@/api/index"
|
||||
|
||||
// 获取用户信息
|
||||
export async function getUserInfo() {
|
||||
try {
|
||||
const { code, data } = await userApi.getUserInfo()
|
||||
if (code != 0) throw new Error("code!=0")
|
||||
console.log("获取用户信息", data);
|
||||
window.localStorage.setItem("panel_user_info", JSON.stringify(data))
|
||||
} catch (error) {
|
||||
console.log("获取用户信息:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户token
|
||||
export async function checkToken() {
|
||||
try {
|
||||
const token = Cookies.get("panel_token") ?? ""
|
||||
const refresh_token = Cookies.get("panel_refresh_token") ?? "";
|
||||
if (!token || !refresh_token) return
|
||||
// 检查token
|
||||
const { code: checkCode, data: checkData } = await userApi.checkToken(token)
|
||||
if (checkCode != 0) throw new Error("checkCode!=0")
|
||||
const expire = Date.parse(checkData.expiresAt);
|
||||
if (!expire) throw new Error("时间解析错误")
|
||||
const expiredPeriod = expire - new Date().getTime();
|
||||
if (expiredPeriod <= 30 * 60 * 1000) {
|
||||
const res = await userApi.refreshToken(refresh_token)
|
||||
if (!res) throw new Error("refreshCode!=0")
|
||||
Cookies.set('panel_token', res.access_token);
|
||||
Cookies.set('panel_refresh_token', res.refresh_token);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("检查用户token:", error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 获取当前日期
|
||||
* @returns {string} 获取当前日期,如 '2024/1/15'
|
||||
*/
|
||||
export const getDate = () => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
return `${year}/${month}/${day}`;
|
||||
};
|
||||
/**
|
||||
* 获取当前时间
|
||||
* @returns {string} 获取当前日期,如 '00:00:00'
|
||||
*/
|
||||
export const getTime = () => {
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, "0");
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
/**
|
||||
* 获取最近8天的周
|
||||
* @returns {Array} 获取最近8天的周
|
||||
*/
|
||||
export function getRecentWeeks() {
|
||||
const weekArr = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||
|
||||
const result = [];
|
||||
const today = new Date();
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
const d = new Date(today);
|
||||
d.setDate(today.getDate() - i);
|
||||
result.push(weekArr[d.getDay()]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 获取最近的前8天日期(包括今天)
|
||||
* @param {number} days - 要获取的天数,默认为8
|
||||
* @returns {string[]} - 格式为 'MM-DD' 的日期数组
|
||||
*/
|
||||
export function getRecentDays(days = 8) {
|
||||
const today = new Date();
|
||||
const dates = [];
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() - i);
|
||||
// 修改日期格式为MM-DD
|
||||
const formattedDate = date.toISOString().split("T")[0].split("-").slice(1).join("-");
|
||||
dates.push(formattedDate);
|
||||
}
|
||||
|
||||
return dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的前12个月日期(包含当月)
|
||||
* @param {number} months - 要获取的月数,默认为12
|
||||
* @returns {string[]} - 格式为 'YYYY-MM' 的日期数组
|
||||
*/
|
||||
export function getRecentMonths(months = 12) {
|
||||
const today = new Date();
|
||||
const dates = [];
|
||||
|
||||
for (let i = months - 1; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setMonth(today.getMonth() - i);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始
|
||||
const formattedDate = `${year}-${month}`;
|
||||
dates.push(formattedDate);
|
||||
}
|
||||
|
||||
return dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前月份的所有日期
|
||||
* @returns {string[]} - 格式为 'DD' 的当前月份全部日期数组
|
||||
*/
|
||||
export function getMonthDays() {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = today.getMonth();
|
||||
|
||||
// 获取当月第一天
|
||||
const firstDay = new Date(year, month, 1);
|
||||
// 获取当月最后一天
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
|
||||
const dates = [];
|
||||
let currentDate = firstDay;
|
||||
|
||||
while (currentDate <= lastDay) {
|
||||
// const formattedDate = `${year}-${String(month + 1).padStart(2, "0")}-${String(currentDate.getDate()).padStart(2, "0")}`;
|
||||
const formattedDate = `${String(currentDate.getDate()).padStart(2, "0")}`;
|
||||
dates.push(formattedDate);
|
||||
|
||||
// 下一天
|
||||
currentDate = new Date(year, month, currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return dates;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 日期范围类型枚举
|
||||
* @typedef {'YESTERDAY' | 'TODAY' | 'THIS_WEEK' | 'THIS_MONTH' | 'CURRENT_SEASON' | 'HALF_YEAR' | 'THIS_YEAR'} DateRangeTypeEnum
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
|
||||
// 注册插件
|
||||
dayjs.extend(quarterOfYear);
|
||||
dayjs.extend(isoWeek);
|
||||
|
||||
/**
|
||||
* 日期工具类 - 转换日期类型为日期范围
|
||||
*/
|
||||
export const DateTool = {
|
||||
/**
|
||||
* 将日期类型转换为日期数组
|
||||
* @param {DateRangeTypeEnum} dateType - 日期类型枚举
|
||||
* @returns {Array<string>} - 日期数组 [开始日期, 结束日期],格式为 YYYY-MM-DD
|
||||
*/
|
||||
transitionDateTypeToDateList(dateType) {
|
||||
const localDateTimes = this.transitionDateTypeToDateTimeList(dateType);
|
||||
return [
|
||||
localDateTimes[0].format('YYYY-MM-DD'),
|
||||
localDateTimes[1].format('YYYY-MM-DD')
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* 将日期类型转换为日期时间数组
|
||||
* @param {DateRangeTypeEnum} dateType - 日期类型枚举
|
||||
* @returns {Array<dayjs.Dayjs>} - 日期时间数组 [开始日期时间, 结束日期时间]
|
||||
*/
|
||||
transitionDateTypeToDateTimeList(dateType) {
|
||||
const now = dayjs();
|
||||
|
||||
switch (dateType) {
|
||||
case 'YESTERDAY':
|
||||
return [
|
||||
dayjs().subtract(1, 'day').startOf('day'),
|
||||
dayjs().subtract(1, 'day').endOf('day')
|
||||
];
|
||||
case 'TODAY':
|
||||
return [
|
||||
dayjs().startOf('day'),
|
||||
dayjs().endOf('day')
|
||||
];
|
||||
case 'THIS_WEEK':
|
||||
return [
|
||||
dayjs().subtract(7, 'day').startOf('day'),
|
||||
dayjs().endOf('day')
|
||||
];
|
||||
case 'THIS_MONTH':
|
||||
return [
|
||||
dayjs().startOf('month'),
|
||||
dayjs().endOf('month')
|
||||
];
|
||||
case 'CURRENT_SEASON':
|
||||
return [
|
||||
dayjs().startOf('quarter'),
|
||||
dayjs().endOf('quarter')
|
||||
];
|
||||
case 'HALF_YEAR':
|
||||
// 半年期间(从当前季度开始的6个月)
|
||||
const halfYearStart = dayjs().startOf('quarter');
|
||||
return [
|
||||
halfYearStart,
|
||||
halfYearStart.add(5, 'month').endOf('month')
|
||||
];
|
||||
case 'THIS_YEAR':
|
||||
return [
|
||||
dayjs().startOf('year'),
|
||||
dayjs().endOf('year')
|
||||
];
|
||||
default:
|
||||
return [null, null];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 将日期类型转换为格式化的日期时间数组
|
||||
* @param {DateRangeTypeEnum} dateType - 日期类型枚举
|
||||
* @param {string} format - 日期格式,默认为 'YYYY-MM-DD HH:mm:ss'
|
||||
* @returns {Array<string>} - 格式化的日期时间数组 [开始日期时间, 结束日期时间]
|
||||
*/
|
||||
getFormattedDateTimeRange(dateType, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
const dateTimeRange = this.transitionDateTypeToDateTimeList(dateType);
|
||||
return [
|
||||
dateTimeRange[0]?.format(format) || '',
|
||||
dateTimeRange[1]?.format(format) || ''
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 显示价格把分转换到元
|
||||
*/
|
||||
export function displayPrice(price, suffix = "元", fixed = 2) {
|
||||
if (typeof price === "undefined") return "0.00" + suffix
|
||||
const parsedNumber = typeof price === "string" ? parseFloat(price) : price
|
||||
return (parsedNumber / 100.0).toFixed(fixed) + suffix
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* 轮询类 - 用于管理定时轮询任务
|
||||
*/
|
||||
class Poll {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} options 配置选项
|
||||
* @param {Function} options.callback 轮询回调函数
|
||||
* @param {number} options.interval 轮询间隔时间(毫秒),默认1000ms
|
||||
* @param {boolean} options.immediate 是否立即执行一次,默认true
|
||||
* @param {number} options.maxRetries 最大重试次数,默认3次
|
||||
* @param {Function} options.onError 错误处理回调
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.callback = options.callback || (() => { });
|
||||
this.interval = options.interval || 1000 + Math.random() * 1000;
|
||||
this.immediate = options.immediate || true;
|
||||
this.maxRetries = options.maxRetries || 3;
|
||||
this.onError = options.onError || ((error) => console.error("轮询错误:", error));
|
||||
|
||||
// 内部状态
|
||||
this.timerId = null;
|
||||
this.isRunning = false;
|
||||
this.isPaused = false;
|
||||
this.retryCount = 0;
|
||||
this.executionCount = 0;
|
||||
|
||||
// 绑定this上下文
|
||||
this._execute = this._execute.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化轮询配置
|
||||
* @param {Function} callback 轮询回调函数
|
||||
* @param {number} interval 轮询间隔时间(毫秒)
|
||||
* @param {Object} options 其他配置选项
|
||||
*/
|
||||
init(callback, interval, options = {}) {
|
||||
// 如果正在运行,先停止
|
||||
if (this.isRunning) {
|
||||
this.stop();
|
||||
}
|
||||
this.callback = callback || this.callback;
|
||||
this.interval = interval || this.interval;
|
||||
this.immediate = options.immediate !== undefined ? options.immediate : this.immediate;
|
||||
this.maxRetries = options.maxRetries !== undefined ? options.maxRetries : this.maxRetries;
|
||||
this.onError = options.onError || this.onError;
|
||||
// 重置状态
|
||||
this.retryCount = 0;
|
||||
this.executionCount = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 开始轮询
|
||||
start() {
|
||||
if (this.isRunning) {
|
||||
return this;
|
||||
}
|
||||
if (!this.callback || typeof this.callback !== "function") {
|
||||
throw new Error("轮询回调函数未定义或不是函数");
|
||||
}
|
||||
this.isRunning = true;
|
||||
this.isPaused = false;
|
||||
// 如果设置了立即执行,先执行一次
|
||||
if (this.immediate) {
|
||||
this._execute();
|
||||
} else {
|
||||
this._scheduleNext();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// 暂停轮询
|
||||
pause() {
|
||||
if (!this.isRunning) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.isPaused = true;
|
||||
this._clearTimer();
|
||||
return this;
|
||||
}
|
||||
|
||||
// 恢复轮询
|
||||
resume() {
|
||||
if (!this.isRunning) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.isPaused) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.isPaused = false;
|
||||
this._scheduleNext();
|
||||
return this;
|
||||
}
|
||||
|
||||
// 停止轮询
|
||||
stop() {
|
||||
if (!this.isRunning) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
this.isPaused = false;
|
||||
this._clearTimer();
|
||||
return this;
|
||||
}
|
||||
|
||||
// 销毁轮询实例
|
||||
destroy() {
|
||||
this.stop();
|
||||
this.callback = null;
|
||||
this.onError = null;
|
||||
this.retryCount = 0;
|
||||
this.executionCount = 0;
|
||||
}
|
||||
|
||||
// 立即执行一次轮询(不影响定时器)
|
||||
executeOnce() {
|
||||
if (typeof this.callback === "function") {
|
||||
this._execute(false); // false表示不调度下一次
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮询间隔
|
||||
* @param {number} newInterval 新的间隔时间(毫秒)
|
||||
*/
|
||||
updateInterval(newInterval) {
|
||||
if (typeof newInterval !== "number" || newInterval <= 0) {
|
||||
throw new Error("间隔时间必须是正数");
|
||||
}
|
||||
|
||||
this.interval = newInterval;
|
||||
|
||||
// 如果正在运行且未暂停,重新调度
|
||||
if (this.isRunning && !this.isPaused) {
|
||||
this._clearTimer();
|
||||
this._scheduleNext();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// 获取轮询状态
|
||||
getStatus() {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
isPaused: this.isPaused,
|
||||
interval: this.interval,
|
||||
executionCount: this.executionCount,
|
||||
retryCount: this.retryCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部方法:执行轮询回调
|
||||
* @param {boolean} scheduleNext 是否调度下一次执行
|
||||
*/
|
||||
async _execute(scheduleNext = true) {
|
||||
if (!this.isRunning || this.isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.executionCount++;
|
||||
|
||||
// 执行回调函数
|
||||
await this.callback();
|
||||
|
||||
// 重置重试计数
|
||||
this.retryCount = 0;
|
||||
|
||||
// 调度下一次执行
|
||||
if (scheduleNext && this.isRunning && !this.isPaused) {
|
||||
this._scheduleNext();
|
||||
}
|
||||
} catch (error) {
|
||||
this.retryCount++;
|
||||
console.error(`轮询执行失败 (第${this.retryCount}次重试):`, error);
|
||||
|
||||
// 调用错误处理回调
|
||||
if (typeof this.onError === "function") {
|
||||
try {
|
||||
await this.onError(error, this.retryCount, this.executionCount);
|
||||
} catch (errorHandlerError) {
|
||||
console.error("错误处理回调执行失败:", errorHandlerError);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要重试
|
||||
if (this.retryCount < this.maxRetries) {
|
||||
if (scheduleNext && this.isRunning && !this.isPaused) {
|
||||
this._scheduleNext();
|
||||
}
|
||||
} else {
|
||||
console.error(`达到最大重试次数 (${this.maxRetries}),停止轮询`);
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内部方法:调度下一次执行
|
||||
_scheduleNext() {
|
||||
this._clearTimer();
|
||||
this.timerId = setTimeout(this._execute, this.interval);
|
||||
}
|
||||
|
||||
// 内部方法:清除定时器
|
||||
_clearTimer() {
|
||||
if (this.timerId) {
|
||||
clearTimeout(this.timerId);
|
||||
this.timerId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Poll;
|
||||
|
||||
// 也可以导出一个工厂函数,方便快速创建轮询实例
|
||||
export function createPoll(callback, interval, options = {}) {
|
||||
return new Poll({
|
||||
callback,
|
||||
interval,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// 导出一个简化的轮询函数,用于简单场景
|
||||
export function simplePoll(callback, interval = 1000) {
|
||||
const poll = new Poll({ callback, interval });
|
||||
poll.start();
|
||||
return poll;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="no-permission">
|
||||
<div class="container">
|
||||
<div class="icon">
|
||||
<el-icon>
|
||||
<WarningFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
<h1>无权限访问</h1>
|
||||
<p>抱歉,您没有权限访问该页面</p>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="quit">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { WarningFilled } from '@element-plus/icons-vue';
|
||||
|
||||
const quit = () => {
|
||||
// 尝试关闭窗口,如果是脚本打开的窗口才能成功关闭
|
||||
try {
|
||||
window.close();
|
||||
} catch (e) {
|
||||
console.error('无法关闭窗口:', e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.no-permission {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #0a1218;
|
||||
background-image: url("@/assets/images/bg-image.png");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: rgba(26, 54, 66, 0.7);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.icon .el-icon {
|
||||
font-size: 64px;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
color: #f2f3f6;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: #a3a6ad;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,495 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-aside class="aside">
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>就餐人数统计</span>
|
||||
</template>
|
||||
<AttendanceStatistic :data="timerObj.byHourType.data.data" :labels="timerObj.byHourType.data.labels"
|
||||
@changeDateType="byHourTypeChangeDateType" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>今日就餐人数统计</span>
|
||||
</template>
|
||||
<TodayClockStatus :data="timerObj.haveDinnerTotal.data.data" :labels="timerObj.haveDinnerTotal.data.labels" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>今日消费次数排行(top5)</span>
|
||||
</template>
|
||||
<PersonThroughRank :data="timerObj.mchPayCount.data" />
|
||||
</AsideBlock>
|
||||
</el-aside>
|
||||
|
||||
<div class="main">
|
||||
<AsideBlock height="calc(100% * 2 / 3)" titleBgWidth="50%">
|
||||
<template #header>
|
||||
<span>食堂报表</span>
|
||||
<div class="report-search">
|
||||
<el-date-picker style="width: 20vw;" v-model="queryParams.createTimeList" type="datetimerange" size="small"
|
||||
placeholder="请选择日期" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
@change="changeQueryDate" />
|
||||
</div>
|
||||
</template>
|
||||
<CanteenReport ref="canteenReportRef" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock titleBgWidth="50%">
|
||||
<template #header>
|
||||
<span>食堂购买人数趋势统计</span>
|
||||
</template>
|
||||
<PersonAbnormalEventLineChart yTitle="购买人数" :data="timerObj.mchPayPersonCountTrend.data" />
|
||||
</AsideBlock>
|
||||
</div>
|
||||
|
||||
<el-aside class="aside">
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>食堂用餐人数统计</span>
|
||||
</template>
|
||||
<AbnormalEventTypeStatus title="用餐人数" type="person" :data="timerObj.mchPayPersonTotal.data"
|
||||
@changeDateType="alarmTotalByTimerChnageDateType" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock height="calc(100% * 2 / 3)">
|
||||
<template #header>
|
||||
<span>食堂用餐实时统计</span>
|
||||
</template>
|
||||
<AbnormalEventList type="person" :data="timerObj.canteenOrderPage.data"
|
||||
@changeDateType="alarmPageDateTypeChange" />
|
||||
</AsideBlock>
|
||||
</el-aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, reactive, ref } from "vue";
|
||||
import { canteenApi } from "@/api/index"
|
||||
|
||||
import useNotification from "@/components/notification/index";
|
||||
|
||||
import { simplePoll } from "@/utils/poll"
|
||||
import { displayPrice } from "@/utils/format-money"
|
||||
|
||||
import AsideBlock from "@/components/aside-block.vue";
|
||||
import AttendanceStatistic from "@/components/attendance-statistic/index.vue";
|
||||
import TodayClockStatus from "@/components/person-today-clock-status.vue";
|
||||
import PersonAbnormalEventLineChart from "@/components/person-abnormal-event-line-chart.vue";
|
||||
import PersonThroughRank from "@/components/person-through-rank.vue";
|
||||
import AbnormalEventTypeStatus from "@/components/abnormal-event-type-status.vue";
|
||||
import AbnormalEventList from "@/components/abnormal-event-list.vue";
|
||||
import CanteenReport from "@/components/canteen-report.vue"
|
||||
|
||||
const { notificationAdd, notificationClear } = useNotification();
|
||||
|
||||
const canteenReportRef = ref()
|
||||
|
||||
const queryParams = reactive({
|
||||
createTimeList: null,
|
||||
})
|
||||
|
||||
const timerObj = reactive({
|
||||
// 食堂用餐人数统计
|
||||
mchPayPersonTotal: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {
|
||||
dateRangeType: "YESTERDAY",
|
||||
},
|
||||
data: null,
|
||||
},
|
||||
// 食堂用餐实时统计
|
||||
canteenOrderPage: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {
|
||||
dateRangeType: "YESTERDAY",
|
||||
current: 1,
|
||||
size: 30,
|
||||
},
|
||||
data: null,
|
||||
},
|
||||
// 食堂购买人数趋势统计
|
||||
mchPayPersonCountTrend: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {},
|
||||
data: null,
|
||||
},
|
||||
// 今日消费次数排行
|
||||
mchPayCount: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {
|
||||
dateRangeType: "TODAY"
|
||||
},
|
||||
data: null,
|
||||
},
|
||||
// 用餐时间段统计
|
||||
byHourType: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {
|
||||
dateRangeType: "YESTERDAY",
|
||||
},
|
||||
data: {
|
||||
data: null,
|
||||
labels: null,
|
||||
},
|
||||
},
|
||||
// 今日就餐人数统计
|
||||
haveDinnerTotal: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {
|
||||
dateRangeType: "TODAY",
|
||||
},
|
||||
data: {
|
||||
data: null,
|
||||
labels: null,
|
||||
},
|
||||
},
|
||||
// 最近异常事件
|
||||
recentlyAlarm: {
|
||||
initial: null,
|
||||
interval: 5 * 1000,
|
||||
queryParams: {},
|
||||
data: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getAttendanceStatisticData();
|
||||
getTodayClockStatusData();
|
||||
getPersonThroughRankData();
|
||||
getAbnormalEventType();
|
||||
getAbnormalEventTypeStatusData();
|
||||
getCanteenOrderList();
|
||||
getNotification();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
notificationClear();
|
||||
for (let key of Object.keys(timerObj)) {
|
||||
const value = timerObj[key];
|
||||
if (value && value.initial) {
|
||||
value.initial.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const changeQueryDate = (e) => {
|
||||
canteenReportRef.value?.refresh(e)
|
||||
}
|
||||
|
||||
// 用餐时间段统计
|
||||
const getAttendanceStatisticData = () => {
|
||||
timerObj.byHourType.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await canteenApi.groupByTimeAndHourType({ ...timerObj.byHourType.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("用餐时间段统计:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
// 昨天
|
||||
const { dateRangeType } = timerObj.byHourType.queryParams;
|
||||
if (dateRangeType === "YESTERDAY") {
|
||||
timerObj.byHourType.data.data = {
|
||||
YESTERDAY: {
|
||||
morning: data.morning.list[0].personCount,
|
||||
noon: data.noon.list[0].personCount,
|
||||
evening: data.evening.list[0].personCount,
|
||||
},
|
||||
};
|
||||
}
|
||||
// 今日
|
||||
if (dateRangeType === "TODAY") {
|
||||
timerObj.byHourType.data.data = {
|
||||
TODAY: {
|
||||
morning: data.morning.list[0].personCount,
|
||||
noon: data.noon.list[0].personCount,
|
||||
evening: data.evening.list[0].personCount,
|
||||
},
|
||||
};
|
||||
}
|
||||
// 本周
|
||||
if (dateRangeType === "THIS_WEEK") {
|
||||
timerObj.byHourType.data.data = { THIS_WEEK: disposeBeLateTotal(data) };
|
||||
timerObj.byHourType.data.labels = {
|
||||
THIS_WEEK: data.morning?.list?.map((v) => {
|
||||
return v.time + "|" + v.weekStr;
|
||||
}),
|
||||
};
|
||||
}
|
||||
// 本月
|
||||
if (dateRangeType === "THIS_MONTH") {
|
||||
timerObj.byHourType.data.data = {
|
||||
THIS_MONTH: data.morning?.list?.map((v) => {
|
||||
const morningCount = data.morning?.list.find((item) => item.time === item.time)?.personCount ?? 0
|
||||
const noonCount = data.noon?.list.find((item) => item.time === item.time)?.personCount ?? 0
|
||||
const eveningCount = data.evening?.list.find((item) => item.time === item.time)?.personCount ?? 0
|
||||
let type = -1;
|
||||
// 如果time大于等于当天
|
||||
const targetDate = new Date(v.time);
|
||||
const today = new Date().setHours(23, 59, 59, 0);
|
||||
if (targetDate <= today) {
|
||||
type = 0;
|
||||
// 用餐人数小于等于0则为休息
|
||||
if (v.personCount <= 0) {
|
||||
type = 1;
|
||||
}
|
||||
}
|
||||
return {
|
||||
day: v.time.slice(-2),
|
||||
type: type,
|
||||
morningCount,
|
||||
noonCount,
|
||||
eveningCount,
|
||||
};
|
||||
}),
|
||||
};
|
||||
timerObj.byHourType.data.labels = {
|
||||
THIS_MONTH: [],
|
||||
};
|
||||
}
|
||||
// 本年
|
||||
if (dateRangeType === "THIS_YEAR") {
|
||||
timerObj.byHourType.data.data = {
|
||||
THIS_YEAR: disposeBeLateTotal(data),
|
||||
};
|
||||
timerObj.byHourType.data.labels = {
|
||||
THIS_YEAR: data.morning?.list?.map((v) => {
|
||||
return v.time.slice(0, -3);
|
||||
}),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("|用餐时间段统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.byHourType.interval);
|
||||
};
|
||||
|
||||
// 处理打卡统计的数据
|
||||
const disposeBeLateTotal = (obj) => {
|
||||
if (!obj) return;
|
||||
return [
|
||||
{
|
||||
text: "早饭",
|
||||
name: "morning",
|
||||
values:
|
||||
obj?.morning?.list?.map((v) => {
|
||||
return Number(v.personCount);
|
||||
}) ?? [],
|
||||
},
|
||||
{
|
||||
text: "中饭",
|
||||
name: "noon",
|
||||
values:
|
||||
obj?.noon?.list?.map((v) => {
|
||||
return Number(v.personCount);
|
||||
}) ?? [],
|
||||
},
|
||||
{
|
||||
text: "晚饭",
|
||||
name: "evening",
|
||||
values:
|
||||
obj?.evening?.list?.map((v) => {
|
||||
return Number(v.personCount);
|
||||
}) ?? [],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const byHourTypeChangeDateType = (type) => {
|
||||
timerObj.byHourType.queryParams.dateRangeType = type;
|
||||
timerObj.byHourType.initial.executeOnce();
|
||||
};
|
||||
|
||||
// 今日就餐人数统计
|
||||
const getTodayClockStatusData = () => {
|
||||
timerObj.haveDinnerTotal.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await canteenApi.groupByTimeAndCustomHour({ ...timerObj.haveDinnerTotal.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("今日就餐人数统计:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
timerObj.haveDinnerTotal.data.data = data.map((v) => {
|
||||
return Number(v.personCount);
|
||||
});
|
||||
timerObj.haveDinnerTotal.data.labels = data.map((v) => {
|
||||
return v.hourType;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("|今日就餐人数统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.haveDinnerTotal.interval);
|
||||
};
|
||||
|
||||
// 今日消费次数排行
|
||||
const getPersonThroughRankData = () => {
|
||||
timerObj.mchPayCount.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await canteenApi.mchPayCountTop({ ...timerObj.mchPayCount.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("今日消费次数排行:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
timerObj.mchPayCount.data = {
|
||||
labels:
|
||||
data
|
||||
?.map((v) => {
|
||||
return v.mchName;
|
||||
})
|
||||
.reverse() ?? [],
|
||||
values:
|
||||
data
|
||||
?.map((v) => {
|
||||
return v.buyCount;
|
||||
})
|
||||
.reverse() ?? [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("|今日消费次数排行|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.mchPayCount.interval);
|
||||
};
|
||||
|
||||
// 食堂购买人数趋势统计
|
||||
const getAbnormalEventType = async () => {
|
||||
timerObj.mchPayPersonCountTrend.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await canteenApi.payPersonCountTrend({ ...timerObj.mchPayPersonCountTrend.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("食堂购买人数趋势统计:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
timerObj.mchPayPersonCountTrend.data = data;
|
||||
} catch (error) {
|
||||
console.error("|食堂购买人数趋势统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.mchPayPersonCountTrend.interval);
|
||||
};
|
||||
|
||||
// 食堂用餐人数统计
|
||||
const getAbnormalEventTypeStatusData = () => {
|
||||
timerObj.mchPayPersonTotal.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await canteenApi.mchPayPersonCount({ ...timerObj.mchPayPersonTotal.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("食堂用餐人数统计:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
timerObj.mchPayPersonTotal.data =
|
||||
data.map((v) => {
|
||||
return {
|
||||
title: v.mchName,
|
||||
type: v.mchId,
|
||||
total: v.personCount,
|
||||
};
|
||||
}) ?? [];
|
||||
} catch (error) {
|
||||
console.error("|食堂用餐人数统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.mchPayPersonTotal.interval);
|
||||
};
|
||||
|
||||
const alarmTotalByTimerChnageDateType = (type) => {
|
||||
timerObj.mchPayPersonTotal.queryParams.dateRangeType = type;
|
||||
timerObj.mchPayPersonTotal.initial.executeOnce();
|
||||
};
|
||||
|
||||
// 食堂用餐实时统计
|
||||
const getCanteenOrderList = () => {
|
||||
timerObj.canteenOrderPage.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await canteenApi.newPage({ ...timerObj.canteenOrderPage.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("食堂用餐实时统计:", data);
|
||||
if (!data || !data.records || !data.records.length) {
|
||||
timerObj.canteenOrderPage.data = [];
|
||||
return;
|
||||
}
|
||||
timerObj.canteenOrderPage.data = data.records.map((v) => {
|
||||
return {
|
||||
tag: v.merchantName,
|
||||
name: `${v.productDesc},共${v.payPrice}元`,
|
||||
time: v.createTime,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("|食堂用餐实时统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.canteenOrderPage.interval);
|
||||
};
|
||||
const alarmPageDateTypeChange = (type) => {
|
||||
timerObj.canteenOrderPage.queryParams.dateRangeType = type;
|
||||
timerObj.canteenOrderPage.initial.executeOnce();
|
||||
};
|
||||
|
||||
// 最近用餐数据
|
||||
const getNotification = () => {
|
||||
timerObj.recentlyAlarm.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await canteenApi.recentlyCanteen({ ...timerObj.recentlyAlarm.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("最近用餐数据:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
notificationAdd({
|
||||
title: "最近用餐数据",
|
||||
content: {
|
||||
tag: data.merchantName,
|
||||
channelName: `${data.productDesc},${displayPrice(data.payPrice)}`,
|
||||
createTime: `${data.createTime}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("|最近报警|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.recentlyAlarm.interval);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.report-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
color: @text-color;
|
||||
|
||||
.aside {
|
||||
height: 100%;
|
||||
width: 24%;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
margin: 0 1vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-aside class="aside">
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>营养分析</span>
|
||||
</template>
|
||||
<NutritionAnalyse :data="timerObj.nutritionAnalyse.data.data"
|
||||
:labels="timerObj.nutritionAnalyse.data.labels" @changeDateType="nutritionAnalyseChangeDateType" />
|
||||
</AsideBlock>
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>今日就餐人数统计</span>
|
||||
</template>
|
||||
</AsideBlock>
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>今日复购排行榜(top5)</span>
|
||||
</template>
|
||||
<PersonThroughRank :data="timerObj.repurchase.data" />
|
||||
</AsideBlock>
|
||||
</el-aside>
|
||||
<div class="main">
|
||||
<AsideBlock height="calc(100% * 2 / 3)" titleBgWidth="50%">
|
||||
<template #header>
|
||||
<span>营养分析</span>
|
||||
<div class="report-search">
|
||||
<el-date-picker style="width: 20vw;" v-model="queryParams.createTimeList" type="datetimerange"
|
||||
size="small" placeholder="请选择日期" format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss" @change="changeQueryDate" />
|
||||
</div>
|
||||
</template>
|
||||
<NutritionReport ref="nutritionReportRef" />
|
||||
</AsideBlock>
|
||||
<AsideBlock titleBgWidth="50%">
|
||||
<template #header>
|
||||
<span>营养分析趋势</span>
|
||||
</template>
|
||||
<PersonAbnormalEventLineChart type="nutrition" yTitle="购买次数"
|
||||
:data="timerObj.nutritionAnalyseTrend.data" />
|
||||
</AsideBlock>
|
||||
</div>
|
||||
<el-aside class="aside">
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>菜品分类购买数量</span>
|
||||
</template>
|
||||
<AbnormalEventTypeStatus title="购买数量" type="shop" :data="timerObj.classBuyCount.data"
|
||||
@changeDateType="alarmTotalByTimerChnageDateType" />
|
||||
</AsideBlock>
|
||||
<AsideBlock height="calc(100% * 2 / 3)">
|
||||
<template #header>
|
||||
<span>菜品喜好排行</span>
|
||||
</template>
|
||||
<NutritionTop :data="timerObj.cuisineTop.data" @change-date-type="changeCuisineTopDateType" />
|
||||
</AsideBlock>
|
||||
</el-aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, reactive, ref } from "vue";
|
||||
import { nutritionApi } from "@/api/index"
|
||||
|
||||
import useNotification from "@/components/notification/index";
|
||||
|
||||
import { simplePoll } from "@/utils/poll"
|
||||
import { displayPrice } from "@/utils/format-money"
|
||||
|
||||
import AsideBlock from "@/components/aside-block.vue";
|
||||
import NutritionReport from "@/components/nutrition-report.vue";
|
||||
import AbnormalEventTypeStatus from "@/components/abnormal-event-type-status.vue";
|
||||
import NutritionAnalyse from "@/components/nutrition-analyse/index.vue"
|
||||
import PersonThroughRank from "@/components/person-through-rank.vue";
|
||||
import PersonAbnormalEventLineChart from "@/components/person-abnormal-event-line-chart.vue";
|
||||
import NutritionTop from "@/components/nutrition-top.vue"
|
||||
|
||||
const { notificationAdd, notificationClear } = useNotification();
|
||||
|
||||
const nutritionReportRef = ref()
|
||||
|
||||
const queryParams = reactive({
|
||||
createTimeList: null,
|
||||
})
|
||||
|
||||
const timerObj = reactive({
|
||||
// 复购排行榜
|
||||
repurchase: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: null,
|
||||
queryParams: {
|
||||
dateRangeType: "YESTERDAY"
|
||||
},
|
||||
},
|
||||
// 营养分析
|
||||
nutritionAnalyse: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: {
|
||||
data: null,
|
||||
labels: null,
|
||||
},
|
||||
queryParams: {
|
||||
dateRangeType: "YESTERDAY"
|
||||
},
|
||||
},
|
||||
// 营养分析趋势
|
||||
nutritionAnalyseTrend: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: {
|
||||
data: null,
|
||||
labels: null,
|
||||
},
|
||||
queryParams: {},
|
||||
},
|
||||
// 菜品分类购买数量
|
||||
classBuyCount: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: null,
|
||||
queryParams: {
|
||||
dateRangeType: "YESTERDAY"
|
||||
},
|
||||
},
|
||||
// 菜品喜好排行
|
||||
cuisineTop: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: null,
|
||||
queryParams: {
|
||||
dateRangeType: "YESTERDAY"
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getRepurchase()
|
||||
getNutritionAnalyse()
|
||||
getClassBuyCount()
|
||||
getCuisineTop()
|
||||
getNutritionAnalyseTrend()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
notificationClear();
|
||||
for (let key of Object.keys(timerObj)) {
|
||||
const value = timerObj[key];
|
||||
if (value && value.initial) {
|
||||
value.initial.destroy();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 复购排行榜
|
||||
const getRepurchase = () => {
|
||||
timerObj.repurchase.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await nutritionApi.repurchase({ ...timerObj.repurchase.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("复购排行榜:", data);
|
||||
if (!data) return;
|
||||
timerObj.repurchase.data = data
|
||||
} catch (error) {
|
||||
console.error("|复购排行榜|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.repurchase.interval)
|
||||
}
|
||||
|
||||
// 营养分析趋势
|
||||
const getNutritionAnalyseTrend = () => {
|
||||
timerObj.nutritionAnalyseTrend.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await nutritionApi.nutritionTrend({ ...timerObj.nutritionAnalyseTrend.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("营养分析趋势:", data);
|
||||
if (!data) return;
|
||||
timerObj.nutritionAnalyseTrend.data = data;
|
||||
} catch (error) {
|
||||
console.error("|营养分析趋势|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.nutritionAnalyseTrend.interval)
|
||||
}
|
||||
|
||||
// 营养分析
|
||||
const getNutritionAnalyse = () => {
|
||||
timerObj.nutritionAnalyse.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await nutritionApi.nutritionByTime({ ...timerObj.nutritionAnalyse.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("营养分析:", data);
|
||||
if (!data) return;
|
||||
const dataKeys = Object.keys(data)
|
||||
const { dateRangeType } = timerObj.nutritionAnalyse.queryParams;
|
||||
// 昨天
|
||||
if (dateRangeType === "YESTERDAY") {
|
||||
timerObj.nutritionAnalyse.data.data = {
|
||||
YESTERDAY: Object.keys(data).map(v => {
|
||||
const _entity = data[v]
|
||||
return {
|
||||
title: _entity.typeStr,
|
||||
total: _entity?.list[0]?.buyProductCount ?? 0
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
// 今天
|
||||
if (dateRangeType === "TODAY") {
|
||||
timerObj.nutritionAnalyse.data.data = {
|
||||
// 今日
|
||||
TODAY: Object.keys(data).map(v => {
|
||||
const _entity = data[v]
|
||||
return {
|
||||
title: _entity.typeStr,
|
||||
total: _entity?.list[0]?.buyProductCount ?? 0
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
// 本周
|
||||
if (dateRangeType === "THIS_WEEK") {
|
||||
timerObj.nutritionAnalyse.data.data = { THIS_WEEK: disposeBeLateTotal(data) };
|
||||
timerObj.nutritionAnalyse.data.labels = {
|
||||
THIS_WEEK: data[dataKeys[0]]?.list?.map((v) => {
|
||||
return v.time + "|" + v.weekStr;
|
||||
}),
|
||||
};
|
||||
}
|
||||
// 本月
|
||||
if (dateRangeType === "THIS_MONTH") {
|
||||
timerObj.nutritionAnalyse.data.data = {
|
||||
THIS_MONTH: data[dataKeys[0]]?.list?.map((v) => {
|
||||
const hcCount = data.hc?.list.find((item) => item.time === item.time)?.buyProductCount ?? 0
|
||||
const scCount = data.sc?.list.find((item) => item.time === item.time)?.buyProductCount ?? 0
|
||||
const zsCount = data.zs?.list.find((item) => item.time === item.time)?.buyProductCount ?? 0
|
||||
return {
|
||||
day: v.time.slice(-2),
|
||||
type: 0,
|
||||
hcCount: Number(hcCount),
|
||||
scCount: Number(scCount),
|
||||
zsCount: Number(zsCount),
|
||||
};
|
||||
}),
|
||||
}
|
||||
timerObj.nutritionAnalyse.data.labels = {
|
||||
THIS_MONTH: [],
|
||||
};
|
||||
}
|
||||
// 本年
|
||||
if (dateRangeType === "THIS_YEAR") {
|
||||
timerObj.nutritionAnalyse.data.data = {
|
||||
THIS_YEAR: disposeBeLateTotal(data),
|
||||
};
|
||||
timerObj.nutritionAnalyse.data.labels = {
|
||||
THIS_YEAR: data[dataKeys[0]]?.list?.map((v) => {
|
||||
return v.time.slice(0, -3);
|
||||
}),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("|营养分析|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.nutritionAnalyse.interval);
|
||||
}
|
||||
|
||||
const disposeBeLateTotal = (obj) => {
|
||||
if (!obj) return;
|
||||
return Object.keys(obj).map(v => {
|
||||
const _entity = obj[v]
|
||||
return {
|
||||
text: _entity.typeStr,
|
||||
name: _entity.type,
|
||||
values:
|
||||
_entity?.list?.map((v) => {
|
||||
return Number(v.buyProductCount);
|
||||
}) ?? [],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const nutritionAnalyseChangeDateType = (type) => {
|
||||
timerObj.nutritionAnalyse.queryParams.dateRangeType = type;
|
||||
timerObj.nutritionAnalyse.initial.executeOnce(type);
|
||||
}
|
||||
|
||||
// 菜品分类购买数量
|
||||
const getClassBuyCount = () => {
|
||||
timerObj.classBuyCount.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await nutritionApi.classCount({ ...timerObj.classBuyCount.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("菜品分类购买数量:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
timerObj.classBuyCount.data =
|
||||
data.map((v) => {
|
||||
return {
|
||||
title: v.classifyName,
|
||||
type: v.classifyId,
|
||||
total: v.buyCount,
|
||||
};
|
||||
}) ?? [];
|
||||
console.log("timerObj.classBuyCount.data:", timerObj.classBuyCount.data);
|
||||
} catch (error) {
|
||||
console.error("|菜品分类购买数量|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.classBuyCount.interval);
|
||||
};
|
||||
|
||||
const alarmTotalByTimerChnageDateType = async (type) => {
|
||||
timerObj.classBuyCount.queryParams.dateRangeType = type;
|
||||
timerObj.classBuyCount.initial.executeOnce();
|
||||
}
|
||||
|
||||
// 菜品喜好排行
|
||||
const getCuisineTop = () => {
|
||||
timerObj.cuisineTop.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await nutritionApi.cuisineTop({ ...timerObj.cuisineTop.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("菜品喜好排行:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
timerObj.cuisineTop.data =
|
||||
data.map((v) => {
|
||||
return {
|
||||
title: v.productName,
|
||||
type: v.productId,
|
||||
total: v.buyProductCount,
|
||||
};
|
||||
}) ?? [];
|
||||
} catch (error) {
|
||||
console.error("|菜品分类购买数量|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.cuisineTop.interval);
|
||||
}
|
||||
|
||||
const changeCuisineTopDateType = (type) => {
|
||||
timerObj.cuisineTop.queryParams.dateRangeType = type;
|
||||
timerObj.cuisineTop.initial.executeOnce();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
color: @text-color;
|
||||
|
||||
.aside {
|
||||
height: 100%;
|
||||
width: 24%;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
margin: 0 1vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="container"></div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-aside class="aside">
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>设备状态统计</span>
|
||||
</template>
|
||||
<DeviceStatus :data="timerObj.deviceTotal.data" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>各商户购买次数趋势统计(近7日)</span>
|
||||
</template>
|
||||
<DeviceOnlineLineChart :data="timerObj.deviceOfflineRate.data.list"
|
||||
:labels="timerObj.deviceOfflineRate.data.labels" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>今日消费次数排行(top5)</span>
|
||||
</template>
|
||||
<DeviceMonitoringQuantity :data="timerObj.topTotal.data" />
|
||||
</AsideBlock>
|
||||
</el-aside>
|
||||
|
||||
<div class="main">
|
||||
<AsideBlock height="calc(100% * 2 / 3)" titleBgWidth="50%">
|
||||
<template #header>
|
||||
<span>实时视频</span>
|
||||
<div class="report-search">
|
||||
<el-date-picker style="width: 20vw;" v-model="queryParams.createTimeList" type="datetimerange" size="small"
|
||||
placeholder="请选择日期" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
|
||||
@change="changeQueryDate" />
|
||||
</div>
|
||||
</template>
|
||||
<ShopReport ref="shopReportRef" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock titleBgWidth="50%">
|
||||
<template #header>
|
||||
<span>各商户购买次数趋势统计</span>
|
||||
</template>
|
||||
<PersonAbnormalEventLineChart type="shop" yTitle="购买次数" :data="timerObj.deviceAlarmTrend.data" />
|
||||
</AsideBlock>
|
||||
</div>
|
||||
|
||||
<el-aside class="aside">
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>超市消费人数统计</span>
|
||||
</template>
|
||||
<AbnormalEventTypeStatus title="购买人数" type="shop" :data="timerObj.areaCameraTotal.data"
|
||||
@changeDateType="alarmTotalByTimerChnageDateType" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock height="calc(100% * 2 / 3)">
|
||||
<template #header>
|
||||
<span>超市购买实时统计</span>
|
||||
</template>
|
||||
<AbnormalEventList type="shop" :data="timerObj.shopOrder.data" @changeDateType="alarmPageDateTypeChange" />
|
||||
</AsideBlock>
|
||||
</el-aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, reactive, ref } from "vue";
|
||||
import { shopApi } from "@/api/index"
|
||||
|
||||
import { simplePoll } from "@/utils/poll"
|
||||
import { displayPrice } from "@/utils/format-money"
|
||||
|
||||
import AsideBlock from "@/components/aside-block.vue";
|
||||
import DeviceStatus from "@/components/device-status.vue";
|
||||
import DeviceOnlineLineChart from "@/components/device-online-line-chart.vue";
|
||||
import PersonAbnormalEventLineChart from "@/components/person-abnormal-event-line-chart.vue";
|
||||
import DeviceMonitoringQuantity from "@/components/device-monitoring-quantity.vue";
|
||||
import AbnormalEventTypeStatus from "@/components/abnormal-event-type-status.vue";
|
||||
import AbnormalEventList from "@/components/abnormal-event-list.vue";
|
||||
import ShopReport from "@/components/shop-report.vue";
|
||||
|
||||
import useNotification from "@/components/notification/index";
|
||||
|
||||
const { notificationAdd, notificationClear } = useNotification();
|
||||
|
||||
const shopReportRef = ref()
|
||||
|
||||
const queryParams = reactive({
|
||||
createTimeList: null,
|
||||
})
|
||||
|
||||
const timerObj = reactive({
|
||||
// 设备状态统计
|
||||
deviceTotal: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: null,
|
||||
queryParams: {},
|
||||
},
|
||||
// 超市消费人数统计
|
||||
areaCameraTotal: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: null,
|
||||
queryParams: {
|
||||
dateRangeType: "YESTERDAY"
|
||||
},
|
||||
},
|
||||
// 超市购买实时统计
|
||||
shopOrder: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: {},
|
||||
queryParams: {
|
||||
current: 1,
|
||||
size: 30,
|
||||
dateRangeType: "YESTERDAY",
|
||||
},
|
||||
},
|
||||
// 设备在线率趋势
|
||||
deviceOfflineRate: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: {
|
||||
list: [],
|
||||
labels: [],
|
||||
},
|
||||
queryParams: {
|
||||
},
|
||||
},
|
||||
// 各商户购买次数趋势统计
|
||||
deviceAlarmTrend: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: {
|
||||
list: [],
|
||||
labels: [],
|
||||
},
|
||||
queryParams: {},
|
||||
},
|
||||
// 今日消费次数排行(top5)
|
||||
topTotal: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
data: null,
|
||||
queryParams: {},
|
||||
},
|
||||
// 最近异常事件
|
||||
recentlyAlarm: {
|
||||
initial: null,
|
||||
interval: 5 * 1000,
|
||||
queryParams: {},
|
||||
data: null,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// getDeviceStatusData();
|
||||
// getDeviceOnlineData();
|
||||
getAreaCameraTotalData();
|
||||
getAlarmData();
|
||||
getAlarmListData();
|
||||
getTopTotalData();
|
||||
getNotification();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
notificationClear();
|
||||
for (let key of Object.keys(timerObj)) {
|
||||
const value = timerObj[key];
|
||||
if (value && value.initial) {
|
||||
value.initial.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 设备状态统计
|
||||
const getDeviceStatusData = () => {
|
||||
timerObj.deviceTotal.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await deviceApi.totalDevice({ ...timerObj.deviceTotal.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("设备状态统计:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const _array = Array(3).fill({});
|
||||
const _keyList = ["door", "gate", "camera"];
|
||||
for (let index = 0; index < _keyList.length; index++) {
|
||||
const target = data[_keyList[index]];
|
||||
if (!target) continue;
|
||||
_array[index] = {
|
||||
name: target.deviceName,
|
||||
total: target.total,
|
||||
type: target.deviceCategory,
|
||||
online: target.online,
|
||||
onlineRate: target.onlineRate,
|
||||
};
|
||||
}
|
||||
timerObj.deviceTotal.data = _array;
|
||||
} catch (error) {
|
||||
console.error("|设备状态统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.deviceTotal.interval);
|
||||
};
|
||||
|
||||
// 设备在线率趋势 (近7日)
|
||||
const getDeviceOnlineData = async () => {
|
||||
timerObj.deviceOfflineRate.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await deviceApi.deviceOfflineRate({ ...timerObj.deviceOfflineRate.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("设备在线率趋势:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
timerObj.deviceOfflineRate.data.list = data.map((v) => {
|
||||
return v.onlineDeviceTotalRate;
|
||||
});
|
||||
timerObj.deviceOfflineRate.data.labels = data.map((v) => {
|
||||
return v.recordTime;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("|设备在线率趋势|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.deviceOfflineRate.interval);
|
||||
};
|
||||
|
||||
// 今日消费次数排行(top5)
|
||||
const getTopTotalData = () => {
|
||||
timerObj.topTotal.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await shopApi.mchPayCountTop({ ...timerObj.topTotal.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("今日消费次数排行:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
timerObj.topTotal.data = {
|
||||
labels:
|
||||
data
|
||||
?.map((v) => {
|
||||
return v.mchName;
|
||||
})
|
||||
.reverse() ?? [],
|
||||
values:
|
||||
data
|
||||
?.map((v) => {
|
||||
return v.buyCount;
|
||||
})
|
||||
.reverse() ?? [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("|今日消费次数排行|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.topTotal.interval);
|
||||
};
|
||||
|
||||
// 超市消费人数统计
|
||||
const getAreaCameraTotalData = () => {
|
||||
timerObj.areaCameraTotal.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await shopApi.mchPayPersonCount({ ...timerObj.areaCameraTotal.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("超市消费人数统计:", data);
|
||||
if (!data || !data.length) {
|
||||
return;
|
||||
}
|
||||
timerObj.areaCameraTotal.data =
|
||||
data.map((v) => {
|
||||
return {
|
||||
title: v.mchName,
|
||||
type: v.mchId,
|
||||
total: v.personCount,
|
||||
};
|
||||
}) ?? [];
|
||||
} catch (error) {
|
||||
console.error("|超市消费人数统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.areaCameraTotal.interval);
|
||||
};
|
||||
|
||||
const alarmTotalByTimerChnageDateType = async (type) => {
|
||||
timerObj.areaCameraTotal.queryParams.dateRangeType = type;
|
||||
timerObj.areaCameraTotal.initial.executeOnce();
|
||||
}
|
||||
|
||||
// 各商户购买次数趋势统计
|
||||
const getAlarmData = async () => {
|
||||
timerObj.deviceAlarmTrend.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await shopApi.mchPayCountTrend({ ...timerObj.deviceAlarmTrend.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("各商户购买次数趋势统计:", data);
|
||||
if (!data || !data.length) {
|
||||
return;
|
||||
}
|
||||
timerObj.deviceAlarmTrend.data = data;
|
||||
} catch (error) {
|
||||
console.error("|各商户购买次数趋势统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.deviceAlarmTrend.interval);
|
||||
};
|
||||
|
||||
// 超市购买实时统计
|
||||
const getAlarmListData = () => {
|
||||
timerObj.shopOrder.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await shopApi.newPage({ ...timerObj.shopOrder.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("超市购买实时统计:", data);
|
||||
if (!data || !data.records || !data.records.length) {
|
||||
timerObj.shopOrder.data = [];
|
||||
return;
|
||||
}
|
||||
timerObj.shopOrder.data = data.records.map((v) => {
|
||||
return {
|
||||
tag: v.merchantName,
|
||||
name: `${v.productDesc},共${v.payPrice}元`,
|
||||
time: v.createTime,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("|超市购买实时统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.shopOrder.interval);
|
||||
};
|
||||
|
||||
const alarmPageDateTypeChange = (type) => {
|
||||
timerObj.shopOrder.queryParams.dateRangeType = type;
|
||||
timerObj.shopOrder.initial.executeOnce();
|
||||
};
|
||||
|
||||
// 最近用超市购买数据
|
||||
const getNotification = () => {
|
||||
timerObj.recentlyAlarm.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await shopApi.recentlyShop({ ...timerObj.recentlyAlarm.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("最近用超市购买数据:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
notificationAdd({
|
||||
title: "最近用超市购买数据",
|
||||
content: {
|
||||
tag: data.merchantName,
|
||||
channelName: `${data.productDesc},${displayPrice(data.payPrice)}`,
|
||||
createTime: `${data.createTime}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("|最近用超市购买数据|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.recentlyAlarm.interval);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
color: #fff;
|
||||
|
||||
.aside {
|
||||
height: 100%;
|
||||
width: 24%;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
margin: 0 1vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<el-aside class="aside">
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>私家车数据统计</span>
|
||||
</template>
|
||||
<CarStatus :data="timerObj.carTotal.data" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>军车出车情况统计</span>
|
||||
</template>
|
||||
<CarGoOutStatus :total="timerObj.carTotal.data?.armyCar" :data="timerObj.armyCarCountByDateType.data" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>军车出车情况排行</span>
|
||||
</template>
|
||||
<CarOutQuantity :data="timerObj.armyCarCountByCarNum.data" @changeDateType="armyCarCountByCarNumChnageDateType" />
|
||||
</AsideBlock>
|
||||
</el-aside>
|
||||
|
||||
<div class="main">
|
||||
<AsideBlock height="calc(100% * 2 / 3)" titleBgWidth="50%">
|
||||
<template #header>
|
||||
<span>实时视频</span>
|
||||
</template>
|
||||
<DeviceVideoV2 ref="deviceVideoV2Ref" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock titleBgWidth="50%">
|
||||
<template #header>
|
||||
<span>车辆异常出入趋势统计</span>
|
||||
</template>
|
||||
<CarAbnormalThroughLineChart :data="timerObj.enterOrExitTotal.data" />
|
||||
</AsideBlock>
|
||||
</div>
|
||||
|
||||
<el-aside class="aside">
|
||||
<AsideBlock>
|
||||
<template #header>
|
||||
<span>车辆异常出入类型统计</span>
|
||||
</template>
|
||||
<AbnormalEventTypeStatus type="car" :data="timerObj.carAlarmTotal.data" @changeDateType="carAlarmChnageDateType" />
|
||||
</AsideBlock>
|
||||
|
||||
<AsideBlock height="calc(100% * 2 / 3)">
|
||||
<template #header>
|
||||
<span>车辆异常出入实时统计</span>
|
||||
</template>
|
||||
<AbnormalEventList type="car" :data="timerObj.carAlarmPage.data" @changeDateType="carAlarmPageChnageDateType" />
|
||||
</AsideBlock>
|
||||
</el-aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AsideBlock from "@/components/aside-block.vue";
|
||||
import CarStatus from "@/components/car-status.vue";
|
||||
import CarGoOutStatus from "@/components/car-go-out-status.vue";
|
||||
import CarAbnormalThroughLineChart from "@/components/car-abnormal-through-line-chart.vue";
|
||||
import DeviceVideoV2 from "@/components/device-video.vue";
|
||||
import CarOutQuantity from "@/components/car-out-quantity.vue";
|
||||
import AbnormalEventTypeStatus from "@/components/abnormal-event-type-status.vue";
|
||||
import AbnormalEventList from "@/components/abnormal-event-list.vue";
|
||||
import { onMounted, onBeforeUnmount, reactive, ref } from "vue";
|
||||
import useNotification from "@/components/notification/index";
|
||||
|
||||
const { notificationAdd, notificationClear } = useNotification();
|
||||
|
||||
const deviceVideoV2Ref = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
groupId: null,
|
||||
carStatusData: [0, 0, 0],
|
||||
carOutQuantityData: {},
|
||||
});
|
||||
|
||||
const timerObj = reactive({
|
||||
// 车辆异常出入类型统计
|
||||
carAlarmTotal: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {
|
||||
type: "YESTERDAY",
|
||||
},
|
||||
data: null,
|
||||
},
|
||||
// 车辆异常出入实时统计
|
||||
carAlarmPage: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {
|
||||
type: "YESTERDAY",
|
||||
current: 1,
|
||||
size: 30,
|
||||
},
|
||||
data: null,
|
||||
},
|
||||
// 车辆异常出入趋势统计
|
||||
enterOrExitTotal: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {},
|
||||
data: null,
|
||||
},
|
||||
// 军车出车情况排行
|
||||
armyCarCountByCarNum: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {
|
||||
type: "YESTERDAY",
|
||||
},
|
||||
data: null,
|
||||
},
|
||||
// 军车出车情况统计
|
||||
armyCarCountByDateType: {
|
||||
initial: null,
|
||||
interval: 10 * 1000,
|
||||
queryParams: {},
|
||||
data: {
|
||||
reignTotal: 0,
|
||||
unReignTotal: 0,
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
// 最近报警
|
||||
recentlyAlarm: {
|
||||
initial: null,
|
||||
interval: 5 * 1000,
|
||||
queryParams: {},
|
||||
data: null,
|
||||
},
|
||||
// 车辆统计
|
||||
carTotal: {
|
||||
initial: null,
|
||||
interval: 5 * 1000,
|
||||
queryParams: {
|
||||
inType: "TODAY",
|
||||
},
|
||||
data: {
|
||||
carManage: {},
|
||||
selfCar: {},
|
||||
foreignCar: {},
|
||||
armyCar: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// getCarStatusData();
|
||||
// getOutStatusData();
|
||||
// getCarOutQuantityData();
|
||||
// getCarAbnormalThroughData();
|
||||
// getAbnormalEventTypeStatusData();
|
||||
// getAbnormalList();
|
||||
// getNotification();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
notificationClear();
|
||||
for (let key of Object.keys(timerObj)) {
|
||||
const value = timerObj[key];
|
||||
if (value && value.initial) {
|
||||
value.initial.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const groupChange = (e) => {
|
||||
if (!e) return;
|
||||
state.groupId = e;
|
||||
deviceVideoV2Ref.value.open({ groupId: state.groupId });
|
||||
};
|
||||
|
||||
// 车辆统计
|
||||
const getCarStatusData = () => {
|
||||
timerObj.carTotal.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await carApi.carTotal({ ...timerObj.carTotal.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("|车辆统计|:", data);
|
||||
timerObj.carTotal.data = data;
|
||||
} catch (error) {
|
||||
console.log("|车辆统计|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.carTotal.interval);
|
||||
};
|
||||
|
||||
// 军车出车情况统计
|
||||
const getOutStatusData = () => {
|
||||
timerObj.armyCarCountByDateType.initial = simplePoll(async () => {
|
||||
let reignTotal = 0;
|
||||
let unReignTotal = 0;
|
||||
let items = [];
|
||||
try {
|
||||
// 军车出车情况排行数组
|
||||
const { code: listCode, data: listData } = await carApi.armyCarCountByDateType({ ...timerObj.armyCarCountByDateType.queryParams });
|
||||
if (listCode != 0) throw new Error("code!=0");
|
||||
console.log("军车出车情况排行:", listData);
|
||||
if (!listData || !listData.length) {
|
||||
items = [];
|
||||
} else {
|
||||
items = listData.map((item) => ({
|
||||
type: item.dateType,
|
||||
typeName: item.dateTypeStr,
|
||||
outNumber: item.timeCount,
|
||||
outDuration: 0,
|
||||
}));
|
||||
}
|
||||
timerObj.armyCarCountByDateType.data = {
|
||||
reignTotal: reignTotal,
|
||||
unReignTotal: unReignTotal,
|
||||
items: items,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log("军车出车情况统计意外的错误:", error);
|
||||
}
|
||||
}, timerObj.armyCarCountByDateType.interval);
|
||||
};
|
||||
|
||||
// 军车出车情况排行
|
||||
const getCarOutQuantityData = () => {
|
||||
timerObj.armyCarCountByCarNum.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await carApi.armyCarCountByCarNum({ ...timerObj.armyCarCountByCarNum.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("军车出车情况排行:", data);
|
||||
if (!data || !data.length) {
|
||||
timerObj.armyCarCountByCarNum.data = [];
|
||||
return;
|
||||
}
|
||||
timerObj.armyCarCountByCarNum.data = data;
|
||||
} catch (error) {
|
||||
console.log("军车出车情况排行意外的错误:", error);
|
||||
}
|
||||
}, timerObj.armyCarCountByCarNum.interval);
|
||||
};
|
||||
|
||||
const armyCarCountByCarNumChnageDateType = (type) => {
|
||||
timerObj.armyCarCountByCarNum.queryParams.type = type;
|
||||
timerObj.armyCarCountByCarNum.initial.executeOnce();
|
||||
};
|
||||
|
||||
// 车辆异常出入趋势统计
|
||||
const getCarAbnormalThroughData = async () => {
|
||||
timerObj.enterOrExitTotal.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await carApi.enterOrExitTotal({ ...timerObj.enterOrExitTotal.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("车辆异常出入趋势统计:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
timerObj.enterOrExitTotal.data = {
|
||||
in: data.enterDetail,
|
||||
out: data.exitDetail,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log("车辆异常出入趋势统计意外的错误:", error);
|
||||
}
|
||||
}, timerObj.enterOrExitTotal.interval);
|
||||
};
|
||||
|
||||
// 车辆异常出入类型统计
|
||||
const getAbnormalEventTypeStatusData = () => {
|
||||
timerObj.carAlarmTotal.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await carApi.carAlarmTotal({ ...timerObj.carAlarmTotal.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("车辆异常出入类型统计:", data);
|
||||
if (!data || !data.length) {
|
||||
return;
|
||||
}
|
||||
timerObj.carAlarmTotal.data = data.map((v) => {
|
||||
return {
|
||||
title: v.armyName,
|
||||
type: v.alarmType,
|
||||
total: v.carCount,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("车辆异常出入类型统计意外的错误:", error);
|
||||
}
|
||||
}, timerObj.carAlarmTotal.interval);
|
||||
};
|
||||
|
||||
const carAlarmChnageDateType = (e) => {
|
||||
timerObj.carAlarmTotal.queryParams.type = e;
|
||||
timerObj.carAlarmTotal.initial.executeOnce();
|
||||
};
|
||||
|
||||
// 车辆异常出入列表
|
||||
const getAbnormalList = () => {
|
||||
timerObj.carAlarmPage.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await carApi.carAlarmPage({ ...timerObj.carAlarmPage.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("车辆异常出入列表:", data);
|
||||
if (!data || !data.records || !data.records.length) {
|
||||
timerObj.carAlarmPage.data = [];
|
||||
return;
|
||||
}
|
||||
timerObj.carAlarmPage.data = data.records.map((v) => {
|
||||
return {
|
||||
alarmList: v.alarmList,
|
||||
name: v.carNum,
|
||||
time: v.capTime,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("|车辆异常出入列表|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.carAlarmPage.interval);
|
||||
};
|
||||
|
||||
const carAlarmPageChnageDateType = (e) => {
|
||||
timerObj.carAlarmPage.queryParams.current = 1;
|
||||
timerObj.carAlarmPage.queryParams.type = e;
|
||||
timerObj.carAlarmPage.initial.executeOnce();
|
||||
};
|
||||
|
||||
// 最近报警
|
||||
const getNotification = () => {
|
||||
timerObj.recentlyAlarm.initial = simplePoll(async () => {
|
||||
try {
|
||||
const { code, data } = await carApi.recentlyAlarm({ ...timerObj.recentlyAlarm.queryParams });
|
||||
if (code != 0) throw new Error("code!=0");
|
||||
console.log("最近报警:", data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
notificationAdd({
|
||||
title: "报警信息",
|
||||
content: {
|
||||
pic: data.parseCarImgUrl,
|
||||
channelName: data.devChnName,
|
||||
createTime: `${data.carNum}-${data.capTime}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("|最近报警|意外的错误:", error);
|
||||
}
|
||||
}, timerObj.recentlyAlarm.interval);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
color: #fff;
|
||||
|
||||
.aside {
|
||||
height: 100%;
|
||||
width: 24%;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
margin: 0 1vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { loadEnv, defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig((mode) => {
|
||||
const env = loadEnv(mode.mode, process.cwd());
|
||||
|
||||
return {
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
[env.VITE_BASE_API]: {
|
||||
target: env.VITE_PROXY_PATH,
|
||||
ws: false,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(new RegExp("^" + env.VITE_BASE_API), ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
additionalData: '@import "@/global.less";',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||