首次提交

This commit is contained in:
Alatus Lee 2025-10-02 17:23:13 +08:00
commit adb06231ea
56 changed files with 34111 additions and 0 deletions

4
.env.development Normal file
View File

@ -0,0 +1,4 @@
# .env.development
NODE_ENV=development
VUE_APP_BASE_API=/dev-api
VUE_APP_TITLE=大屏数据可视化系统

3
.env.production Normal file
View File

@ -0,0 +1,3 @@
NODE_ENV=production
VUE_APP_BASE_API=http://your-ruoyi-server.com # 生产环境若依后端地址
VUE_APP_TITLE=大屏数据可视化系统

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

507
1.html Normal file
View File

@ -0,0 +1,507 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我是战斗人 - 精英战斗展示</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
:root {
--primary: #e63946;
--secondary: #f1faee;
--accent: #a8dadc;
--dark: #1d3557;
--darker: #0d1b2a;
}
body {
background: linear-gradient(135deg, var(--darker) 0%, var(--dark) 100%);
color: var(--secondary);
min-height: 100vh;
overflow-x: hidden;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 0;
border-bottom: 1px solid rgba(241, 250, 238, 0.1);
margin-bottom: 2rem;
}
.logo {
display: flex;
align-items: center;
gap: 0.8rem;
font-size: 1.8rem;
font-weight: 700;
color: var(--secondary);
text-shadow: 0 0 10px rgba(230, 57, 70, 0.5);
}
.logo i {
color: var(--primary);
}
nav ul {
display: flex;
list-style: none;
gap: 2rem;
}
nav a {
color: var(--secondary);
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
}
nav a:hover {
color: var(--primary);
}
nav a::after {
content: '';
position: absolute;
width: 0;
height: 2px;
bottom: -5px;
left: 0;
background-color: var(--primary);
transition: width 0.3s ease;
}
nav a:hover::after {
width: 100%;
}
.hero {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-bottom: 3rem;
}
.hero h1 {
font-size: 3.5rem;
margin-bottom: 1rem;
background: linear-gradient(to right, var(--primary), var(--accent));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.hero p {
font-size: 1.2rem;
max-width: 600px;
color: var(--accent);
margin-bottom: 2rem;
}
.video-container {
position: relative;
width: 100%;
max-width: 900px;
margin: 0 auto 3rem;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
transition: transform 0.3s ease;
}
.video-container:hover {
transform: translateY(-5px);
}
.video-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, rgba(29, 53, 87, 0.2), rgba(168, 218, 220, 0.1));
z-index: 1;
pointer-events: none;
}
video {
width: 100%;
display: block;
border-radius: 12px;
}
.video-controls {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 1rem;
background: linear-gradient(to top, rgba(13, 27, 42, 0.9), transparent);
display: flex;
justify-content: center;
gap: 1rem;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 2;
}
.video-container:hover .video-controls {
opacity: 1;
}
.control-btn {
background: rgba(241, 250, 238, 0.2);
border: none;
color: var(--secondary);
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
}
.control-btn:hover {
background: var(--primary);
transform: scale(1.1);
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.feature-card {
background: rgba(29, 53, 87, 0.4);
padding: 2rem;
border-radius: 10px;
text-align: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid rgba(168, 218, 220, 0.1);
backdrop-filter: blur(5px);
}
.feature-card:hover {
transform: translateY(-10px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
border-color: rgba(168, 218, 220, 0.3);
}
.feature-icon {
font-size: 2.5rem;
color: var(--primary);
margin-bottom: 1rem;
}
.feature-card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--accent);
}
.stats {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 2rem;
margin-bottom: 3rem;
padding: 2rem;
background: rgba(29, 53, 87, 0.3);
border-radius: 10px;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary);
margin-bottom: 0.5rem;
}
.stat-label {
color: var(--accent);
font-size: 1rem;
}
.cta {
text-align: center;
padding: 3rem 2rem;
background: linear-gradient(135deg, rgba(29, 53, 87, 0.6), rgba(13, 27, 42, 0.8));
border-radius: 12px;
margin-bottom: 2rem;
}
.cta h2 {
font-size: 2.2rem;
margin-bottom: 1rem;
color: var(--secondary);
}
.cta p {
max-width: 600px;
margin: 0 auto 2rem;
color: var(--accent);
}
.btn {
display: inline-block;
padding: 0.8rem 2rem;
background: var(--primary);
color: var(--secondary);
border: none;
border-radius: 50px;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
box-shadow: 0 4px 15px rgba(230, 57, 70, 0.3);
}
.btn:hover {
background: #c1121f;
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(230, 57, 70, 0.4);
}
footer {
text-align: center;
padding: 2rem 0;
border-top: 1px solid rgba(241, 250, 238, 0.1);
color: var(--accent);
font-size: 0.9rem;
}
.social-links {
display: flex;
justify-content: center;
gap: 1.5rem;
margin: 1.5rem 0;
}
.social-links a {
color: var(--accent);
font-size: 1.5rem;
transition: all 0.3s ease;
}
.social-links a:hover {
color: var(--primary);
transform: translateY(-3px);
}
@media (max-width: 768px) {
header {
flex-direction: column;
gap: 1rem;
}
nav ul {
gap: 1rem;
}
.hero h1 {
font-size: 2.5rem;
}
.features {
grid-template-columns: 1fr;
}
.stats {
flex-direction: column;
gap: 1.5rem;
}
}
/* 动画效果 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 1s ease forwards;
}
.delay-1 { animation-delay: 0.2s; }
.delay-2 { animation-delay: 0.4s; }
.delay-3 { animation-delay: 0.6s; }
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">
<i class="fas fa-fist-raised"></i>
<span>战斗精英</span>
</div>
<nav>
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">关于我们</a></li>
<li><a href="#">训练课程</a></li>
<li><a href="#">战斗展示</a></li>
<li><a href="#">联系我们</a></li>
</ul>
</nav>
</header>
<section class="hero">
<h1 class="fade-in">我是战斗人</h1>
<p class="fade-in delay-1">探索极限,挑战自我,体验战斗艺术的精髓。我们的训练结合传统与现代技术,打造最强大的战斗精英。</p>
</section>
<div class="video-container fade-in delay-2">
<video id="mainVideo" src="https://robotstorm.tech/_nuxt/videos/videoDemo.1589869.MP4" controls></video>
<div class="video-controls">
<button class="control-btn" id="playBtn"><i class="fas fa-play"></i></button>
<button class="control-btn" id="pauseBtn"><i class="fas fa-pause"></i></button>
<button class="control-btn" id="volumeUp"><i class="fas fa-volume-up"></i></button>
<button class="control-btn" id="volumeDown"><i class="fas fa-volume-down"></i></button>
<button class="control-btn" id="fullscreenBtn"><i class="fas fa-expand"></i></button>
</div>
</div>
<section class="features">
<div class="feature-card fade-in delay-1">
<div class="feature-icon">
<i class="fas fa-shield-alt"></i>
</div>
<h3>防御技巧</h3>
<p>学习最先进的防御技术,保护自己免受攻击。我们的课程涵盖各种场景下的防御策略。</p>
</div>
<div class="feature-card fade-in delay-2">
<div class="feature-icon">
<i class="fas fa-bolt"></i>
</div>
<h3>快速反应</h3>
<p>训练你的反应速度和决策能力,在关键时刻做出正确判断,掌握战斗主动权。</p>
</div>
<div class="feature-card fade-in delay-3">
<div class="feature-icon">
<i class="fas fa-users"></i>
</div>
<h3>团队协作</h3>
<p>学习如何在团队环境中协调行动,发挥集体力量,实现更高效的战斗配合。</p>
</div>
</section>
<section class="stats">
<div class="stat-item">
<div class="stat-number">500+</div>
<div class="stat-label">训练课程</div>
</div>
<div class="stat-item">
<div class="stat-number">10K+</div>
<div class="stat-label">满意学员</div>
</div>
<div class="stat-item">
<div class="stat-number">15</div>
<div class="stat-label">专业教练</div>
</div>
<div class="stat-item">
<div class="stat-number">98%</div>
<div class="stat-label">成功率</div>
</div>
</section>
<section class="cta fade-in delay-2">
<h2>准备好成为战斗精英了吗?</h2>
<p>加入我们,开启你的战斗之旅。无论你是初学者还是经验丰富的战士,我们都有适合你的课程。</p>
<a href="#" class="btn">立即加入</a>
</section>
<footer>
<div class="social-links">
<a href="#"><i class="fab fa-weibo"></i></a>
<a href="#"><i class="fab fa-weixin"></i></a>
<a href="#"><i class="fab fa-qq"></i></a>
<a href="#"><i class="fab fa-tiktok"></i></a>
</div>
<p>&copy; 2023 战斗精英 | 专业战斗训练平台</p>
</footer>
</div>
<script>
// 视频控制功能
const video = document.getElementById('mainVideo');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const volumeUp = document.getElementById('volumeUp');
const volumeDown = document.getElementById('volumeDown');
const fullscreenBtn = document.getElementById('fullscreenBtn');
playBtn.addEventListener('click', () => {
video.play();
});
pauseBtn.addEventListener('click', () => {
video.pause();
});
volumeUp.addEventListener('click', () => {
if (video.volume < 1) video.volume += 0.1;
});
volumeDown.addEventListener('click', () => {
if (video.volume > 0) video.volume -= 0.1;
});
fullscreenBtn.addEventListener('click', () => {
if (video.requestFullscreen) {
video.requestFullscreen();
} else if (video.webkitRequestFullscreen) {
video.webkitRequestFullscreen();
} else if (video.msRequestFullscreen) {
video.msRequestFullscreen();
}
});
// 添加滚动动画效果
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('fade-in');
}
});
}, observerOptions);
// 观察所有具有动画潜力的元素
document.querySelectorAll('.feature-card, .stat-item, .cta').forEach(el => {
observer.observe(el);
});
</script>
</body>
</html>

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

221
README.md Normal file
View File

@ -0,0 +1,221 @@
**通知最新的低代码大屏系统GoView已开源详见[https://gitee.com/MTrun/go-view](https://gitee.com/MTrun/go-view)**
<p align="center">
<img src="https://gitee.com/MTrun/go-view/raw/master/readme/logo-t-y.png" alt="go-view" />
</p>
**长期赞助商**
<div align="center">
<a href="http://www.ccflow.org/?from=vueBigScreenGitee" target="_blank">
<img src="https://gitee.com/dromara/go-view/raw/master/readme/sponsors/ccflow-banner.png" alt="go-view" style="width: 320px!important;" width="320px!important;" />
</a>
</div>
## 一、项目描述
- 一个基于 Vue、Datav、Echart 框架的 " **数据大屏项目** ",通过 Vue 组件实现数据动态刷新渲染,内部图表可实现自由替换。部分图表使用 DataV 自带组件,可进行更改,详情请点击下方 DataV 文档。
- [**Vue3 版本请点击这里查看,使用 Hooks+TypeScript 实现,全新内容等你探索!**](https://gitee.com/MTrun/vue-big-screen-plugin)
- [**React 版本请点击这里查看,全新界面超级好看!!!**](https://gitee.com/MTrun/react-big-screen)
- 项目需要全屏展示(按 F11
- 项目部分区域使用了全局注册方式,增加了打包体积,在实际运用中请使用 **按需引入**
- 拉取项目之后,建议按照自己的功能区域重命名文件,现以简单的位置进行区分。
- 项目环境Vue-cli-5.x、DataV-2.7.3、Echarts-4.6.0(如果5.x版本有问题请切换到4.x版本)、Webpack-4.0、Npm-9.x、Node-v18。
- 请拉取 master 分支的代码,其余分支是开发分支。
- 需要其它地图数据的,请查看我的其它项目(有一个地图合集)
友情链接:
1. [Vue 官方文档](https://cn.vuejs.org/v2/guide/instance.html)
2. [DataV 官方文档](http://datav.jiaminghi.com/guide/)
3. [echarts 实例](https://echarts.apache.org/examples/zh/index.html)[echarts API 文档](https://echarts.apache.org/zh/api.html#echarts)
项目展示
![项目展示](https://images.gitee.com/uploads/images/2020/1208/183608_b893a510_4964818.gif "20201208_221020.gif")
## 二、主要文件介绍
| 文件 | 作用/功能 |
| ------------------- | --------------------------------------------------------------------- |
| main.js | 主目录文件,引入 Echart/DataV 等文件 |
| utils | 工具函数与 mixins 函数等 |
| views/ index.vue | 项目主结构 |
| views/其余文件 | 界面各个区域组件(按照位置来命名) |
| assets | 静态资源目录,放置 logo 与背景图片 |
| assets / style.scss | 通用 CSS 文件,全局项目快捷样式调节 |
| assets / index.scss | Index 界面的 CSS 文件 |
| components/echart | 所有 echart 图表(按照位置来命名) |
| common/... | 全局封装的 ECharts 和 flexible 插件代码(适配屏幕尺寸,可定制化修改) |
## 三、使用介绍
### 启动项目
需要提前安装好 `nodejs``pnpm`,下载项目后在项目主目录下运行 `pnpm` 拉取依赖包。安装完依赖包之后然后使用 `vue-cli` 或者直接使用命令`npm run serve`,就可以启动项目,启动项目后需要手动全屏(按 F11。如果编译项目的时候提示没有 DataV 框架的依赖,输入 `npm install @jiaminghi/data-view` 或者 `yarn add @jiaminghi/data-view` 进行手动安装。
### 封装组件渲染图表
所有的 ECharts 图表都是基于 `common/echart/index.vue` 封装组件创建的,已经对数据和屏幕改动进行了监听,能够动态渲染图表数据和大小。在监听窗口小大的模块,使用了防抖函数来控制更新频率,节约浏览器性能。
项目配置了默认的 ECharts 图表样式,文件地址:`common/echart/theme.json`
封装的渲染图表组件支持传入以下参数,可根据业务需求自行添加/删除。
参数名称 | 类型 | 作用/功能 |
| -------------------| --------- | ------------------------------|
| id | String | 唯一 id渲染图表的节点非必填使用了 $el|
| className | String | class样式名称非必填 |
| options | Object | ECharts 配置(必填) |
| height | String | 图表高度(建议填) |
| width | String | 图表宽度(建议填) |
### 动态渲染图表
动态渲染图表案例为 `components` 目录下各个图表组件index 文件负责数据获取和处理chart 文件负责监听和数据渲染。
chart 文件的主要逻辑为:
```html
<template>
<div>
<Echart :options="options" id="id" height="height" width="width" ></Echart>
</div>
</template>
<script>
// 引入封装组件
import Echart from '@/common/echart'
export default {
// 定义配置数据
data(){ return { options: {}}},
// 声明组件
components: { Echart},
// 接收数据
props: {
cdata: {
type: Object,
default: () => ({})
},
},
// 进行监听,也可以使用 computed 计算属性实现此功能
watch: {
cdata: {
handler (newData) {
this.options ={
// 这里编写 ECharts 配置
}
},
// 立即监听
immediate: true,
// 深度监听
deep: true
}
}
};
</script>
```
### 复用图表组件
复用图表组件案例为中间部分的 `任务通过率与任务达标率` 模块,两个图表类似,区别在于颜色和主要渲染数据。只需要传入对应的唯一 id 和样式,然后在复用的组件 `components/echart/center/centerChartRate` 里进行接收并在对应位置赋值即可。
如:在调用处 `views/center.vue` 里去定义好数据并传入组件
```js
//组件调用
<span>今日任务通过率</span>
<centerChart :id="rate[0].id" :tips="rate[0].tips" :colorObj="rate[0].colorData" />
<span>今日任务达标率</span>
<centerChart :id="rate[1].id" :tips="rate[1].tips" :colorObj="rate[1].colorData" />
...
import centerChart from "@/components/echart/center/centerChartRate";
data() {
return {
rate: [
{
id: "centerRate1",
tips: 60,
...
},
{
id: "centerRate2",
tips: 40,
colorData: {
...
}
}
]
}
}
```
### 更换边框
边框是使用了 DataV 自带的组件,只需要去 views 目录下去寻找对应的位置去查找并替换就可以,具体的种类请去 DavaV 官网查看
如:
```html
<dv-border-box-1></dv-border-box-1>
<dv-border-box-2></dv-border-box-2>
<dv-border-box-3></dv-border-box-3>
```
### 更换图表
直接进入 `components/echart` 下的文件修改成你要的 echarts 模样,可以去[echarts 官方社区](https://gallery.echartsjs.com/explore.html#sort=rank~timeframe=all~author=all)里面查看案例。
### Mixins 解决自适应适配功能
使用 mixins 注入解决了界面大小变动图表自适应适配的功能,函数在 `utils/resizeMixins.js` 中,应用在 `common/echart/index.vue` 的封装渲染组件,主要是对 `this.chart` 进行了功能注入。
### 屏幕适配
1.5 版本项目放弃了 flexible 插件方案,将 rem 改回px使用更流程通用的 `css3scale` 缩放方案,通过 `ref` 指向 `views/index`,屏幕改变时缩放内容。项目的基准尺寸是 `1920px*1080px`,所以支持同比例屏幕 100% 填充,如果非同比例则会自动计算比例居中填充,不足的部分则留白。实现代码在 `src/utils/userDraw` ,如果有其它的适配方案,欢迎交流。
### 请求数据
现在的项目未使用前后端数据请求,建议使用 axios 进行数据请求,在 main.js 位置进行全局配置。
- axios 的 main.js 配置参考范例(因人而异)
```js
import axios from 'axios';
//把方法放到vue的原型上这样就可以全局使用了
Vue.prototype.$http = axios.create({
//设置20秒超时时间
timeout: 20000,
baseURL: 'http://172.0.0.1:80080', //这里写后端地址
});
```
## 四、更新情况
1. 增加了 Echart 组件复用的功能,如:中间任务达标率的两个百分比图使用的是同一个组件。
2. 修复了头部右侧的图案条不对称的问题。
3. 使用 Mixins 注入图表响应式代码scale方案之后无需使用
4. vue-awesome 改成按需引入的方式。
5. 封装渲染函数,抽离了数据使逻辑更加清晰。
6. 新增地图组件,并添加自动轮播功能
7. 将适配方案从 rem 改成 scale
## 五、反馈
QQ群二维码1032272034
<img src="public/QQ3.png" width="200px" />
## 六、相关大屏案例
(以下案例基于此项目二次开发):
1. 支持地图下钻https://gitee.com/memeda520/IofTV-Screen
![输入图片说明](public/other_image.png)
2. 重写结构支持响应式布局https://gitee.com/BigCatHome/koi-screen
![输入图片说明](public/other_image2.png)
## 七、其余
这个项目是个人的作品,难免会有问题和 BUG如果有问题请进行评论我也会尽力去更新自己也在前端学习的路上欢迎交流非常感谢

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

19108
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"version": "1.5.1",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@jiaminghi/data-view": "^2.7.3",
"@types/echarts": "^4.4.3",
"core-js": "^3.6.4",
"echarts": "^4.6.0",
"vue": "^2.6.11",
"vue-awesome": "^4.0.2",
"vue-router": "^3.1.5",
"vuex": "^3.1.2",
"axios": "^0.21.4",
"element-ui": "^2.15.13",
"file-saver": "^2.0.5"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.2.0",
"@vue/cli-plugin-eslint": "^4.2.0",
"@vue/cli-service": "^5.0.8",
"babel-eslint": "^10.0.3",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.1.2",
"sass": "^1.25.0",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}

9565
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/QQ3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

18
public/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>设备数据大屏可视化系统</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

BIN
public/other_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
public/other_image2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

14
src/App.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<style lang="scss" scoped>
#app {
width: 100vw;
height: 100vh;
background-color: #020308;
overflow: hidden;
}
</style>

81
src/api/data.js Normal file
View File

@ -0,0 +1,81 @@
import request from '@/utils/request'
// 获取设备总按摩时间统计
export function getDeviceTotalMassageTime(params) {
return request({
url: '/dev-api/device/massage/getDeviceTotalMassageTime',
method: 'get',
params: params
})
}
// 获取按摩头类型分布统计
export function getHeadTypeStats() {
return request({
url: '/dev-api/device/massage/headTypeDistribution',
method: 'get'
})
}
// 获取中心面板统计数据
export function getCenterInfoStats() {
return request({
url: '/dev-api/device/device/deviceCenter/deviceStats',
method: 'get'
})
}
// 获取设备使用时长统计
export function getDeviceUsageStats() {
return request({
url: '/dev-api/device/device/massageInfo/getDeviceUsageStats',
method: 'get'
})
}
// 获取仪表盘图表数据 - 新增接口
export function getDashboardChartData() {
return request({
url: '/dev-api/device/device/dashboard/chartData',
method: 'get'
})
}
// 获取设备趋势数据 - 新增接口
export function getDeviceTrendData() {
return request({
url: '/dev-api/device/device/dashboard/trendData',
method: 'get'
})
}
// 获取中心面板综合数据
export function getComprehensiveCenterStats() {
return request({
url: '/dev-api/device/device/deviceCenter/comprehensiveStats',
method: 'get'
})
}
// 获取设备使用分析数据
export function getDeviceUsageAnalysis() {
return request({
url: '/dev-api/device/device/deviceUsageAnalysis',
method: 'get'
})
}
// 获取雷达图统计数据
export function getRadarStats() {
return request({
url: '/dev-api/device/massage/radarStats',
method: 'get'
})
}
export function getDeviceCountByProvince() {
return request({
url: '/dev-api/device/device/getDeviceCountByProvince',
method: 'get'
})
}

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
src/assets/pageBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

View File

@ -0,0 +1,98 @@
// 颜色
$colors: (
"primary": #1A5CD7,
"info-1": #4394e4,
"info": #4b67af,
"white": #ffffff,
"light": #f9f9f9,
"grey-1": #999999,
"grey": #666666,
"dark-1": #5f5f5f,
"dark": #222222,
"black-1": #171823,
"black": #000000,
"icon": #5cd9e8
);
// 字体大小
$base-font-size: 0.2rem;
$font-sizes: (
xxs: 0.1,
//8px
xs: 0.125,
//10px
sm: 0.2875,
//12px
md: 0.1625,
//13px
lg: 0.175,
//14px
xl: 0.2,
//16px
xxl: 0.225,
//18px
xxxl: 0.25 //20px,,,,
);
// 宽高
.w-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
//flex
.d-flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-nowrap {
flex-wrap: nowrap;
}
$flex-jc: (
start: flex-start,
end: flex-end,
center: center,
between: space-between,
around: space-around,
evenly: space-evenly,
);
$flex-ai: (
start: flex-start,
end: flex-end,
center: center,
stretch: stretch,
);
.flex-1 {
flex: 1;
}
//.mt-1 => margin top
//spacing
$spacing-types: (
m: margin,
p: padding,
);
$spacing-directions: (
t: top,
r: right,
b: bottom,
l: left,
);
$spacing-base-size: 0.5rem;
$spacing-sizes: (
0: 0,
1: 0.5,
2: 1,
3: 1.5,
4: 2,
5: 2.5,
);

144
src/assets/scss/index.scss Normal file
View File

@ -0,0 +1,144 @@
#index {
color: #d3d6dd;
width: 1920px;
height: 1080px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: left top;
overflow: hidden;
.bg {
width: 100%;
height: 100%;
padding: 16px 16px 0 16px;
background-image: url("../assets/pageBg.png");
background-size: cover;
background-position: center center;
}
.host-body {
.dv-dec-10,
.dv-dec-10-s {
width: 33.3%;
height: 5px;
}
.dv-dec-10-s {
transform: rotateY(180deg);
}
.dv-dec-8 {
width: 200px;
height: 50px;
}
.title {
position: relative;
width: 500px;
text-align: center;
background-size: cover;
background-repeat: no-repeat;
.title-text {
font-size: 24px;
position: absolute;
bottom: 0;
left: 50%;
transform: translate(-50%);
}
.dv-dec-6 {
position: absolute;
bottom: -30px;
left: 50%;
width: 250px;
height: 8px;
transform: translate(-50%);
}
}
// 第二行
.aside-width {
width: 40%;
}
.react-r-s,
.react-l-s {
background-color: #0f1325;
}
// 平行四边形
.react-right {
&.react-l-s {
text-align: right;
width: 500px;
}
font-size: 18px;
width: 300px;
line-height: 50px;
text-align: center;
transform: skewX(-45deg);
.react-after {
position: absolute;
right: -25px;
top: 0;
height: 50px;
width: 50px;
background-color: #0f1325;
transform: skewX(45deg);
}
.text {
display: inline-block;
transform: skewX(45deg);
}
}
.react-left {
&.react-l-s {
width: 500px;
text-align: left;
}
font-size: 18px;
width: 300px;
height: 50px;
line-height: 50px;
text-align: center;
transform: skewX(45deg);
background-color: #0f1325;
.react-left {
position: absolute;
left: -25px;
top: 0;
height: 50px;
width: 50px;
background-color: #0f1325;
transform: skewX(-45deg);
}
.text {
display: inline-block;
transform: skewX(-45deg);
}
}
.body-box {
margin-top: 16px;
display: flex;
flex-direction: column;
//下方区域的布局
.content-box {
display: grid;
grid-template-columns: 2fr 3fr 5fr 3fr 2fr;
}
// 底部数据
.bottom-box {
margin-top: 10px;
display: grid;
grid-template-columns: repeat(2, 50%);
}
}
}
}

186
src/assets/scss/style.scss Normal file
View File

@ -0,0 +1,186 @@
@import "./variables";
// 全局样式
* {
margin: 0;
padding: 0;
list-style-type: none;
outline: none;
box-sizing: border-box;
}
html {
margin: 0;
padding: 0;
}
body {
font-family: Arial, Helvetica, sans-serif;
line-height: 1.2em;
background-color: #f1f1f1;
margin: 0;
padding: 0;
overflow: hidden;
}
a {
color: #343440;
text-decoration: none;
}
.clearfix {
&::after {
content: "";
display: table;
height: 0;
line-height: 0;
visibility: hidden;
clear: both;
}
}
//浮动
.float-r {
float: right;
}
//浮动
.float-l {
float: left;
}
// 字体加粗
.fw-b {
font-weight: bold;
}
//文章一行显示多余省略号显示
.title-item {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bg-color-black {
background-color: rgba(19, 25, 47, 0.6);
}
.bg-color-blue {
background-color: #1a5cd7;
}
.colorBlack {
color: #272727 !important;
&:hover {
color: #272727 !important;
}
}
.colorGrass {
color: #33cea0;
&:hover {
color: #33cea0 !important;
}
}
.colorRed {
color: #ff5722;
&:hover {
color: #ff5722 !important;
}
}
.colorText {
color: #d3d6dd !important;
&:hover {
color: #d3d6dd !important;
}
}
.colorBlue {
color: #257dff !important;
&:hover {
color: #257dff !important;
}
}
//颜色
@each $colorkey, $color in $colors {
.text-#{$colorkey} {
color: $color;
}
.bg-#{$colorkey} {
background-color: $color;
}
}
//对齐
@each $var in (left, center, right) {
.text-#{$var} {
text-align: $var !important;
}
}
//flex
@each $key, $value in $flex-jc {
.jc-#{$key} {
justify-content: $value;
}
}
@each $key, $value in $flex-ai {
.ai-#{$key} {
align-items: $value;
}
}
//字体
@each $fontkey, $fontvalue in $font-sizes {
.fs-#{$fontkey} {
font-size: $fontvalue * $base-font-size;
}
}
//.mt-1 => margin top
//spacing
@each $typekey, $type in $spacing-types {
//.m-1
@each $sizekey, $size in $spacing-sizes {
.#{$typekey}-#{$sizekey} {
#{$type}: $size * $spacing-base-size;
}
}
//.mx-1
@each $sizekey, $size in $spacing-sizes {
.#{$typekey}x-#{$sizekey} {
#{$type}-left: $size * $spacing-base-size;
#{$type}-right: $size * $spacing-base-size;
}
.#{$typekey}y-#{$sizekey} {
#{$type}-top: $size * $spacing-base-size;
#{$type}-bottom: $size * $spacing-base-size;
}
}
//.mt-1
@each $directionkey, $direction in $spacing-directions {
@each $sizekey, $size in $spacing-sizes {
.#{$typekey}#{$directionkey}-#{$sizekey} {
#{$type}-#{$direction}: $size * $spacing-base-size;
}
}
}
.#{$typekey} {
#{$type}: 0;
}
}

View File

@ -0,0 +1,66 @@
<template>
<div :id="id" :class="className" :style="{ height: height, width: width }" />
</template>
<script>
import tdTheme from './theme.json' //
import '../map/fujian.js'
export default {
name: 'echart',
props: {
className: {
type: String,
default: 'chart'
},
id: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '2.5rem'
},
options: {
type: Object,
default: ()=>({})
}
},
data () {
return {
chart: null
}
},
watch: {
options: {
handler (options) {
// trueechart
this.chart.setOption(options, true)
},
deep: true
}
},
mounted () {
this.$echarts.registerTheme('tdTheme', tdTheme); //
this.initChart();
},
beforeDestroy () {
this.chart.dispose()
this.chart = null
},
methods: {
initChart () {
// echart
this.chart = this.$echarts.init(this.$el, 'tdTheme')
this.chart.setOption(this.options, true)
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,490 @@
{
"color": [
"#2d8cf0",
"#19be6b",
"#ff9900",
"#E46CBB",
"#9A66E4",
"#ed3f14"
],
"backgroundColor": "rgba(0,0,0,0)",
"textStyle": {},
"title": {
"textStyle": {
"color": "#516b91"
},
"subtextStyle": {
"color": "#93b7e3"
}
},
"line": {
"itemStyle": {
"normal": {
"borderWidth": "2"
}
},
"lineStyle": {
"normal": {
"width": "2"
}
},
"symbolSize": "6",
"symbol": "emptyCircle",
"smooth": true
},
"radar": {
"itemStyle": {
"normal": {
"borderWidth": "2"
}
},
"lineStyle": {
"normal": {
"width": "2"
}
},
"symbolSize": "6",
"symbol": "emptyCircle",
"smooth": true
},
"bar": {
"itemStyle": {
"normal": {
"barBorderWidth": 0,
"barBorderColor": "#ccc"
},
"emphasis": {
"barBorderWidth": 0,
"barBorderColor": "#ccc"
}
}
},
"pie": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"scatter": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"boxplot": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"parallel": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"sankey": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"funnel": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"gauge": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"emphasis": {
"borderWidth": 0,
"borderColor": "#ccc"
}
}
},
"candlestick": {
"itemStyle": {
"normal": {
"color": "#edafda",
"color0": "transparent",
"borderColor": "#d680bc",
"borderColor0": "#8fd3e8",
"borderWidth": "2"
}
}
},
"graph": {
"itemStyle": {
"normal": {
"borderWidth": 0,
"borderColor": "#ccc"
}
},
"lineStyle": {
"normal": {
"width": 1,
"color": "#aaa"
}
},
"symbolSize": "6",
"symbol": "emptyCircle",
"smooth": true,
"color": [
"#2d8cf0",
"#19be6b",
"#f5ae4a",
"#9189d5",
"#56cae2",
"#cbb0e3"
],
"label": {
"normal": {
"textStyle": {
"color": "#eee"
}
}
}
},
"map": {
"itemStyle": {
"normal": {
"areaColor": "#f3f3f3",
"borderColor": "#516b91",
"borderWidth": 0.5
},
"emphasis": {
"areaColor": "rgba(165,231,240,1)",
"borderColor": "#516b91",
"borderWidth": 1
}
},
"label": {
"normal": {
"textStyle": {
"color": "#000"
}
},
"emphasis": {
"textStyle": {
"color": "rgb(81,107,145)"
}
}
}
},
"geo": {
"itemStyle": {
"normal": {
"areaColor": "#f3f3f3",
"borderColor": "#516b91",
"borderWidth": 0.5
},
"emphasis": {
"areaColor": "rgba(165,231,240,1)",
"borderColor": "#516b91",
"borderWidth": 1
}
},
"label": {
"normal": {
"textStyle": {
"color": "#000"
}
},
"emphasis": {
"textStyle": {
"color": "rgb(81,107,145)"
}
}
}
},
"categoryAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"textStyle": {
"color": "#fff"
}
},
"splitLine": {
"show": false,
"lineStyle": {
"color": [
"#eeeeee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.05)",
"rgba(200,200,200,0.02)"
]
}
}
},
"valueAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"textStyle": {
"color": "#fff"
}
},
"splitLine": {
"show": false,
"lineStyle": {
"color": [
"#eeeeee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.05)",
"rgba(200,200,200,0.02)"
]
}
}
},
"logAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"textStyle": {
"color": "#999999"
}
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eeeeee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.05)",
"rgba(200,200,200,0.02)"
]
}
}
},
"timeAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"textStyle": {
"color": "#999999"
}
},
"splitLine": {
"show": true,
"lineStyle": {
"color": [
"#eeeeee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(250,250,250,0.05)",
"rgba(200,200,200,0.02)"
]
}
}
},
"toolbox": {
"iconStyle": {
"normal": {
"borderColor": "#999"
},
"emphasis": {
"borderColor": "#666"
}
}
},
"legend": {
"textStyle": {
"color": "#fff"
}
},
"tooltip": {
"axisPointer": {
"lineStyle": {
"color": "#ccc",
"width": 1
},
"crossStyle": {
"color": "#ccc",
"width": 1
}
}
},
"timeline": {
"lineStyle": {
"color": "#8fd3e8",
"width": 1
},
"itemStyle": {
"normal": {
"color": "#8fd3e8",
"borderWidth": 1
},
"emphasis": {
"color": "#8fd3e8"
}
},
"controlStyle": {
"normal": {
"color": "#8fd3e8",
"borderColor": "#8fd3e8",
"borderWidth": 0.5
},
"emphasis": {
"color": "#8fd3e8",
"borderColor": "#8fd3e8",
"borderWidth": 0.5
}
},
"checkpointStyle": {
"color": "#8fd3e8",
"borderColor": "rgba(138,124,168,0.37)"
},
"label": {
"normal": {
"textStyle": {
"color": "#8fd3e8"
}
},
"emphasis": {
"textStyle": {
"color": "#8fd3e8"
}
}
}
},
"visualMap": {
"color": [
"#516b91",
"#59c4e6",
"#a5e7f0"
]
},
"dataZoom": {
"backgroundColor": "rgba(0,0,0,0)",
"dataBackgroundColor": "rgba(255,255,255,0.3)",
"fillerColor": "rgba(167,183,204,0.4)",
"handleColor": "#a7b7cc",
"handleSize": "100%",
"textStyle": {
"color": "#333"
}
},
"markPoint": {
"label": {
"normal": {
"textStyle": {
"color": "#eee"
}
},
"emphasis": {
"textStyle": {
"color": "#eee"
}
}
}
}
}

48
src/common/map/fujian.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,177 @@
<template>
<div>
<Echart
:options="options"
id="bottomLeftChart"
height="480px"
width="100%"
></Echart>
</div>
</template>
<script>
import Echart from '@/common/echart'
export default {
data () {
return {
options: {},
};
},
components: {
Echart,
},
props: {
cdata: {
type: Object,
default: () => ({})
},
},
watch: {
cdata: {
handler (newData) {
this.options = {
tooltip: {
trigger: "axis",
backgroundColor: "rgba(255,255,255,0.1)",
axisPointer: {
type: "shadow",
label: {
show: true,
backgroundColor: "#7B7DDC"
}
},
formatter: function(params) {
let result = params[0].name + '<br/>';
params.forEach(function(item) {
var value = item.value;
var seriesName = item.seriesName;
if (seriesName === '使用率') {
value = (value * 100).toFixed(1) + '%';
} else {
value = value + ' 分钟';
}
result += '<span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color:' + item.color + '"></span>' + seriesName + '' + value + '<br/>';
});
return result;
}
},
legend: {
data: ["实际使用时长", "计划使用时长", "使用率"],
textStyle: {
color: "#B4B4B4"
},
top: "0%"
},
grid: {
x: "8%",
width: "88%",
y: "4%"
},
xAxis: {
data: newData.category,
axisLine: {
lineStyle: {
color: "#B4B4B4"
}
},
axisTick: {
show: false
},
axisLabel: {
interval: 0, //
rotate: 45, // 45
textStyle: {
fontSize: 10
}
}
},
yAxis: [
{
type: 'value',
name: '使用时长(分钟)',
splitLine: { show: false },
axisLine: {
lineStyle: {
color: "#B4B4B4"
}
},
axisLabel: {
formatter: '{value}'
}
},
{
type: 'value',
name: '使用率',
splitLine: { show: false },
axisLine: {
lineStyle: {
color: "#B4B4B4"
}
},
axisLabel: {
formatter: function(value) {
return (value * 100).toFixed(0) + '%';
}
},
min: 0,
max: 1
}
],
series: [
{
name: "使用率",
type: "line",
smooth: true,
showAllSymbol: true,
symbol: "emptyCircle",
symbolSize: 8,
yAxisIndex: 1,
itemStyle: {
normal: {
color: "#F02FC2"
}
},
data: newData.rateData
},
{
name: "实际使用时长",
type: "bar",
barWidth: 10,
itemStyle: {
normal: {
barBorderRadius: 5,
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#956FD4" },
{ offset: 1, color: "#3EACE5" }
])
}
},
data: newData.barData
},
{
name: "计划使用时长",
type: "bar",
barGap: "-100%",
barWidth: 10,
itemStyle: {
normal: {
barBorderRadius: 5,
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(156,107,211,0.8)" },
{ offset: 0.2, color: "rgba(156,107,211,0.5)" },
{ offset: 1, color: "rgba(156,107,211,0.2)" }
])
}
},
z: -12,
data: newData.lineData
}
]
}
},
immediate: true,
deep: true
},
},
}
</script>

View File

@ -0,0 +1,58 @@
<template>
<div>
<Chart :cdata="cdata" />
</div>
</template>
<script>
import { getDeviceUsageStats } from '@/api/data'
import Chart from './chart.vue'
export default {
data () {
return {
cdata: {
category: [],
lineData: [],
barData: [],
rateData: []
}
};
},
components: {
Chart,
},
mounted () {
this.setData();
},
methods: {
setData () {
// 使
getDeviceUsageStats().then(response => {
console.log('接口响应:', response);
if (response.code === 200) {
console.log('实际数据:', response.data);
this.cdata = response.data;
} else {
console.error('获取设备使用统计失败:', response.msg);
this.setDefaultData();
}
}).catch(error => {
console.error('获取设备使用统计失败', error);
this.setDefaultData();
});
},
setDefaultData () {
//
this.cdata = {
category: ["设备1", "设备2", "设备3", "设备4", "设备5"],
lineData: [1000, 1200, 800, 1500, 900],
barData: [600, 800, 400, 1000, 500],
rateData: [0.60, 0.67, 0.50, 0.67, 0.56]
};
},
}
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,359 @@
<template>
<div>
<Echart
:options="options"
id="centreLeft1Chart"
height="480px"
width="100%"
></Echart>
</div>
</template>
<script>
import Echart from '@/common/echart'
export default {
data() {
return {
options: {},
//
colorList: {
linearYtoG: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 1,
colorStops: [
{
offset: 0,
color: '#f5b44d'
},
{
offset: 1,
color: '#28f8de'
}
]
},
linearGtoB: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{
offset: 0,
color: '#43dfa2'
},
{
offset: 1,
color: '#28f8de'
}
]
},
linearBtoG: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{
offset: 0,
color: '#1c98e8'
},
{
offset: 1,
color: '#28f8de'
}
]
},
areaBtoG: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(35,184,210,.2)'
},
{
offset: 1,
color: 'rgba(35,184,210,0)'
}
]
}
}
}
},
components: {
Echart
},
props: {
cdata: {
type: Object,
default: () => ({})
}
},
watch: {
cdata: {
handler(newData) {
this.options = {
title: {
text: '设备运行分析',
textStyle: {
color: '#D3D6DD',
fontSize: 24,
fontWeight: 'normal'
},
subtext: newData.year + '/' + newData.weekCategory[6],
subtextStyle: {
color: '#fff',
fontSize: 16
},
top: 50,
left: 80
},
legend: {
top: 120,
left: 80,
orient: 'vertical',
itemGap: 15,
itemWidth: 12,
itemHeight: 12,
data: ['设备平均', '当前设备'],
textStyle: {
color: '#fff',
fontSize: 14
}
},
tooltip: {
trigger: 'item'
},
radar: {
center: ['68%', '27%'],
radius: '40%',
name: {
color: '#fff',
fontSize: 12
},
splitNumber: 8,
axisLine: {
lineStyle: {
color: this.colorList.linearYtoG,
opacity: 0.6
}
},
splitLine: {
lineStyle: {
color: this.colorList.linearYtoG,
opacity: 0.6
}
},
splitArea: {
areaStyle: {
color: '#fff',
opacity: 0.1,
shadowBlur: 25,
shadowColor: '#000',
shadowOffsetX: 0,
shadowOffsetY: 5
}
},
indicator: [
{
name: '在线率',
max: 10000
},
{
name: '使用频率',
max: 10
},
{
name: '按摩时长',
max: 12
},
{
name: '设备评分',
max: 5
}
]
},
grid: {
left: 90,
right: 80,
bottom: 40,
top: '60%'
},
xAxis: {
type: 'category',
position: 'bottom',
axisLine: true,
axisLabel: {
color: 'rgba(255,255,255,.8)',
fontSize: 12
},
data: newData.weekCategory
},
// Y
yAxis: {
name: '按摩时长(分钟)',
nameLocation: 'end',
nameGap: 24,
nameTextStyle: {
color: 'rgba(255,255,255,.5)',
fontSize: 14
},
max: newData.maxData,
splitNumber: 4,
axisLine: {
lineStyle: {
opacity: 0
}
},
splitLine: {
show: true,
lineStyle: {
color: '#fff',
opacity: 0.1
}
},
axisLabel: {
color: 'rgba(255,255,255,.8)',
fontSize: 12
}
},
series: [
{
name: '',
type: 'radar',
symbolSize: 0,
data: [
{
value: newData.radarDataAvg && newData.radarDataAvg.length > 6 ? newData.radarDataAvg[6] : [5000, 7, 9, 3.2],
name: '设备平均',
itemStyle: {
normal: {
color: '#f8d351'
}
},
lineStyle: {
normal: {
opacity: 0
}
},
areaStyle: {
normal: {
color: '#f8d351',
shadowBlur: 25,
shadowColor: 'rgba(248,211,81,.3)',
shadowOffsetX: 0,
shadowOffsetY: -10,
opacity: 1
}
}
},
{
value: newData.radarData && newData.radarData.length > 6 ? newData.radarData[6] : [6000, 8, 10, 4.0],
name: '当前设备',
itemStyle: {
normal: {
color: '#43dfa2'
}
},
lineStyle: {
normal: {
opacity: 0
}
},
areaStyle: {
normal: {
color: this.colorList.linearGtoB,
shadowBlur: 15,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 5,
opacity: 0.8
}
}
}
]
},
{
name: '每日按摩时长',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
symbolSize: 8,
itemStyle: {
normal: {
color: '#fff'
}
},
lineStyle: {
normal: {
color: this.colorList.linearBtoG,
width: 3
}
},
areaStyle: {
normal: {
color: this.colorList.areaBtoG
}
},
data: newData.weekLineData || [],
lineSmooth: true,
markLine: {
silent: true,
data: [
{
type: 'average',
name: '平均值'
}
],
precision: 0,
label: {
normal: {
formatter: '平均值: \n {c}分钟'
}
},
lineStyle: {
normal: {
color: 'rgba(248,211,81,.7)'
}
}
},
tooltip: {
position: 'top',
formatter: '{c} 分钟',
backgroundColor: 'rgba(28,152,232,.2)',
padding: 6
}
},
{
name: '占位背景',
type: 'bar',
itemStyle: {
normal: {
show: true,
color: '#000',
opacity: 0
}
},
silent: true,
barWidth: '50%',
data: newData.weekMaxData || [],
animation: false
}
]
}
},
immediate: true,
deep: true
}
}
}
</script>

View File

@ -0,0 +1,110 @@
[file name]: index.vue
<template>
<div>
<Chart :cdata="cdata" />
</div>
</template>
<script>
import Chart from './chart.vue'
import { getDashboardChartData } from '@/api/data' //
export default {
data () {
return {
drawTiming: null,
cdata: {
year: null,
weekCategory: [],
radarData: [],
radarDataAvg: [],
maxData: 12000,
weekMaxData: [],
weekLineData: []
}
}
},
components: {
Chart,
},
mounted () {
this.drawTimingFn();
},
beforeDestroy () {
clearInterval(this.drawTiming);
},
methods: {
drawTimingFn () {
this.getDashboardData(); // 使
this.drawTiming = setInterval(() => {
this.getDashboardData();
}, 6000);
},
async getDashboardData() {
try {
const response = await getDashboardChartData();
if (response && response.code === 200) {
this.cdata = response.data;
console.log('仪表盘数据获取成功:', this.cdata);
} else {
console.error('获取仪表盘数据失败,使用模拟数据');
this.setData(); // 使
}
} catch (error) {
console.error('获取仪表盘数据异常,使用模拟数据:', error);
this.setData(); // 使
}
},
// setData
setData () {
//
this.cdata.weekCategory = [];
this.cdata.weekMaxData = [];
this.cdata.weekLineData = [];
this.cdata.radarData = [];
this.cdata.radarDataAvg = [];
let dateBase = new Date();
this.cdata.year = dateBase.getFullYear();
//
for (let i = 0; i < 7; i++) {
//
let date = new Date();
this.cdata.weekCategory.unshift([date.getMonth() + 1, date.getDate()-i].join("/"));
// 线
this.cdata.weekMaxData.push(this.cdata.maxData);
let distance = Math.round(Math.random() * 11000 + 500);
this.cdata.weekLineData.push(distance);
//
//
let averageSpeed = +(Math.random() * 5 + 3).toFixed(3);
let maxSpeed = averageSpeed + +(Math.random() * 3).toFixed(2);
let hour = +(distance / 1000 / averageSpeed).toFixed(1);
let radarDayData = [distance, averageSpeed, maxSpeed, hour];
this.cdata.radarData.unshift(radarDayData);
//
let distanceAvg = Math.round(Math.random() * 8000 + 4000);
let averageSpeedAvg = +(Math.random() * 4 + 4).toFixed(3);
let maxSpeedAvg = averageSpeedAvg + +(Math.random() * 2).toFixed(2);
let hourAvg = +(distance / 1000 / averageSpeed).toFixed(1);
let radarDayDataAvg = [
distanceAvg,
averageSpeedAvg,
maxSpeedAvg,
hourAvg
];
this.cdata.radarDataAvg.unshift(radarDayDataAvg);
}
}
}
};
</script>
<style lang="scss" scoped>
</style>
[file content end]

View File

@ -0,0 +1,104 @@
<template>
<div>
<!-- 通过率/达标率 -->
<Echart
:options="options"
:id="id"
height="100px"
width="100px"
></Echart>
</div>
</template>
<script>
import Echart from '@/common/echart'
export default {
data () {
return {
options: {},
};
},
components: {
Echart,
},
props: {
id: {
type: String,
required: true,
default: "chartRate"
},
tips: {
type: Number,
required: true,
default: 50
},
colorObj: {
type: Object,
default: function () {
return {
textStyle: "#3fc0fb",
series: {
color: ["#00bcd44a", "transparent"],
dataColor: {
normal: "#03a9f4",
shadowColor: "#97e2f5"
}
}
};
}
}
},
watch: {
// tips
tips: {
handler (newData) {
this.options = {
title:{
text: newData * 1 + "%",
x: "center",
y: "center",
textStyle: {
color: this.colorObj.textStyle,
fontSize: 16
}
},
series: [
{
type: "pie",
radius: ["75%", "80%"],
center: ["50%", "50%"],
hoverAnimation: false,
color: this.colorObj.series.color,
label: {
normal: {
show: false
}
},
data: [
{
value: newData,
itemStyle: {
normal: {
color: this.colorObj.series.dataColor.normal,
shadowBlur: 10,
shadowColor: this.colorObj.series.dataColor.shadowColor
}
}
},
{
value: 100 - newData
}
]
}
]
}
},
immediate: true,
deep: true
}
}
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,82 @@
<template>
<div>
<Echart
:options="options"
id="centreLeft1Chart"
height="220px"
width="260px"
></Echart>
</div>
</template>
<script>
import Echart from '@/common/echart'
export default {
data () {
return {
options: {},
};
},
components: {
Echart,
},
props: {
cdata: {
type: Object,
default: () => ({})
},
},
watch: {
cdata: {
handler (newData) {
this.options = {
color: [
"#37a2da",
"#32c5e9",
"#9fe6b8",
"#ffdb5c",
"#ff9f7f",
"#fb7293",
"#e7bcf3",
"#8378ea"
],
tooltip: {
trigger: "item",
// {a} {b}
formatter: "{b} : {c} ({d}%)"
},
toolbox: {
show: true
},
calculable: true,
legend: {
orient: "horizontal",
icon: "circle",
bottom: 0,
x: "center",
data: newData.xData,
textStyle: {
color: "#fff"
}
},
series: [
{
name: "通过率统计", // legend
type: "pie",
radius: [10, 50],
roseType: "area",
center: ["50%", "40%"],
data: newData.seriesData
}
]
}
},
immediate: true,
deep: true
}
}
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,86 @@
<template>
<div>
<Chart :cdata="cdata" />
</div>
</template>
<script>
import Chart from './chart.vue';
import { getHeadTypeStats } from '@/api/data.js'; // API
export default {
data () {
return {
cdata: {
xData: [],
seriesData: []
},
//
massageTypeMap: {
thermotherapy: '深部热疗',
shockwave: '点阵按摩',
ball: '全能滚珠',
touch: '触觉按摩',
finger: '指疗通络',
roller: '滚滚刺疗',
stone: '温砭舒揉',
ion: '离子光灸'
}
}
},
components: {
Chart,
},
mounted () {
this.loadChartData();
},
methods: {
//
transformData(apiData) {
const transformed = {
xData: [],
seriesData: []
};
// xDataseriesData
if (apiData.xData && apiData.seriesData) {
transformed.xData = apiData.xData.map(item => this.massageTypeMap[item] || item);
transformed.seriesData = apiData.seriesData.map(item => ({
...item,
name: this.massageTypeMap[item.name] || item.name
}));
}
return transformed;
},
async loadChartData() {
try {
const response = await getHeadTypeStats();
if (response.code === 200) {
//
this.cdata = this.transformData(response.data);
} else {
console.error('获取数据失败:', response.msg);
this.setDefaultData();
}
} catch (error) {
console.error('请求失败:', error);
this.setDefaultData();
}
},
setDefaultData() {
// 使
this.cdata = {
xData: ["默认数据1", "默认数据2", "默认数据3"],
seriesData: [
{ value: 30, name: "默认数据1" },
{ value: 20, name: "默认数据2" },
{ value: 50, name: "默认数据3" }
]
};
}
}
}
</script>

View File

@ -0,0 +1,359 @@
<template>
<div>
<Echart
id="centreLeft2Chart"
ref="centreLeft2ChartRef"
:options="options"
height="360px"
width="330px"
></Echart>
</div>
</template>
<script>
import Echart from '@/common/echart';
export default {
data() {
return {
options: {},
intervalId: null,
preSelectMapIndex: 0
};
},
components: {
Echart,
},
props: {
cdata: {
type: Array,
default: () => [],
},
},
watch: {
cdata: {
handler(newData) {
this.initMap(newData);
},
immediate: true,
deep: true,
},
},
methods: {
initMap(newData) {
console.log('接收到的地图数据:', newData); //
//
const geoCoordMap = {
'北京市': [116.405285, 39.904989],
'天津市': [117.190182, 39.125596],
'河北省': [114.502461, 38.045474],
'山西省': [112.549248, 37.857014],
'内蒙古自治区': [111.670801, 40.818311],
'辽宁省': [123.429096, 41.796767],
'吉林省': [125.3245, 43.886841],
'黑龙江省': [126.642464, 45.756967],
'上海市': [121.472644, 31.231706],
'江苏省': [118.767413, 32.041544],
'浙江省': [120.153576, 30.287459],
'安徽省': [117.283042, 31.86119],
'福建省': [119.306239, 26.075302],
'江西省': [115.892151, 28.676493],
'山东省': [117.000923, 36.675807],
'河南省': [113.665412, 34.757975],
'湖北省': [114.298572, 30.584355],
'湖南省': [112.982279, 28.19409],
'广东省': [113.280637, 23.125178],
'广西壮族自治区': [108.320004, 22.82402],
'海南省': [110.33119, 20.031971],
'重庆市': [106.504962, 29.533155],
'四川省': [104.065735, 30.659462],
'贵州省': [106.713478, 26.578343],
'云南省': [102.712251, 25.040609],
'西藏自治区': [91.132212, 29.660361],
'陕西省': [108.948024, 34.263161],
'甘肃省': [103.823557, 36.058039],
'青海省': [101.778916, 36.623178],
'宁夏回族自治区': [106.278179, 38.46637],
'新疆维吾尔自治区': [87.617733, 43.792818],
'台湾': [121.509062, 25.044332],
'香港': [114.173355, 22.320048],
'澳门': [113.54909, 22.198951]
};
// -
const processedData = (newData || []).map(item => {
//
return {
name: item.name,
value: item.value || 0,
onlineCount: item.onlineCount || 0,
offlineCount: item.offlineCount || 0
};
});
console.log('处理后的地图数据:', processedData); //
//
let convertData = (data) => {
let scatterData = [];
for (var i = 0; i < data.length; i++) {
if (!data[i] || !data[i].name) continue;
var geoCoord = geoCoordMap[data[i].name];
if (geoCoord) {
scatterData.push({
name: data[i].name,
value: geoCoord.concat(data[i].value || 0),
onlineCount: data[i].onlineCount || 0,
offlineCount: data[i].offlineCount || 0
});
} else {
console.warn(`未找到 ${data[i].name} 的坐标信息`);
}
}
return scatterData;
};
//
this.options = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00f2fc',
borderWidth: 1,
textStyle: {
fontSize: 12,
lineHeight: 18,
color: '#fff'
},
formatter: (params) => {
console.log('Tooltip params:', params); //
if (!params || !params.data) {
return '<div style="color: #999;">暂无数据</div>';
}
const data = params.data;
const name = data.name || '未知区域';
const total = data.value || 0;
const online = data.onlineCount || 0;
const offline = data.offlineCount || 0;
return `
<div style="font-size: 13px; color: #fff; margin-bottom: 4px;">
<strong>${name}</strong>
</div>
<div style="font-size: 12px; color: #ccc; margin-bottom: 2px;">
设备总数: <strong style="color: #ffd700">${total}</strong>
</div>
<div style="font-size: 12px; color: #19be6b; margin-bottom: 2px;">
在线设备: <strong>${online}</strong>
</div>
<div style="font-size: 12px; color: #ed3f14;">
离线设备: <strong>${offline}</strong>
</div>
`;
}
},
visualMap: {
min: 0,
max: this.getMaxValue(processedData),
show: true,
left: 'left',
top: 'bottom',
text: ['高', '低'],
seriesIndex: [0],
inRange: {
color: ['#e0f7ff', '#0066cc']
},
textStyle: {
color: '#fff'
}
},
geo: {
show: true,
map: 'china',
roam: false,
zoom: 1.2,
top: '10%',
left: '5%',
right: '5%',
label: {
emphasis: {
show: false
}
},
itemStyle: {
normal: {
areaColor: 'rgba(19, 54, 162, 0.5)',
borderColor: 'rgba(0, 242, 252, 0.3)',
borderWidth: 1
},
emphasis: {
areaColor: 'rgba(79, 127, 255, 0.8)',
borderColor: 'rgba(0, 242, 252, 0.6)',
borderWidth: 2
}
},
emphasis: {
itemStyle: {
areaColor: '#4f7fff'
},
label: {
show: false
}
}
},
series: [
{
// -
name: '设备数量',
type: 'map',
map: 'china', // 使 map mapType
geoIndex: 0,
data: processedData, // 使
emphasis: {
itemStyle: {
areaColor: '#4f7fff'
},
label: {
show: false
}
}
},
{
//
name: '散点效果',
type: 'effectScatter',
coordinateSystem: 'geo',
data: convertData(processedData),
symbolSize: function(val) {
return Math.max(6, Math.min(15, (val[2] || 0) / 10));
},
showEffectOn: 'render',
rippleEffect: {
brushType: 'stroke',
scale: 3,
period: 4
},
hoverAnimation: false,
itemStyle: {
color: '#99FBFE',
shadowBlur: 10,
shadowColor: '#fff'
},
zlevel: 1,
tooltip: {
show: false
}
}
]
};
console.log('ECharts 配置:', this.options); //
//
this.$nextTick(() => {
this.handleMapRandomSelect();
});
},
getMaxValue(data) {
if (!data || !Array.isArray(data) || data.length === 0) return 10;
const values = data.map(item => item.value || 0).filter(val => !isNaN(val));
return values.length > 0 ? Math.max(...values) : 10;
},
//
startInterval() {
const _self = this;
const time = 4000;
if (this.intervalId !== null) {
clearInterval(this.intervalId);
}
this.intervalId = setInterval(() => {
_self.reSelectMapRandomArea();
}, time);
},
//
reSelectMapRandomArea() {
const length = this.cdata ? this.cdata.length : 0;
if (length === 0) return;
this.$nextTick(() => {
try {
const map = this.$refs.centreLeft2ChartRef.chart;
let index = Math.floor(Math.random() * length);
while (index === this.preSelectMapIndex || index >= length) {
index = Math.floor(Math.random() * length);
}
//
map.dispatchAction({
type: 'highlight',
seriesIndex: 0, //
dataIndex: index,
});
// tooltip
map.dispatchAction({
type: 'showTip',
seriesIndex: 0, //
dataIndex: index,
});
this.preSelectMapIndex = index;
} catch (error) {
console.log('地图操作错误:', error)
}
});
},
handleMapRandomSelect() {
this.$nextTick(() => {
try {
const map = this.$refs.centreLeft2ChartRef.chart;
const _self = this;
setTimeout(() => {
_self.reSelectMapRandomArea();
}, 1000);
//
map.on('mouseover', { seriesIndex: 0 }, function (params) {
clearInterval(_self.intervalId);
//
map.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: params.dataIndex,
});
_self.preSelectMapIndex = params.dataIndex;
});
//
map.on('mouseout', { seriesIndex: 0 }, function () {
//
map.dispatchAction({
type: 'downplay',
seriesIndex: 0
});
_self.startInterval();
});
this.startInterval();
} catch (error) {
console.log('地图事件绑定错误:', error)
}
});
},
},
beforeDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
};
</script>

View File

@ -0,0 +1,83 @@
<template>
<div>
<Chart :cdata="cdata" />
</div>
</template>
<script>
import Chart from './chart.vue';
import { getDeviceCountByProvince } from '@/api/data.js'; //
export default {
data () {
return {
cdata: []
}
},
components: {
Chart,
},
mounted () {
this.loadChinaMapData();
},
methods: {
async loadChinaMapData() {
try {
//
const response = await getDeviceCountByProvince();
if (response.code === 200) {
this.cdata = response.data;
} else {
console.error('加载数据失败:', response.msg);
// 使
this.cdata = this.getMockData();
}
} catch (error) {
console.error('加载全国地图数据失败:', error);
// 使
this.cdata = this.getMockData();
}
},
getMockData() {
// -
return [
{ name: '北京市', value: 45 },
{ name: '天津市', value: 18 },
{ name: '河北省', value: 23 },
{ name: '山西省', value: 9 },
{ name: '内蒙古自治区', value: 6 },
{ name: '辽宁省', value: 20 },
{ name: '吉林省', value: 8 },
{ name: '黑龙江省', value: 15 },
{ name: '上海市', value: 42 },
{ name: '江苏省', value: 78 },
{ name: '浙江省', value: 89 },
{ name: '安徽省', value: 28 },
{ name: '福建省', value: 45 },
{ name: '江西省', value: 25 },
{ name: '山东省', value: 67 },
{ name: '河南省', value: 38 },
{ name: '湖北省', value: 35 },
{ name: '湖南省', value: 32 },
{ name: '广东省', value: 156 },
{ name: '广西壮族自治区', value: 14 },
{ name: '海南省', value: 8 },
{ name: '重庆市', value: 25 },
{ name: '四川省', value: 42 },
{ name: '贵州省', value: 10 },
{ name: '云南省', value: 12 },
{ name: '西藏自治区', value: 2 },
{ name: '陕西省', value: 18 },
{ name: '甘肃省', value: 7 },
{ name: '青海省', value: 3 },
{ name: '宁夏回族自治区', value: 4 },
{ name: '新疆维吾尔自治区', value: 5 }
];
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,132 @@
<template>
<div>
<Echart
:options="options"
id="centreRight2Chart1"
height="200px"
width="260px"
></Echart>
</div>
</template>
<script>
import Echart from '@/common/echart'
export default {
data () {
return {
options: {},
};
},
components: {
Echart,
},
props: {
cdata: {
type: Object,
default: () => ({})
},
},
watch: {
cdata: {
handler (newData) {
//
let lineStyle = {
normal: {
width: 1,
opacity: 0.5
}
};
this.options = {
radar: {
indicator: newData.indicatorData,
shape: "circle",
splitNumber: 5,
radius: ["0%", "65%"],
name: {
textStyle: {
color: "rgb(238, 197, 102)"
}
},
splitLine: {
lineStyle: {
color: [
"rgba(238, 197, 102, 0.1)",
"rgba(238, 197, 102, 0.2)",
"rgba(238, 197, 102, 0.4)",
"rgba(238, 197, 102, 0.6)",
"rgba(238, 197, 102, 0.8)",
"rgba(238, 197, 102, 1)"
].reverse()
}
},
splitArea: {
show: false
},
axisLine: {
lineStyle: {
color: "rgba(238, 197, 102, 0.5)"
}
}
},
series: [
{
name: "北京",
type: "radar",
lineStyle: lineStyle,
data: newData.dataBJ,
symbol: "none",
itemStyle: {
normal: {
color: "#F9713C"
}
},
areaStyle: {
normal: {
opacity: 0.1
}
}
},
{
name: "上海",
type: "radar",
lineStyle: lineStyle,
data: newData.dataSH,
symbol: "none",
itemStyle: {
normal: {
color: "#B3E4A1"
}
},
areaStyle: {
normal: {
opacity: 0.05
}
}
},
{
name: "广州",
type: "radar",
lineStyle: lineStyle,
data: newData.dataGZ,
symbol: "none",
itemStyle: {
normal: {
color: "rgb(238, 197, 102)"
}
},
areaStyle: {
normal: {
opacity: 0.05
}
}
} //end
]
}
},
immediate: true,
deep: true
}
}
};
</script>

View File

@ -0,0 +1,61 @@
<template>
<div>
<Chart :cdata="cdata" />
</div>
</template>
<script>
import Chart from './chart.vue';
import { getRadarStats } from '@/api/data'; //
export default {
data () {
return {
cdata: {
indicatorData: [],
dataBJ: [],
dataSH: [],
dataGZ: []
}
}
},
components: {
Chart,
},
mounted() {
this.loadRadarData();
},
methods: {
async loadRadarData() {
try {
const response = await getRadarStats();
if (response.code === 200) {
this.cdata = response.data;
} else {
console.error('获取雷达图数据失败:', response.msg);
this.loadDefaultData();
}
} catch (error) {
console.error('获取雷达图数据异常:', error);
this.loadDefaultData();
}
},
loadDefaultData() {
//
this.cdata = {
indicatorData: [
{ name: "按摩次数", max: 100 },
{ name: "总时长(分)", max: 250 },
{ name: "按摩头类型", max: 5 },
{ name: "身体部位", max: 6 },
{ name: "平均时长", max: 200 },
{ name: "使用频率", max: 50 }
],
dataBJ: [[94, 69, 3, 4, 114, 39]],
dataSH: [[91, 45, 2, 3, 125, 23]],
dataGZ: [[84, 94, 4, 5, 140, 18]]
};
}
}
}
</script>

34
src/main.js Normal file
View File

@ -0,0 +1,34 @@
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import dataV from '@jiaminghi/data-view';
// 引入全局css
import './assets/scss/style.scss';
// 按需引入vue-awesome图标
import Icon from 'vue-awesome/components/Icon';
import 'vue-awesome/icons/chart-bar.js';
import 'vue-awesome/icons/chart-area.js';
import 'echarts/map/js/china.js'
import 'vue-awesome/icons/chart-pie.js';
import 'vue-awesome/icons/chart-line.js';
import 'vue-awesome/icons/align-left.js';
import ElementUI from 'element-ui'
//引入echart
//4.x 引用方式
import echarts from 'echarts'
//5.x 引用方式为按需引用
//希望使用5.x版本的话,需要在package.json中更新版本号,并切换引用方式
//import * as echarts from 'echarts'
Vue.prototype.$echarts = echarts
Vue.config.productionTip = false;
import 'echarts/map/js/china.js' // 确保这个路径正确
// 全局注册
Vue.component('icon', Icon);
Vue.use(dataV);
Vue.use(ElementUI);
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');

15
src/router/index.js Normal file
View File

@ -0,0 +1,15 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [{
path: '/',
name: 'index',
component: () => import('../views/index.vue')
}]
const router = new VueRouter({
routes
})
export default router

15
src/store/index.js Normal file
View File

@ -0,0 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})

36
src/utils/auth.js Normal file
View File

@ -0,0 +1,36 @@
// 认证相关工具函数 - 简化版本
// 获取token从若依系统共享
export function getToken() {
// 尝试从不同位置获取若依的token
return (
localStorage.getItem('Admin-Token') ||
sessionStorage.getItem('Admin-Token') ||
getCookie('Admin-Token') ||
''
)
}
// 设置token
export function setToken(token) {
localStorage.setItem('Admin-Token', token)
}
// 移除token
export function removeToken() {
localStorage.removeItem('Admin-Token')
sessionStorage.removeItem('Admin-Token')
deleteCookie('Admin-Token')
}
// Cookie操作辅助函数
function getCookie(name) {
const value = `; ${document.cookie}`
const parts = value.split(`; ${name}=`)
if (parts.length === 2) return parts.pop().split(';').shift()
return null
}
function deleteCookie(name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`
}

77
src/utils/cache.js Normal file
View File

@ -0,0 +1,77 @@
// 缓存工具 - 提供sessionStorage和localStorage的封装
export default {
// sessionStorage
session: {
// 设置sessionStorage
set(key, value) {
if (typeof value === 'object') {
value = JSON.stringify(value)
}
sessionStorage.setItem(key, value)
},
// 获取sessionStorage
get(key) {
const value = sessionStorage.getItem(key)
try {
return JSON.parse(value)
} catch (e) {
return value
}
},
// 获取sessionStorageJSON格式
getJSON(key) {
const value = sessionStorage.getItem(key)
try {
return JSON.parse(value)
} catch (e) {
return null
}
},
// 移除sessionStorage
remove(key) {
sessionStorage.removeItem(key)
},
// 清空sessionStorage
clear() {
sessionStorage.clear()
}
},
// localStorage
local: {
// 设置localStorage
set(key, value) {
if (typeof value === 'object') {
value = JSON.stringify(value)
}
localStorage.setItem(key, value)
},
// 获取localStorage
get(key) {
const value = localStorage.getItem(key)
try {
return JSON.parse(value)
} catch (e) {
return value
}
},
// 获取localStorageJSON格式
getJSON(key) {
const value = localStorage.getItem(key)
try {
return JSON.parse(value)
} catch (e) {
return null
}
},
// 移除localStorage
remove(key) {
localStorage.removeItem(key)
},
// 清空localStorage
clear() {
localStorage.clear()
}
}
}

57
src/utils/drawMixin.js Normal file
View File

@ -0,0 +1,57 @@
// 屏幕适配 mixin 函数
// * 默认缩放值
const scale = {
width: '1',
height: '1',
}
// * 设计稿尺寸px
const baseWidth = 1920
const baseHeight = 1080
// * 需保持的比例默认1.77778
const baseProportion = parseFloat((baseWidth / baseHeight).toFixed(5))
export default {
data() {
return {
// * 定时函数
drawTiming: null
}
},
mounted () {
this.calcRate()
window.addEventListener('resize', this.resize)
},
beforeDestroy () {
window.removeEventListener('resize', this.resize)
},
methods: {
calcRate () {
const appRef = this.$refs["appRef"]
if (!appRef) return
// 当前宽高比
const currentRate = parseFloat((window.innerWidth / window.innerHeight).toFixed(5))
if (appRef) {
if (currentRate > baseProportion) {
// 表示更宽
scale.width = ((window.innerHeight * baseProportion) / baseWidth).toFixed(5)
scale.height = (window.innerHeight / baseHeight).toFixed(5)
appRef.style.transform = `scale(${scale.width}, ${scale.height}) translate(-50%, -50%)`
} else {
// 表示更高
scale.height = ((window.innerWidth / baseProportion) / baseHeight).toFixed(5)
scale.width = (window.innerWidth / baseWidth).toFixed(5)
appRef.style.transform = `scale(${scale.width}, ${scale.height}) translate(-50%, -50%)`
}
}
},
resize () {
clearTimeout(this.drawTiming)
this.drawTiming = setTimeout(() => {
this.calcRate()
}, 200)
}
},
}

14
src/utils/errorCode.js Normal file
View File

@ -0,0 +1,14 @@
// 错误码映射
export default {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
'default': '系统未知错误,请反馈给管理员',
'500': '服务器内部错误',
'501': '网络未实现',
'502': '网络错误',
'503': '服务不可用',
'504': '网络超时',
'505': 'http版本不支持该请求',
'601': '警告信息'
}

51
src/utils/index.js Normal file
View File

@ -0,0 +1,51 @@
/**
* @param {Function} fn 防抖函数
* @param {Number} delay 延迟时间
*/
export function debounce(fn, delay) {
var timer;
return function () {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
/**
* @param {date} time 需要转换的时间
* @param {String} fmt 需要转换的格式 yyyy-MM-ddyyyy-MM-dd HH:mm:ss
*/
export function formatTime(time, fmt) {
if (!time) return '';
else {
const date = new Date(time);
const o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'H+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
S: date.getMilliseconds(),
};
if (/(y+)/.test(fmt))
fmt = fmt.replace(
RegExp.$1,
(date.getFullYear() + '').substr(4 - RegExp.$1.length)
);
for (const k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1
? o[k]
: ('00' + o[k]).substr(('' + o[k]).length)
);
}
}
return fmt;
}
}

74
src/utils/request.js Normal file
View File

@ -0,0 +1,74 @@
import axios from 'axios'
import { Notification, Message } from 'element-ui'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// 开发环境使用空baseURL让代理处理
baseURL: process.env.NODE_ENV === 'development' ? '' : process.env.VUE_APP_BASE_API,
timeout: 10000
})
// request拦截器
service.interceptors.request.use(config => {
const isToken = (config.headers || {}).isToken === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken()
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
const code = res.data.code || 200
const msg = errorCode[code] || res.data.msg || errorCode['default']
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
Message({
message: '认证失败,请确保已在若依系统登录',
type: 'error',
duration: 5000
})
return Promise.reject(new Error('认证失败'))
} else if (code === 500) {
console.log(res)
Message({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
Message({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg))
} else if (code !== 200) {
Notification.error({ title: msg })
return Promise.reject('error')
} else {
return Promise.resolve(res.data)
}
},
error => {
console.log('err' + error)
let { message } = error
if (message == "Network Error") {
message = "后端接口连接异常"
} else if (message.includes("timeout")) {
message = "系统接口请求超时"
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常"
}
Message({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)
export default service

33
src/utils/resizeMixin.js Normal file
View File

@ -0,0 +1,33 @@
// 混入代码 resize-mixins.js
// 改成 Scale 缩放之后,没有使用这个代码,但是保留
import { debounce } from '@/utils';
const resizeChartMethod = '$__resizeChartMethod';
export default {
data() {
// 在组件内部将图表 init 的引用映射到 chart 属性上
return {
chart: null,
};
},
created() {
window.addEventListener('resize', this[resizeChartMethod], false);
},
activated() {
// 防止 keep-alive 之后图表变形
if (this.chart) {
this.chart.resize()
}
},
beforeDestroy() {
window.removeEventListener('reisze', this[resizeChartMethod]);
},
methods: {
// 防抖函数来控制 resize 的频率
[resizeChartMethod]: debounce(function() {
if (this.chart) {
this.chart.resize();
}
}, 300),
},
};

40
src/utils/ruoyi.js Normal file
View File

@ -0,0 +1,40 @@
// 参数处理工具函数
/**
* 参数处理
* @param {*} params 参数
*/
export function tansParams(params) {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName]
const 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') {
const params = propName + '[' + key + ']'
const subPart = encodeURIComponent(params) + "="
result += subPart + encodeURIComponent(value[key]) + "&"
}
}
} else {
result += part + encodeURIComponent(value) + "&"
}
}
}
return result
}
/**
* 验证是否为blob格式
* @param {*} data
* @returns
*/
export function blobValidate(data) {
// 简单的blob验证
if (!data) return false
if (data instanceof Blob) return true
if (data.type && data.size !== undefined) return true
return false
}

52
src/views/bottomLeft.vue Normal file
View File

@ -0,0 +1,52 @@
<template>
<div id="bottomLeft">
<div class="bg-color-black">
<div class="d-flex pt-2 pl-2">
<span>
<icon name="chart-bar" class="text-icon"></icon>
</span>
<div class="d-flex">
<span class="fs-xl text mx-2">数据统计图</span>
</div>
</div>
<div>
<BottomLeftChart />
</div>
</div>
</div>
</template>
<script>
import BottomLeftChart from '@/components/echart/bottom/bottomLeftChart'
export default {
components: {
BottomLeftChart
}
}
</script>
<style lang="scss" scoped>
$box-height: 520px;
$box-width: 100%;
#bottomLeft {
padding: 20px 16px;
height: $box-height;
width: $box-width;
border-radius: 5px;
.bg-color-black {
height: $box-height - 35px;
border-radius: 10px;
}
.text {
color: #c3cbde;
}
.chart-box {
margin-top: 16px;
width: 170px;
height: 170px;
.active-ring-name {
padding-top: 10px;
}
}
}
</style>

60
src/views/bottomRight.vue Normal file
View File

@ -0,0 +1,60 @@
<template>
<div id="bottomRight">
<div class="bg-color-black">
<div class="d-flex pt-2 pl-2">
<span>
<icon name="chart-area" class="text-icon"></icon>
</span>
<div class="d-flex">
<span class="fs-xl text mx-2">工单修复以及满意度统计图</span>
<div class="decoration2">
<dv-decoration-2 :reverse="true" style="width:5px;height:6rem;" />
</div>
</div>
</div>
<div>
<BottomRightChart />
</div>
</div>
</div>
</template>
<script>
import BottomRightChart from "@/components/echart/bottom/bottomRightChart";
export default {
components: {
BottomRightChart
}
};
</script>
<style lang="scss" class>
$box-height: 520px;
$box-width: 100%;
#bottomRight {
padding: 14px 16px;
height: $box-height;
width: $box-width;
border-radius: 5px;
.bg-color-black {
height: $box-height - 30px;
border-radius: 10px;
}
.text {
color: #c3cbde;
}
//线
.decoration2 {
position: absolute;
right: 0.125rem;
}
.chart-box {
margin-top: 16px;
width: 170px;
height: 170px;
.active-ring-name {
padding-top: 10px;
}
}
}
</style>

335
src/views/center.vue Normal file
View File

@ -0,0 +1,335 @@
<template>
<div id="center">
<div class="up">
<div
class="bg-color-black item"
v-for="item in titleItem"
:key="item.title"
>
<p class="ml-3 colorBlue fw-b fs-xl">{{ item.title }}</p>
<div>
<dv-digital-flop
class="dv-dig-flop ml-1 mt-2 pl-3"
:config="item.number"
/>
</div>
</div>
</div>
<div class="down">
<div class="ranking bg-color-black">
<span>
<icon name="chart-pie" class="text-icon"></icon>
</span>
<span class="fs-xl text mx-2 mb-1 pl-3">设备使用排名榜</span>
<dv-scroll-ranking-board class="dv-scr-rank-board mt-1" :config="ranking" />
</div>
<div class="percent">
<div class="item bg-color-black">
<span>设备在线率</span>
<CenterChart
:id="rate[0].id"
:tips="rate[0].tips"
:colorObj="rate[0].colorData"
/>
</div>
<div class="item bg-color-black">
<span>设备使用率</span>
<CenterChart
:id="rate[1].id"
:tips="rate[1].tips"
:colorObj="rate[1].colorData"
/>
</div>
<div class="water">
<dv-water-level-pond class="dv-wa-le-po" :config="water" />
</div>
</div>
</div>
</div>
</template>
<script>
import CenterChart from '@/components/echart/center/centerChartRate'
import { getComprehensiveCenterStats } from '@/api/data'
export default {
data() {
return {
titleItem: [
{
title: '总设备数',
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}次',
style: {
fontSize: 26,
fill: '#3fc0fb'
}
}
},
{
title: '在线设备',
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}台',
style: {
fontSize: 26,
fill: '#67e0e3'
}
}
},
{
title: '今日按摩次数',
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}次',
style: {
fontSize: 26,
fill: '#ff9800'
}
}
},
{
title: '离线设备',
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}台',
style: {
fontSize: 26,
fill: '#f56c6c'
}
}
},
{
title: '本月按摩时长',
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}分钟',
style: {
fontSize: 26,
fill: '#67c23a'
}
}
},
{
title: '活跃设备',
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}台',
style: {
fontSize: 26,
fill: '#409eff'
}
}
}
],
ranking: {
data: [],
carousel: 'single',
unit: '次'
},
water: {
data: [0],
shape: 'roundRect',
formatter: '{value}%',
waveNum: 3
},
rate: [
{
id: 'centerRate1',
tips: 0,
colorData: {
textStyle: '#3fc0fb',
series: {
color: ['#00bcd44a', 'transparent'],
dataColor: {
normal: '#03a9f4',
shadowColor: '#97e2f5'
}
}
}
},
{
id: 'centerRate2',
tips: 0,
colorData: {
textStyle: '#67e0e3',
series: {
color: ['#faf3a378', 'transparent'],
dataColor: {
normal: '#ff9800',
shadowColor: '#fcebad'
}
}
}
}
]
}
},
components: {
CenterChart
},
created() {
this.loadCenterData()
},
methods: {
async loadCenterData() {
try {
const response = await getComprehensiveCenterStats()
if (response.code === 200) {
const data = response.data
// - 使
this.updateTitleItems(data)
// - 使
this.ranking = data.ranking
// - 线使
this.updateRateData(data)
// -
if (data.waterData) {
this.water = data.waterData
}
}
} catch (error) {
console.error('加载中心面板数据失败:', error)
// 使
this.setDefaultDeviceData()
}
},
updateTitleItems(data) {
//
if (data.titleItems && data.titleItems.length >= 6) {
this.titleItem = data.titleItems.map((item, index) => {
const config = {
number: [item.number?.number?.[0] || 0],
toFixed: 0,
textAlign: 'left',
content: '{nt}' + this.getUnitByIndex(index),
style: {
fontSize: 26,
fill: this.getColorByIndex(index)
}
}
return {
title: item.title,
number: config
}
})
}
},
updateRateData(data) {
// 线使
if (data.rateData && data.rateData.length >= 2) {
this.rate[0].tips = data.rateData[0].tips || 0 // 线
this.rate[1].tips = data.rateData[1].tips || 0 // 使
}
},
getUnitByIndex(index) {
const units = ['台', '台', '次', '台', '分钟', '台']
return units[index] || ''
},
getColorByIndex(index) {
const colors = ['#3fc0fb', '#67e0e3', '#ff9800', '#f56c6c', '#67c23a', '#409eff']
return colors[index] || '#3fc0fb'
},
setDefaultDeviceData() {
//
this.ranking.data = [
{ name: '设备A', value: 156 },
{ name: '设备B', value: 128 },
{ name: '设备C', value: 97 },
{ name: '设备D', value: 85 },
{ name: '设备E', value: 73 }
]
this.rate[0].tips = 75 // 线75%
this.rate[1].tips = 60 // 使60%
this.water.data = [68] // 68%
}
}
}
</script>
<style lang="scss" scoped>
#center {
display: flex;
flex-direction: column;
.up {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
.item {
border-radius: 6px;
padding-top: 8px;
margin-top: 8px;
width: 32%;
height: 70px;
.dv-dig-flop {
width: 150px;
height: 30px;
}
}
}
.down {
padding: 6px 4px;
padding-bottom: 0;
width: 100%;
display: flex;
height: 255px;
justify-content: space-between;
.bg-color-black {
border-radius: 5px;
}
.ranking {
padding: 10px;
width: 59%;
.dv-scr-rank-board {
height: 225px;
}
}
.percent {
width: 40%;
display: flex;
flex-wrap: wrap;
.item {
width: 50%;
height: 120px;
span {
margin-top: 8px;
font-size: 14px;
display: flex;
justify-content: center;
}
}
.water {
width: 100%;
.dv-wa-le-po {
height: 120px;
}
}
}
}
}
</style>

205
src/views/centerLeft1.vue Normal file
View File

@ -0,0 +1,205 @@
<template>
<div id="centerLeft1">
<div class="bg-color-black">
<div class="d-flex pt-2 pl-2">
<span>
<icon name="chart-bar" class="text-icon"></icon>
</span>
<div class="d-flex">
<span class="fs-xl text mx-2">设备运行统计</span>
<dv-decoration-3 class="dv-dec-3" />
</div>
</div>
<div class="d-flex jc-center">
<CenterLeft1Chart />
</div>
<!-- 4个主要的数据 -->
<div class="bottom-data">
<div
class="item-box mt-2"
v-for="(item, index) in numberData"
:key="index"
>
<div class="d-flex">
<span class="coin"></span>
<dv-digital-flop class="dv-digital-flop" :config="item.number" />
</div>
<p class="text" style="text-align: center;">
{{ item.text }}
<span class="colorYellow">({{ getUnit(item.type) }})</span>
</p>
</div>
</div>
</div>
</div>
</template>
<script>
import CenterLeft1Chart from '@/components/echart/centerLeft/centerLeft1Chart'
import { getCenterInfoStats } from '@/api/data'
export default {
data() {
return {
numberData: [
{
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}',
style: {
fontSize: 24
}
},
text: '今日按摩任务',
type: 'todayTaskCount'
},
{
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}',
style: {
fontSize: 24
}
},
text: '累计完成数量',
type: 'totalTaskCount'
},
{
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}',
style: {
fontSize: 24
}
},
text: '在线设备数',
type: 'onlineDeviceCount'
},
{
number: {
number: [0],
toFixed: 0,
textAlign: 'left',
content: '{nt}',
style: {
fontSize: 24
}
},
text: '离线设备数',
type: 'offlineDeviceCount'
}
]
}
},
components: {
CenterLeft1Chart
},
mounted() {
this.loadData()
this.changeTiming()
},
methods: {
getUnit(type) {
const units = {
todayTaskCount: '次',
totalTaskCount: '次',
onlineDeviceCount: '台',
offlineDeviceCount: '台'
}
return units[type] || '件'
},
changeTiming() {
setInterval(() => {
this.loadData()
}, 5000)
},
async loadData() {
try {
const response = await getCenterInfoStats()
if (response.code === 200) {
const data = response.data
console.log('获取到的数据:', data)
// 使
if (data) {
//
this.numberData.forEach((item) => {
if (data[item.type] !== undefined) {
item.number.number[0] = data[item.type]
}
})
//
this.numberData.forEach((item) => {
item.number = { ...item.number }
})
}
}
} catch (error) {
console.error('获取设备数据失败:', error)
}
}
}
}
</script>
<style lang="scss" scoped>
$box-width: 300px;
$box-height: 410px;
#centerLeft1 {
padding: 16px;
height: $box-height;
width: $box-width;
border-radius: 10px;
.bg-color-black {
height: $box-height - 30px;
border-radius: 10px;
}
.text {
color: #c3cbde;
}
.dv-dec-3 {
position: relative;
width: 100px;
height: 20px;
top: -3px;
}
.bottom-data {
.item-box {
& > div {
padding-right: 5px;
}
font-size: 14px;
float: right;
position: relative;
width: 50%;
color: #d3d6dd;
.dv-digital-flop {
width: 120px;
height: 30px;
}
//
.coin {
position: relative;
top: 6px;
font-size: 20px;
color: #ffc107;
}
.colorYellow {
color: yellowgreen;
}
p {
text-align: center;
}
}
}
}
</style>

59
src/views/centerLeft2.vue Normal file
View File

@ -0,0 +1,59 @@
<template>
<div id="centerLeft1">
<div class="bg-color-black">
<div class="d-flex pt-2 pl-2">
<span>
<icon name="chart-pie" class="text-icon"></icon>
</span>
<div class="d-flex">
<span class="fs-xl text mx-2">地图数据</span>
<dv-decoration-1 class="dv-dec-1" />
</div>
</div>
<div class="d-flex jc-center">
<CenterLeft2Chart />
</div>
</div>
</div>
</template>
<script>
import CenterLeft2Chart from "@/components/echart/centerLeft/centerLeft2Chart";
export default {
components: {
CenterLeft2Chart
},
};
</script>
<style lang="scss" scoped>
#centerLeft1 {
$box-width: 300px;
$box-height: 410px;
padding: 16px;
height: $box-height;
min-width: $box-width;
border-radius: 5px;
.bg-color-black {
height: $box-height - 30px;
border-radius: 10px;
}
.text {
color: #c3cbde;
}
.dv-dec-1 {
position: relative;
width: 100px;
height: 20px;
top: -3px;
}
.chart-box {
margin-top: 16px;
width: 170px;
height: 170px;
.active-ring-name {
padding-top: 10px;
}
}
}
</style>

127
src/views/centerRight1.vue Normal file
View File

@ -0,0 +1,127 @@
<template>
<div id="centerRight1">
<div class="bg-color-black">
<div class="d-flex pt-2 pl-2">
<span>
<icon name="chart-line" class="text-icon"></icon>
</span>
<div class="d-flex">
<span class="fs-xl text mx-2">设备按摩时长排行榜</span>
</div>
</div>
<div class="d-flex jc-center body-box">
<dv-scroll-board class="dv-scr-board" :config="config" />
</div>
</div>
</div>
</template>
<script>
import { getDeviceTotalMassageTime } from '@/api/data'
import { Message } from 'element-ui'
export default {
data() {
return {
config: {
header: ['排名', '设备名称', '总按摩时长(分钟)'],
data: [],
rowNum: 7,
headerHeight: 35,
headerBGC: '#0f1325',
oddRowBGC: '#0f1325',
evenRowBGC: '#171c33',
index: false, //
columnWidth: [60, 120, 120],
align: ['center']
},
loading: false
}
},
mounted() {
this.loadDeviceUsageData()
},
methods: {
async loadDeviceUsageData() {
this.loading = true
try {
console.log('开始请求设备数据...')
const response = await getDeviceTotalMassageTime()
console.log('API响应:', response)
if (response && response.code === 200) {
const deviceData = response.data || []
const tableData = this.formatDeviceData(deviceData)
this.config = {
...this.config,
data: tableData
}
console.log('设备按摩数据加载成功:', deviceData)
Message.success('数据加载成功')
} else {
console.error('API返回错误:', response)
Message.error('数据加载失败: ' + (response.msg || '未知错误'))
this.useMockData()
}
} catch (error) {
console.error('请求设备数据异常:', error)
Message.error('设备数据加载失败: ' + error.message)
this.useMockData()
} finally {
this.loading = false
}
},
formatDeviceData(deviceData) {
if (!deviceData || !Array.isArray(deviceData)) {
return []
}
const sortedData = deviceData.sort((a, b) => {
const timeA = a.totalMassageTime || a.duration || a.value || 0
const timeB = b.totalMassageTime || b.duration || b.value || 0
return timeB - timeA
})
return sortedData.map((item, index) => {
const deviceName = item.deviceName || item.name || item.deviceId || `设备${index + 1}`
const massageTime = item.totalMassageTime || item.duration || item.value || 0
const minutes = Math.round(massageTime)
return [
`${index + 1}`, //
deviceName,
`<span class='colorGrass'>${minutes}分钟</span>`
]
})
},
useMockData() {
const mockData = [
{ deviceName: '按摩设备A', totalMassageTime: 456 },
{ deviceName: '按摩设备B', totalMassageTime: 389 },
{ deviceName: '按摩设备C', totalMassageTime: 342 },
{ deviceName: '按摩设备D', totalMassageTime: 298 },
{ deviceName: '按摩设备E', totalMassageTime: 267 },
{ deviceName: '按摩设备F', totalMassageTime: 234 },
{ deviceName: '按摩设备G', totalMassageTime: 198 },
{ deviceName: '按摩设备H', totalMassageTime: 167 },
{ deviceName: '按摩设备I', totalMassageTime: 145 },
{ deviceName: '按摩设备J', totalMassageTime: 123 }
]
const tableData = this.formatDeviceData(mockData)
this.config = {
...this.config,
data: tableData
}
console.log('使用模拟数据:', mockData)
Message.info('使用模拟数据展示')
}
}
}
</script>

188
src/views/centerRight2.vue Normal file
View File

@ -0,0 +1,188 @@
<template>
<div id="centerRight2">
<div class="bg-color-black">
<div class="d-flex pt-2 pl-2">
<span>
<icon name="align-left" class="text-icon"></icon>
</span>
<span class="fs-xl text mx-2">设备使用分析</span>
</div>
<div class="d-flex ai-center flex-column body-box">
<!-- 胶囊图显示设备使用排名 -->
<dv-capsule-chart class="dv-cap-chart" :config="capsuleConfig" />
<!-- 雷达图显示设备多维度分析 -->
<centerRight2Chart1 :cdata="radarData" />
<!-- 统计指标 -->
<div class="stats-container">
<div class="stat-item">
<span class="stat-label">总设备数</span>
<span class="stat-value">{{ stats.totalDevices }}</span>
</div>
<div class="stat-item">
<span class="stat-label">在线率</span>
<span class="stat-value">{{ stats.onlineRate.toFixed(1) }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">今日任务</span>
<span class="stat-value">{{ stats.todayTasks }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import centerRight2Chart1 from '@/components/echart/centerRight/centerRightChart'
import { getDeviceUsageAnalysis } from '@/api/data'
export default {
data() {
return {
capsuleConfig: {
data: []
},
radarData: {
indicatorData: [],
dataBJ: [],
dataSH: [],
dataGZ: []
},
stats: {
totalDevices: 0,
onlineDevices: 0,
offlineDevices: 0,
onlineRate: 0,
totalTasks: 0,
todayTasks: 0,
avgTasksPerDevice: 0
}
}
},
components: { centerRight2Chart1 },
mounted() {
this.loadDeviceUsageData()
},
methods: {
async loadDeviceUsageData() {
try {
const response = await getDeviceUsageAnalysis()
const data = response.data
//
this.capsuleConfig = {
data: data.capsuleData
}
//
this.radarData = {
indicatorData: data.radarData.indicatorData,
dataBJ: [data.radarData.device1],
dataSH: [data.radarData.device2],
dataGZ: [data.radarData.device3]
}
//
this.stats = data.stats
} catch (error) {
console.error('加载设备使用分析数据失败:', error)
this.setDefaultData()
}
},
setDefaultData() {
this.capsuleConfig = {
data: [
{ name: '设备A', value: 167 },
{ name: '设备B', value: 123 },
{ name: '设备C', value: 98 },
{ name: '设备D', value: 87 },
{ name: '设备E', value: 76 }
]
}
this.radarData = {
indicatorData: [
{ name: "使用频率", max: 100 },
{ name: "运行时长", max: 100 },
{ name: "在线率", max: 100 },
{ name: "按摩次数", max: 100 },
{ name: "设备健康", max: 100 },
{ name: "任务效率", max: 100 }
],
dataBJ: [[85, 70, 90, 60, 75, 50]],
dataSH: [[70, 85, 65, 80, 60, 70]],
dataGZ: [[90, 60, 75, 70, 85, 55]]
}
this.stats = {
totalDevices: 50,
onlineDevices: 35,
offlineDevices: 15,
onlineRate: 70.0,
totalTasks: 1200,
todayTasks: 25,
avgTasksPerDevice: 24.0
}
}
}
}
</script>
<style lang="scss" scoped>
#centerRight2 {
$box-height: 410px;
$box-width: 340px;
padding: 5px;
height: $box-height;
width: $box-width;
border-radius: 5px;
.bg-color-black {
padding: 5px;
height: $box-height;
width: $box-width;
border-radius: 10px;
}
.text {
color: #c3cbde;
}
.body-box {
border-radius: 10px;
overflow: hidden;
.dv-cap-chart {
width: 100%;
height: 160px;
}
.stats-container {
display: flex;
justify-content: space-around;
padding: 10px;
width: 100%;
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
.stat-label {
font-size: 12px;
color: #c3cbde;
margin-bottom: 4px;
}
.stat-value {
font-size: 16px;
color: #ffd700;
font-weight: bold;
}
}
}
}
}
</style>

153
src/views/index.vue Normal file
View File

@ -0,0 +1,153 @@
<template>
<div id="index" ref="appRef">
<div class="bg">
<dv-loading v-if="loading">Loading...</dv-loading>
<div v-else class="host-body">
<div class="d-flex jc-center">
<dv-decoration-10 class="dv-dec-10" />
<div class="d-flex jc-center">
<dv-decoration-8 class="dv-dec-8" :color="decorationColor" />
<div class="title">
<span class="title-text">设备BI大数据看板</span>
<dv-decoration-6
class="dv-dec-6"
:reverse="true"
:color="['#50e3c2', '#67a1e5']"
/>
</div>
<dv-decoration-8
class="dv-dec-8"
:reverse="true"
:color="decorationColor"
/>
</div>
<dv-decoration-10 class="dv-dec-10-s" />
</div>
<!-- 第二行 -->
<div class="d-flex jc-between px-2">
<div class="d-flex aside-width">
<div class="react-left ml-4 react-l-s">
<span class="react-left"></span>
<span class="text">设备数据展示</span>
</div>
<div class="react-left ml-3">
<span class="text">设备使用情况展示</span>
</div>
</div>
<div class="d-flex aside-width">
<div class="react-right bg-color-blue mr-3">
<span class="text fw-b">设备管理BI数据大屏</span>
</div>
<div class="react-right mr-4 react-l-s">
<span class="react-after"></span>
<span class="text"
>{{ dateYear }} {{ dateWeek }} {{ dateDay }}</span
>
</div>
</div>
</div>
<div class="body-box">
<!-- 第三行数据 -->
<div class="content-box">
<div>
<dv-border-box-12>
<centerLeft1 />
</dv-border-box-12>
</div>
<div>
<dv-border-box-12>
<centerLeft2 />
</dv-border-box-12>
</div>
<!-- 中间 -->
<div>
<center />
</div>
<!-- 中间 -->
<div>
<centerRight2 />
</div>
<div>
<dv-border-box-13>
<centerRight1 />
</dv-border-box-13>
</div>
</div>
<!-- 第四行数据 -->
<div class="bottom-box">
<dv-border-box-13>
<bottomLeft />
</dv-border-box-13>
<dv-border-box-12>
<bottomRight />
</dv-border-box-12>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import drawMixin from "../utils/drawMixin";
import { formatTime } from '../utils/index.js'
import centerLeft1 from './centerLeft1'
import centerLeft2 from './centerLeft2'
import centerRight1 from './centerRight1'
import centerRight2 from './centerRight2'
import center from './center'
import bottomLeft from './bottomLeft'
import bottomRight from './bottomRight'
export default {
mixins: [ drawMixin ],
data() {
return {
timing: null,
loading: true,
dateDay: null,
dateYear: null,
dateWeek: null,
weekday: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
decorationColor: ['#568aea', '#000000']
}
},
components: {
centerLeft1,
centerLeft2,
centerRight1,
centerRight2,
center,
bottomLeft,
bottomRight
},
mounted() {
this.timeFn()
this.cancelLoading()
},
beforeDestroy () {
clearInterval(this.timing)
},
methods: {
timeFn() {
this.timing = setInterval(() => {
this.dateDay = formatTime(new Date(), 'HH: mm: ss')
this.dateYear = formatTime(new Date(), 'yyyy-MM-dd')
this.dateWeek = this.weekday[new Date().getDay()]
}, 1000)
},
cancelLoading() {
setTimeout(() => {
this.loading = false
}, 500)
}
}
}
</script>
<style lang="scss" scoped>
@import '../assets/scss/index.scss';
</style>

44
vue.config.js Normal file
View File

@ -0,0 +1,44 @@
const { defineConfig } = require('@vue/cli-service')
const path = require('path')
const resolve = dir => {
return path.join(__dirname, dir)
}
module.exports = defineConfig({
publicPath: './',
transpileDependencies: [],
chainWebpack: config => {
config.resolve.alias
.set('_c', resolve('src/components'))
},
// 修正 devServer 配置
devServer: {
port: 81,
host: 'localhost',
open: true,
// 添加禁用主机检查,避免 Invalid Host header 错误
allowedHosts: 'all',
// 添加更严格的代理配置
proxy: {
'/dev-api': {
target: 'http://localhost:8080', // 后端在8080端口
changeOrigin: true,
secure: false, // 如果后端是http需要这个
logLevel: 'debug', // 可以查看代理日志
pathRewrite: {
'^/dev-api': '' // 移除 /dev-api 前缀,直接转发到后端根路径
},
// 添加headers确保正确传递
headers: {
Connection: 'keep-alive'
}
}
}
},
outputDir: 'dist',
assetsDir: 'static'
})