代码提交

This commit is contained in:
lixiaobang 2025-06-10 09:41:33 +08:00
parent 01d8de7f11
commit 0f1740f301
29 changed files with 10912 additions and 10905 deletions

46
.gitignore vendored
View File

@ -1,23 +1,23 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
**/*.log
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
package-lock.json
yarn.lock
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
**/*.log
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
package-lock.json
yarn.lock

View File

@ -1,3 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

52
.vscode/settings.json vendored
View File

@ -1,26 +1,26 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"i18n-ally.localesPaths": ["src/assets/locales"]
}
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"i18n-ally.localesPaths": ["src/assets/locales"]
}

42
LICENSE
View File

@ -1,21 +1,21 @@
MIT License
Copyright (c) 2022 daidai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License
Copyright (c) 2022 daidai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

626
README.md
View File

@ -1,313 +1,313 @@
## 项目描述
[IofTV-Screen](https://gitee.com/daidaibg/IofTV-Screen/tree/main) 的 Vue3+vite版本
### 与vue2版本对比
#### 功能
功能采用与vue2版本相同功能
因为要与vue2版本相同功能有些组件不兼容vue3版本例如胶囊柱图数字滚动皆重新封装为组件整体来说功能属实相同。根据自己需求选择[vue2](#vue2版本地址)版本与[vue3](#本项目地址 vue3+vite)版本
#### 样式
进行微调,整体看着更加美观
- 项目需要全屏展示(按 F11
- 项目部分区域使用了全局注册方式,增加了打包体积,在实际运用中请使用 **按需引入**
- 项目环境Vite、Echarts、Npm、Nodeaxios,mock,vue3。
- 请拉取 master 分支的代码,其余分支是开发分支。
- 在项目public目录下存放地图数据合集根据地市编存放。
友情链接:
1. [Vue 官方文档](https://cn.vuejs.org/)
3. [echarts 实例](https://gitee.com/link?target=https%3A%2F%2Fecharts.apache.org%2Fexamples%2Fzh%2Findex.html)[echarts API 文档](https://gitee.com/link?target=https%3A%2F%2Fecharts.apache.org%2Fzh%2Fapi.html%23echarts)
4. [mock.js官网](http://mockjs.com/examples.html)
5. [axios官网](https://axios-http.com/)
**项目展示**
![项目展示](https://www.daidaibg.com/bigscreen/a-img/home.png)
### 项目预览地址
[https://www.daidaibg.com/bigscreen-vue3](https://www.daidaibg.com/bigscreen-vue3)
### 项目仓库地址
#### 本项目地址 vue3+vite
**github地址**
[https://github.com/daidaibg/IofTV-Screen-Vue3](https://github.com/daidaibg/IofTV-Screen-Vue3)
**Gitee地址**
[https://gitee.com/daidaibg/IofTV-Screen-Vue3](https://gitee.com/daidaibg/IofTV-Screen-Vue3)
#### vue2版本地址
**github地址**
[https://github.com/daidaibg/IofTV-Screen](https://github.com/daidaibg/IofTV-Screen)
**Gitee地址**
[https://gitee.com/daidaibg/IofTV-Screen](https://gitee.com/daidaibg/IofTV-Screen)
### 采用自适应组件方式,
### 滚动设置,自适应设置
项目中可以进行滚动配置,内容是否滚动
点击右上角设置按钮
![设置](https://www.daidaibg.com/bigscreen/a-img/setting.png)
可以进行以下配置,可以自行代码中进行修改或增加配置
![在这里插入图片描述](https://www.daidaibg.com/bigscreen/a-img/setting2.png)
## 2、主要文件介绍
| 文件 | 作用/功能 |
| ----------------- | ------------------------------------------------------------ |
| main.js | 主目录文件,引入 Echart/DataV 等文件 |
| utils | 工具函数与 mixins 函数等 |
| views/ home.vue | 项目主结构 |
| views/其余文件 | 界面各个区域组件(按照位置来命名) |
| assets | 静态资源目录,放置 logo 与背景图片 |
| assets / css/ | 通用 CSS 文件,全局项目快捷样式调节 |
| components/echart | 所有 echart 图表(按照位置来命名) |
| common/... | 全局封装的 ECharts 和 flexible 插件代码(适配屏幕尺寸,可定制化修改) |
| api/api.js | 接口封装文件 |
| mock | 模拟数据接口地址 |
###
## 使用介绍
### 安装
```npm
npm install
```
### 启动
```npm
npm run dev
```
### 取消mock模拟数据
```javascript
// src\main.ts文件
把下面两行代码注释掉就可以了。
import { mockXHR } from "@/mock/index";
mockXHR()
```
##
## 公用组件
封装了除面条外个别用到的组件
### 自适应缩放组件
#### 注意
采用Scale方式会自动给组件父元素添加overflow:hidden
#### 使用
```vue
<template>
<scale-screen width="1920" height="1080">
<div>
content
</div>
</scale-screen>
</template>
<script>
import ScaleScreen from 'scale-screen'
export default {
name:'Demo',
components:{
VScaleScreen
}
}
</script>
```
#### API
| 属性 | 说明 | 类型 | 默认值 |
| ------------ | ------------------------------------------------------------ | -------------------------------- | ------ |
| selfAdaption | 是否进行自适应 | Boolean | true |
| width | 大屏宽度 | `Number` or `String` | 1920 |
| height | 大屏高度 | `Number` or `String` | 1080 |
| autoScale | 自适应配置配置为boolean类型时为启动或者关闭自适应配置为对象时若x为truex轴产生边距y为true时y轴产生边距启用fullScreen时此配置失效 | Boolean or {x:boolean,y:boolean} | true |
| delay | 窗口变化防抖延迟时间 | Number | 500 |
| fullScreen | 全屏自适应启用此配置项时会存在拉伸效果同时autoScale失效非必要情况下不建议开启 | Boolean | false |
| boxStyle | 修改容器样式如居中展示时侧边背景色符合Vue双向绑定style标准格式 | Object | null |
| wrapperStyle | 修改自适应区域样式符合Vue双向绑定style标准格式 | Object | null |
### 外边框
因为我的项目外边框几乎一样还有title,所以封装了此组件。
根据自己需求更改更换外边框src\components\item-wrap\item-wrap.vue下更换。
```vue
<ItemWrap
title="我是title"
>
<div>我是谁?</div>
</ItemWrap>
```
| 参数 | 描述 | 默认值 | 类型 | 可选值 |
| :---: | :--: | :----: | :----: | :----: |
| title | 标头 | - | string | - |
### CountUp 数字滚动
以下属性同 coutup.js 配置项same as countup.js properties
#### Props
| Name | Type | Default | Description |
| -------- | ---------------- | ------- | ------------------------------------------------------------ |
| endVal | Number \| String | - | 结束值 |
| startVal | Number \| String | 0 | 起始值 |
| duration | Number | 2.5 | 动画时长,单位:秒 |
| options | Object | - | [countUp.js](https://github.com/inorganik/countUp.js) options 配置项 |
以下为组件特有属性extension properties
| Name | Type | Default | Description |
| -------- | ----------------- | ------- | ----------------------------- |
| autoplay | Boolean | true | 是否自动计数 |
| loop | Boolean \| Number | false | 循环次数,有限次数 / 无限循环 |
| delay | Number | 0 | loop 循环的间隔时间,单位:秒 |
#### 插槽slots
| Name | Description |
| ------ | ----------- |
| prefix | 前缀 |
| suffix | 后缀 |
#### 事件Events
| Name | Description | return |
| --------- | -------------------------- | ------------ |
| @init | CountUp 实例初始化完成触发 | CountUp 实例 |
| @finished | 计数结束时触发 | - |
#### countup.js 配置项说明
```ts
interface CountUpOptions {
startVal?: number // number to start at (0) 开始数值,默认 0
decimalPlaces?: number // number of decimal places (0) 小数点 位数
duration?: number // animation duration in seconds (2) 动画时长
useGrouping?: boolean // example: 1,000 vs 1000 (true) 是否使用千分位
useEasing?: boolean // ease animation (true) 是否开启动画过渡默认动画函数为easeOutExpo
smartEasingThreshold?: number // smooth easing for large numbers above this if useEasing (999)
smartEasingAmount?: number // amount to be eased for numbers above threshold (333)
separator?: string // grouping separator (',') 千分位分隔符
decimal?: string // decimal ('.') 小数点分隔符
// easingFn: easing function for animation (easeOutExpo) 动画函数
easingFn?: (t: number, b: number, c: number, d: number) => number
formattingFn?: (n: number) => string // this function formats result 格式化结果
prefix?: string // text prepended to result 数值前缀
suffix?: string // text appended to result 数值后缀
numerals?: string[] // numeral glyph substitution 数字符号替换 0 - 9例如替换为 [a,b,c,d,e,f,g,h,i,j]
enableScrollSpy?: boolean // start animation when target is in view 在可视范围内才开始动画
scrollSpyDelay?: number // delay (ms) after target comes into view 目标进入可视范围内后的延迟时间(毫秒)
}
```
### 胶囊柱图
#### Props
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
| :----: | :------: | :-------------: | :-----------------------: | :-----: |
| data | 柱数据 | `Array<Object>` | [data属性](#data属性) | `[]` |
| config | 基础配置 | Object | [config属性](#config属性) | `false` |
#### config属性
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
| :-------: | :------: | :-------------: | :----: | :-----: |
| unit | 单位 | `String` | --- | `''` |
| colors | 环颜色 | `Array<String>` | [1] | [2] |
| showValue | 显示数值 | `Boolean` | --- | `false` |
#### 注释config注释
[1] 颜色支持`hex|rgb|rgba|颜色关键字`等四种类型。
[2] 默认配色为`['#37a2da', '#32c5e9', '#67e0e3', '#9fe6b8', '#ffdb5c', '#ff9f7f', '#fb7293']`。
#### data属性
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
| :---: | :------: | :------: | :----: | :----: |
| name | 柱名称 | `String` | --- | --- |
| value | 柱对应值 | `Number` | --- | --- |
### 无缝轮播组件
看此文档 优化次源码
[https://doc.wssio.com/opensource/vue3-seamless-scroll/](https://doc.wssio.com/opensource/vue3-seamless-scroll/)
## 中间地图
### 南海显隐控制
根据需求来,**修改此值请刷新页面**
```indexs/center-map.vue``` 文件中```isSouthChinaSea```变量 默认不显示南海(false),为```true```的时候显示南海
```
isSouthChinaSea:false,//默认不显示南海改为true可显示南海
```
## 大屏交流反馈(面条的群)
### 大屏QQ群
QQ群号
一群713105837 (已满)
二群495755841
## 项目描述
[IofTV-Screen](https://gitee.com/daidaibg/IofTV-Screen/tree/main) 的 Vue3+vite版本
### 与vue2版本对比
#### 功能
功能采用与vue2版本相同功能
因为要与vue2版本相同功能有些组件不兼容vue3版本例如胶囊柱图数字滚动皆重新封装为组件整体来说功能属实相同。根据自己需求选择[vue2](#vue2版本地址)版本与[vue3](#本项目地址 vue3+vite)版本
#### 样式
进行微调,整体看着更加美观
- 项目需要全屏展示(按 F11
- 项目部分区域使用了全局注册方式,增加了打包体积,在实际运用中请使用 **按需引入**
- 项目环境Vite、Echarts、Npm、Nodeaxios,mock,vue3。
- 请拉取 master 分支的代码,其余分支是开发分支。
- 在项目public目录下存放地图数据合集根据地市编存放。
友情链接:
1. [Vue 官方文档](https://cn.vuejs.org/)
3. [echarts 实例](https://gitee.com/link?target=https%3A%2F%2Fecharts.apache.org%2Fexamples%2Fzh%2Findex.html)[echarts API 文档](https://gitee.com/link?target=https%3A%2F%2Fecharts.apache.org%2Fzh%2Fapi.html%23echarts)
4. [mock.js官网](http://mockjs.com/examples.html)
5. [axios官网](https://axios-http.com/)
**项目展示**
![项目展示](https://www.daidaibg.com/bigscreen/a-img/home.png)
### 项目预览地址
[https://www.daidaibg.com/bigscreen-vue3](https://www.daidaibg.com/bigscreen-vue3)
### 项目仓库地址
#### 本项目地址 vue3+vite
**github地址**
[https://github.com/daidaibg/IofTV-Screen-Vue3](https://github.com/daidaibg/IofTV-Screen-Vue3)
**Gitee地址**
[https://gitee.com/daidaibg/IofTV-Screen-Vue3](https://gitee.com/daidaibg/IofTV-Screen-Vue3)
#### vue2版本地址
**github地址**
[https://github.com/daidaibg/IofTV-Screen](https://github.com/daidaibg/IofTV-Screen)
**Gitee地址**
[https://gitee.com/daidaibg/IofTV-Screen](https://gitee.com/daidaibg/IofTV-Screen)
### 采用自适应组件方式,
### 滚动设置,自适应设置
项目中可以进行滚动配置,内容是否滚动
点击右上角设置按钮
![设置](https://www.daidaibg.com/bigscreen/a-img/setting.png)
可以进行以下配置,可以自行代码中进行修改或增加配置
![在这里插入图片描述](https://www.daidaibg.com/bigscreen/a-img/setting2.png)
## 2、主要文件介绍
| 文件 | 作用/功能 |
| ----------------- | ------------------------------------------------------------ |
| main.js | 主目录文件,引入 Echart/DataV 等文件 |
| utils | 工具函数与 mixins 函数等 |
| views/ home.vue | 项目主结构 |
| views/其余文件 | 界面各个区域组件(按照位置来命名) |
| assets | 静态资源目录,放置 logo 与背景图片 |
| assets / css/ | 通用 CSS 文件,全局项目快捷样式调节 |
| components/echart | 所有 echart 图表(按照位置来命名) |
| common/... | 全局封装的 ECharts 和 flexible 插件代码(适配屏幕尺寸,可定制化修改) |
| api/api.js | 接口封装文件 |
| mock | 模拟数据接口地址 |
###
## 使用介绍
### 安装
```npm
npm install
```
### 启动
```npm
npm run dev
```
### 取消mock模拟数据
```javascript
// src\main.ts文件
把下面两行代码注释掉就可以了。
import { mockXHR } from "@/mock/index";
mockXHR()
```
##
## 公用组件
封装了除面条外个别用到的组件
### 自适应缩放组件
#### 注意
采用Scale方式会自动给组件父元素添加overflow:hidden
#### 使用
```vue
<template>
<scale-screen width="1920" height="1080">
<div>
content
</div>
</scale-screen>
</template>
<script>
import ScaleScreen from 'scale-screen'
export default {
name:'Demo',
components:{
VScaleScreen
}
}
</script>
```
#### API
| 属性 | 说明 | 类型 | 默认值 |
| ------------ | ------------------------------------------------------------ | -------------------------------- | ------ |
| selfAdaption | 是否进行自适应 | Boolean | true |
| width | 大屏宽度 | `Number` or `String` | 1920 |
| height | 大屏高度 | `Number` or `String` | 1080 |
| autoScale | 自适应配置配置为boolean类型时为启动或者关闭自适应配置为对象时若x为truex轴产生边距y为true时y轴产生边距启用fullScreen时此配置失效 | Boolean or {x:boolean,y:boolean} | true |
| delay | 窗口变化防抖延迟时间 | Number | 500 |
| fullScreen | 全屏自适应启用此配置项时会存在拉伸效果同时autoScale失效非必要情况下不建议开启 | Boolean | false |
| boxStyle | 修改容器样式如居中展示时侧边背景色符合Vue双向绑定style标准格式 | Object | null |
| wrapperStyle | 修改自适应区域样式符合Vue双向绑定style标准格式 | Object | null |
### 外边框
因为我的项目外边框几乎一样还有title,所以封装了此组件。
根据自己需求更改更换外边框src\components\item-wrap\item-wrap.vue下更换。
```vue
<ItemWrap
title="我是title"
>
<div>我是谁?</div>
</ItemWrap>
```
| 参数 | 描述 | 默认值 | 类型 | 可选值 |
| :---: | :--: | :----: | :----: | :----: |
| title | 标头 | - | string | - |
### CountUp 数字滚动
以下属性同 coutup.js 配置项same as countup.js properties
#### Props
| Name | Type | Default | Description |
| -------- | ---------------- | ------- | ------------------------------------------------------------ |
| endVal | Number \| String | - | 结束值 |
| startVal | Number \| String | 0 | 起始值 |
| duration | Number | 2.5 | 动画时长,单位:秒 |
| options | Object | - | [countUp.js](https://github.com/inorganik/countUp.js) options 配置项 |
以下为组件特有属性extension properties
| Name | Type | Default | Description |
| -------- | ----------------- | ------- | ----------------------------- |
| autoplay | Boolean | true | 是否自动计数 |
| loop | Boolean \| Number | false | 循环次数,有限次数 / 无限循环 |
| delay | Number | 0 | loop 循环的间隔时间,单位:秒 |
#### 插槽slots
| Name | Description |
| ------ | ----------- |
| prefix | 前缀 |
| suffix | 后缀 |
#### 事件Events
| Name | Description | return |
| --------- | -------------------------- | ------------ |
| @init | CountUp 实例初始化完成触发 | CountUp 实例 |
| @finished | 计数结束时触发 | - |
#### countup.js 配置项说明
```ts
interface CountUpOptions {
startVal?: number // number to start at (0) 开始数值,默认 0
decimalPlaces?: number // number of decimal places (0) 小数点 位数
duration?: number // animation duration in seconds (2) 动画时长
useGrouping?: boolean // example: 1,000 vs 1000 (true) 是否使用千分位
useEasing?: boolean // ease animation (true) 是否开启动画过渡默认动画函数为easeOutExpo
smartEasingThreshold?: number // smooth easing for large numbers above this if useEasing (999)
smartEasingAmount?: number // amount to be eased for numbers above threshold (333)
separator?: string // grouping separator (',') 千分位分隔符
decimal?: string // decimal ('.') 小数点分隔符
// easingFn: easing function for animation (easeOutExpo) 动画函数
easingFn?: (t: number, b: number, c: number, d: number) => number
formattingFn?: (n: number) => string // this function formats result 格式化结果
prefix?: string // text prepended to result 数值前缀
suffix?: string // text appended to result 数值后缀
numerals?: string[] // numeral glyph substitution 数字符号替换 0 - 9例如替换为 [a,b,c,d,e,f,g,h,i,j]
enableScrollSpy?: boolean // start animation when target is in view 在可视范围内才开始动画
scrollSpyDelay?: number // delay (ms) after target comes into view 目标进入可视范围内后的延迟时间(毫秒)
}
```
### 胶囊柱图
#### Props
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
| :----: | :------: | :-------------: | :-----------------------: | :-----: |
| data | 柱数据 | `Array<Object>` | [data属性](#data属性) | `[]` |
| config | 基础配置 | Object | [config属性](#config属性) | `false` |
#### config属性
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
| :-------: | :------: | :-------------: | :----: | :-----: |
| unit | 单位 | `String` | --- | `''` |
| colors | 环颜色 | `Array<String>` | [1] | [2] |
| showValue | 显示数值 | `Boolean` | --- | `false` |
#### 注释config注释
[1] 颜色支持`hex|rgb|rgba|颜色关键字`等四种类型。
[2] 默认配色为`['#37a2da', '#32c5e9', '#67e0e3', '#9fe6b8', '#ffdb5c', '#ff9f7f', '#fb7293']`。
#### data属性
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
| :---: | :------: | :------: | :----: | :----: |
| name | 柱名称 | `String` | --- | --- |
| value | 柱对应值 | `Number` | --- | --- |
### 无缝轮播组件
看此文档 优化次源码
[https://doc.wssio.com/opensource/vue3-seamless-scroll/](https://doc.wssio.com/opensource/vue3-seamless-scroll/)
## 中间地图
### 南海显隐控制
根据需求来,**修改此值请刷新页面**
```indexs/center-map.vue``` 文件中```isSouthChinaSea```变量 默认不显示南海(false),为```true```的时候显示南海
```
isSouthChinaSea:false,//默认不显示南海改为true可显示南海
```
## 大屏交流反馈(面条的群)
### 大屏QQ群
QQ群号
一群713105837 (已满)
二群495755841

2
env.d.ts vendored
View File

@ -1 +1 @@
/// <reference types="vite/client" />
/// <reference types="vite/client" />

View File

@ -1,19 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IofTV-Screen-Vue3</title>
<script
type="text/javascript"
src="<%= BASE_URL %>static/map_load.js"
></script>
<!-- <script src="https://api.map.baidu.com/api?v=3.0&ak=a76Liq52IWX8S5WkSi2z5WvUYRMaNMRe"></script>
<script src="https://api.map.baidu.com/getscript?v=3.0&ak=a76Liq52IWX8S5WkSi2z5WvUYRMaNMRe&s=1"></script> -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IofTV-Screen-Vue3</title>
<script
type="text/javascript"
src="<%= BASE_URL %>static/map_load.js"
></script>
<!-- <script src="https://api.map.baidu.com/api?v=3.0&ak=a76Liq52IWX8S5WkSi2z5WvUYRMaNMRe"></script>
<script src="https://api.map.baidu.com/getscript?v=3.0&ak=a76Liq52IWX8S5WkSi2z5WvUYRMaNMRe&s=1"></script> -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

11702
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,48 @@
{
"name": "test",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build:old": "run-p type-check build-only",
"build": "vite build",
"preview": "vite preview --port 4173",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"axios": "^1.6.8",
"countup.js": "^2.8.0",
"dayjs": "^1.11.10",
"echarts": "^5.5.0",
"element-plus": "^2.6.2",
"hls.js": "^1.6.5",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"pinia": "^2.1.7",
"vite-plugin-html": "^3.2.2",
"vue": "^3.4.21",
"vue-echarts": "^6.6.9",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",
"@types/mockjs": "^1.0.10",
"@types/node": "^20.11.30",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/tsconfig": "^0.5.1",
"@vueuse/core": "^10.9.0",
"autoprefixer": "^10.4.19",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.38",
"sass": "^1.72.0",
"tailwindcss": "^3.4.3",
"typescript": "~5.4.3",
"unplugin-auto-import": "^0.17.5",
"unplugin-element-plus": "^0.8.0",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.6",
"vue-tsc": "^2.0.7"
}
}
{
"name": "test",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build:old": "run-p type-check build-only",
"build": "vite build",
"preview": "vite preview --port 4173",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"axios": "^1.6.8",
"countup.js": "^2.8.0",
"dayjs": "^1.11.10",
"echarts": "^5.5.0",
"element-plus": "^2.6.2",
"hls.js": "^1.6.5",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"pinia": "^2.1.7",
"vite-plugin-html": "^3.2.2",
"vue": "^3.4.21",
"vue-echarts": "^6.6.9",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",
"@types/mockjs": "^1.0.10",
"@types/node": "^20.11.30",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/tsconfig": "^0.5.1",
"@vueuse/core": "^10.9.0",
"autoprefixer": "^10.4.19",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.38",
"sass": "^1.72.0",
"tailwindcss": "^3.4.3",
"typescript": "~5.4.3",
"unplugin-auto-import": "^0.17.5",
"unplugin-element-plus": "^0.8.0",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.6",
"vue-tsc": "^2.0.7"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -4,13 +4,20 @@
var bmapcfg = {
'imgext': '.jpg', //瓦片图的后缀 ------ 根据需要修改,一般是 .png .jpg
//这里我直接用的路径是/static/bmap_offline_demo/tiles如果瓦片数量较大可改为瓦片服务的地址
tiles_dir: "images", //普通瓦片图的地址,为空默认在 offlinemap/tiles/ 目
tiles_path : 'http://172.16.1.162:8123/HeBeiDarkMap/',
tiles_hybrid: 'http://172.16.1.162:8123/HeBeiDarkMap/', //卫星瓦片图的地址,为空默认在 offlinemap/tiles_hybrid/ 目
tiles_self : 'http://172.16.1.162:8123/HeBeiDarkMap/' //自定义图层的地址,为空默认在 offlinemap/tiles_self/ 目录
// tiles_dir: "images", //普通瓦片图的地址,为空默认在 offlinemap/tiles/ 目
//现场
tiles_dir: "map", //普通瓦片图的地址,为空默认在 offlinemap/tiles/ 目
// tiles_path : 'http://172.16.1.162:8123/HeBeiDarkMap/',
// tiles_hybrid: 'http://172.16.1.162:8123/HeBeiDarkMap/', //卫星瓦片图的地址,为空默认在 offlinemap/tiles_hybrid/ 目
// tiles_self : 'http://172.16.1.162:8123/HeBeiDarkMap/' //自定义图层的地址,为空默认在 offlinemap/tiles_self/ 目录
// tiles_path: "http://111.229.30.246:4008/HeBeiDarkMap/",
// tiles_hybrid: "http://111.229.30.246:4008/HeBeiDarkMap/", //卫星瓦片图的地址,为空默认在 offlinemap/tiles_hybrid/ 目
// tiles_self: "http://111.229.30.246:4008/HeBeiDarkMap/", //自定义图层的地址,为空默认在 offlinemap/tiles_self/ 目录
//现场
tiles_path: "http://13.14.64.150:3638/",
tiles_hybrid: "http://13.14.64.150:3638/", //卫星瓦片图的地址,为空默认在 offlinemap/tiles_hybrid/ 目
tiles_self: "http://13.14.64.150:3638/", //自定义图层的地址,为空默认在 offlinemap/tiles_self/ 目录
};
//下面的保持不动///

View File

@ -1,12 +1,12 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style scoped>
</style>
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style scoped>
</style>

View File

@ -1,3 +1,3 @@
import MessageContent from './index.vue';
export default MessageContent ;
import MessageContent from './index.vue';
export default MessageContent ;

View File

@ -1,8 +1,8 @@
<!-- eslint-disable vue/valid-template-root -->
<template></template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
// window 便js使
window['$message'] = ElMessage
</script>
<!-- eslint-disable vue/valid-template-root -->
<template></template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
// window 便js使
window['$message'] = ElMessage
</script>

View File

@ -1,20 +1,20 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import '@/assets/css/main.scss'
import '@/assets/css/tailwind.css'
import {registerEcharts} from "@/plugins/echarts"
//不使用mock 请注释掉
import { mockXHR } from "@/mock/index";
mockXHR()
const app = createApp(App)
registerEcharts(app)
app.use(createPinia())
app.use(router)
app.mount('#app')
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import '@/assets/css/main.scss'
import '@/assets/css/tailwind.css'
import {registerEcharts} from "@/plugins/echarts"
//不使用mock 请注释掉
import { mockXHR } from "@/mock/index";
mockXHR()
const app = createApp(App)
registerEcharts(app)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@ -1,30 +1,30 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import type {RouteRecordRaw} from "vue-router"
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/index',
},
{
path: '/home',
name: 'home',
component: () => import('@/views/HomeView.vue'),
children:[
{
path: '/index',
name: 'index',
component: () => import('@/views/index/index.vue'),
}
]
},
]
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
})
router.beforeEach((to, from, next) => {
next();
})
export default router
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import type {RouteRecordRaw} from "vue-router"
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/index',
},
{
path: '/home',
name: 'home',
component: () => import('@/views/HomeView.vue'),
children:[
{
path: '/index',
name: 'index',
component: () => import('@/views/index/index.vue'),
}
]
},
]
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
})
router.beforeEach((to, from, next) => {
next();
})
export default router

View File

@ -1,2 +1,2 @@
import mitt from 'mitt';
import mitt from 'mitt';
export const emitter = mitt();

View File

@ -1,27 +1,27 @@
import { ref, onMounted } from "vue";
/**
* YYYY-MM-DD
*/
export const useTodayTime = () => {
const todayTime = ref<string>("");
const getTodayTime = () => {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始
const day = String(date.getDate()).padStart(2, '0');
const weekDay = date.toLocaleDateString('zh-CN', { weekday: 'long' }); // 星期几
todayTime.value = `${year}-${month}-${day}`;
};
onMounted(() => {
getTodayTime();
});
return {
todayTime,
getTodayTime,
};
import { ref, onMounted } from "vue";
/**
* YYYY-MM-DD
*/
export const useTodayTime = () => {
const todayTime = ref<string>("");
const getTodayTime = () => {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始
const day = String(date.getDate()).padStart(2, '0');
const weekDay = date.toLocaleDateString('zh-CN', { weekday: 'long' }); // 星期几
todayTime.value = `${year}-${month}-${day}`;
};
onMounted(() => {
getTodayTime();
});
return {
todayTime,
getTodayTime,
};
};

View File

@ -1,102 +1,102 @@
<script setup lang="ts">
import { ref } from "vue";
import { RouterView } from "vue-router";
import ScaleScreen from "@/components/scale-screen";
import Left from "./component/left.vue";
import Right from "./component/right.vue";
import Center from "./component/center.vue";
import Headers from "./header.vue";
import Setting from "./setting.vue";
import { useSettingStore } from "@/stores/index";
import { storeToRefs } from "pinia";
import MessageContent from "@/components/Plugins/MessageContent";
import Map from "./component/map.vue";
import Home from "./index/index.vue"
const settingStore = useSettingStore();
const { isScale } = storeToRefs(settingStore);
const wrapperStyle = {};
</script>
<template>
<scale-screen
width="12960"
height="2430"
:delay="500"
:fullScreen="false"
:boxStyle="{
background: '#03050C',
overflow: isScale ? 'hidden' : 'auto',
}"
:wrapperStyle="wrapperStyle"
:autoScale="isScale"
>
<div class="content_wrap">
<!-- 背景层 -->
<div class="background_layer">
<Map />
<div class="header_layer">
<Headers />
</div>
<div class="left">
<Left title=""></Left>
</div>
<div class="right">
<Right title=""></Right>
</div>
<Center class="center" />
</div>
<!-- 前景层 -->
<!-- <div class="foreground_layer">
<Headers />
<RouterView />
</div> -->
<!-- <MessageContent /> -->
</div>
</scale-screen>
<Setting />
</template>
<style lang="scss" scoped>
.content_wrap {
width: 100%;
height: 100%;
position: relative;
background-image: url("@/assets/img/pageBg.png");
background-size: cover;
background-position: center center;
overflow: hidden; //
}
.background_layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
// z-index: 0; //
position: relative;
.header_layer{
position: absolute;
top: 0;
}
.left{
position: absolute;
top: 100px;
left: 0;
}
.right{
position: absolute;
top: 100px;
right: 0;
}
.center{
position: absolute;
top: 100px;
}
}
.foreground_layer {
position: relative;
// z-index: 1; //
}
</style>
<script setup lang="ts">
import { ref } from "vue";
import { RouterView } from "vue-router";
import ScaleScreen from "@/components/scale-screen";
import Left from "./component/left.vue";
import Right from "./component/right.vue";
import Center from "./component/center.vue";
import Headers from "./header.vue";
import Setting from "./setting.vue";
import { useSettingStore } from "@/stores/index";
import { storeToRefs } from "pinia";
import MessageContent from "@/components/Plugins/MessageContent";
import Map from "./component/map.vue";
import Home from "./index/index.vue"
const settingStore = useSettingStore();
const { isScale } = storeToRefs(settingStore);
const wrapperStyle = {};
</script>
<template>
<scale-screen
width="12960"
height="2430"
:delay="500"
:fullScreen="false"
:boxStyle="{
background: '#03050C',
overflow: isScale ? 'hidden' : 'auto',
}"
:wrapperStyle="wrapperStyle"
:autoScale="isScale"
>
<div class="content_wrap">
<!-- 背景层 -->
<div class="background_layer">
<Map />
<div class="header_layer">
<Headers />
</div>
<div class="left">
<Left title=""></Left>
</div>
<div class="right">
<Right title=""></Right>
</div>
<Center class="center" />
</div>
<!-- 前景层 -->
<!-- <div class="foreground_layer">
<Headers />
<RouterView />
</div> -->
<!-- <MessageContent /> -->
</div>
</scale-screen>
<Setting />
</template>
<style lang="scss" scoped>
.content_wrap {
width: 100%;
height: 100%;
position: relative;
background-image: url("@/assets/img/pageBg.png");
background-size: cover;
background-position: center center;
overflow: hidden; //
}
.background_layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
// z-index: 0; //
position: relative;
.header_layer{
position: absolute;
top: 0;
}
.left{
position: absolute;
top: 100px;
left: 0;
}
.right{
position: absolute;
top: 100px;
right: 0;
}
.center{
position: absolute;
top: 100px;
}
}
.foreground_layer {
position: relative;
// z-index: 1; //
}
</style>

View File

@ -1,75 +1,75 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import useCharts from "@/hooks/useEcharts";
import { todayTrafficCount } from "@/api/modules/index";
import type { EChartsOption } from "echarts";
// DOM ref ECharts
const chartRef = ref<HTMLElement | null>(null);
let chart: ReturnType<typeof useCharts> | null = null;
//
const trafficData = reactive({
total: 0,
car: { value: 0, percentage: 0, name: "小型车" },
truck: { value: 0, percentage: 0, name: "货车" },
bus: { value: 0, percentage: 0, name: "客车" },
});
//
const chartOptions: EChartsOption = {
tooltip: { trigger: "item", formatter: "{b}: {c} ({d}%)" },
legend: { show: false },
series: [
{
name: "车流量分布",
type: "pie",
radius: ["65%", "80%"],
center: ["50%", "50%"],
data: [], //
},
],
};
// +
onMounted(async () => {
if (chartRef.value) {
chart = useCharts(chartRef.value);
chart.setOption(chartOptions); //
}
await loadTrafficData();
});
//
async function loadTrafficData() {
try {
const res = await todayTrafficCount(); // cartruckbus
trafficData.total = res.total;
trafficData.car.value = res.car;
trafficData.truck.value = res.truck;
trafficData.bus.value = res.bus;
const newChartData = [
{ value: res.car, name: "小型车" },
{ value: res.truck, name: "货车" },
{ value: res.bus, name: "客车" },
];
if (chart) {
chart.setOption({
series: [
{
data: newChartData,
},
],
});
}
} catch (error) {
console.error("获取车流量数据失败:", error);
}
}
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import useCharts from "@/hooks/useEcharts";
import { todayTrafficCount } from "@/api/modules/index";
import type { EChartsOption } from "echarts";
// DOM ref ECharts
const chartRef = ref<HTMLElement | null>(null);
let chart: ReturnType<typeof useCharts> | null = null;
//
const trafficData = reactive({
total: 0,
car: { value: 0, percentage: 0, name: "小型车" },
truck: { value: 0, percentage: 0, name: "货车" },
bus: { value: 0, percentage: 0, name: "客车" },
});
//
const chartOptions: EChartsOption = {
tooltip: { trigger: "item", formatter: "{b}: {c} ({d}%)" },
legend: { show: false },
series: [
{
name: "车流量分布",
type: "pie",
radius: ["65%", "80%"],
center: ["50%", "50%"],
data: [], //
},
],
};
// +
onMounted(async () => {
if (chartRef.value) {
chart = useCharts(chartRef.value);
chart.setOption(chartOptions); //
}
await loadTrafficData();
});
//
async function loadTrafficData() {
try {
const res = await todayTrafficCount(); // cartruckbus
trafficData.total = res.total;
trafficData.car.value = res.car;
trafficData.truck.value = res.truck;
trafficData.bus.value = res.bus;
const newChartData = [
{ value: res.car, name: "小型车" },
{ value: res.truck, name: "货车" },
{ value: res.bus, name: "客车" },
];
if (chart) {
chart.setOption({
series: [
{
data: newChartData,
},
],
});
}
} catch (error) {
console.error("获取车流量数据失败:", error);
}
}
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
</template>

View File

@ -1,449 +1,449 @@
<template>
<div class="center-container">
<div class="statistics-panel">
<div v-for="(stat, index) in statistics" :key="index" class="stat-card" style="position: relative;">
<div
class="box"
@mouseenter="stat.label === '服务区' && (showDropdown = true)"
@mouseleave="stat.label === '服务区' && (showDropdown = false)"
style="position: relative;"
>
<div class="icon-wrapper">
<img :src="stat.image" :alt="stat.label" />
</div>
<div class="stat-content">
<div class="stat-label">
{{ stat.label }}
</div>
<div class="stat-value">
<span class="number">{{ stat.value }}</span>
</div>
</div>
<!-- 只在服务区显示下拉选择 -->
<template v-if="stat.label === '服务区'">
<div
v-show="showDropdown"
class="dropdown large-dropdown"
@mouseenter="showDropdown = true"
@mouseleave="showDropdown = false"
>
<div
class="dropdown-item"
v-for="area in serviceAreas"
:key="area"
@click="selectServiceArea(area)"
>
{{ area }}
</div>
</div>
</template>
</div>
</div>
</div>
<!-- 下面弹框标题用当前选中的服务区 -->
<!-- <div v-if="showServiceArea" class="map-container">
<div class="map-title">
<div class="title-content">
<span class="title-text">{{ selectedServiceArea }}</span>
</div>
<div class="close-btn" @click="closeDialog">×</div>
</div>
<div class="map-con">
<RealTimeImageCenter />
</div>
</div> -->
<div v-if="showServiceArea" style="width: 5664px;" class="map-container RealTimeImage">
<div class="map-title">
<div class="title-content">
<span class="title-text">{{selectedServiceArea}}</span>
</div>
<div class="close-btn" @click="closeDialog">×</div>
</div>
<div class="map-con">
<RealTimeImageCenter />
</div>
</div>
<div class="map-container bg-header bg-header-one" style="width: 1800px;" v-if="showDeviceInfo">
<div class="map-title">
<div class="title-content">
<span class="title-text">设备信息NO{{ deviceInfo.deviceId }}</span>
</div>
<div class="close-btn" @click="closeDeviceInfo">×</div>
</div>
<div class="map-con">
<DeviceInfoComponent :deviceInfo="deviceInfo" />
</div>
</div>
<div class="map-container bg-header bg-header-two" style="width: 1800px;height: 1000px;" v-if="showDeviceInfoTwo">
<div class="map-title">
<div class="title-content">
<span class="title-text">设备信息NO{{ deviceInfo.deviceId }}</span>
</div>
<div class="close-btn" @click="closeDeviceInfo">×</div>
</div>
<div class="map-con">
<DeviceInfoComponentTwo :deviceInfo="deviceInfoTwo" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, nextTick } from "vue";
import { onMounted } from 'vue';
import fee from "@/assets/img/fee.png";
import km from "@/assets/img/km.png";
import toll from "@/assets/img/toll.png";
import RealTimeImageCenter from "./RealTimeImageCenter.vue";
import DeviceInfoComponent from "./DeviceInfoComponent.vue";
import bridge from "@/assets/img/bridge.png";
import hub from "@/assets/img/hub.png";
import { getDeviceList } from "@/api/modules/index";
import DeviceInfoComponentTwo from "./DeviceInfoComponentTwo.vue";
import Map from "./map.vue"
import { emitter } from '@/utils/eventBus';
const showServiceArea = ref(false);
const showMap = ref(true);
const showDeviceInfo = ref(false);
const showDeviceInfoTwo = ref(false);
const closeDialog = () => {
showServiceArea.value = false;
};
const showDropdown = ref(false);
const serviceAreas = ["南皮服务区", "东光服务区"];
const selectedServiceArea = ref("南皮服务区");
function selectServiceArea(area) {
selectedServiceArea.value = area;
showServiceArea.value = true;
showDropdown.value = false;
}
//
const statistics = ref([
{
image: km,
value: "78.338",
label: "全里程(公里)",
},
{
image: toll,
value: "5",
label: "收费站",
},
{
image: fee,
value: "2",
label: "服务区",
},
{
image: bridge,
value: "7",
label: "桥梁(座)",
},
{
image: hub,
value: "7",
label: "隧道互通(座)",
},
]);
//
const deviceInfo = ref({
deviceType: '视频监控',
cameraType: '枪机',
deviceCode: '3_CAM1_0',
roadSection: '路段名称',
direction: '邯郸方向',
stakeNumber: 'K18+324',
longitude: '44.230145',
latitude: '45.65412',
ipAddress: '13.106.2.10'
});
const deviceInfoTwo = ref({
deviceType: '视频监控',
cameraType: '枪机',
deviceCode: '3_CAM1_0',
roadSection: '路段名称',
direction: '邯郸方向',
stakeNumber: 'K18+324',
longitude: '44.230145',
latitude: '45.65412',
ipAddress: '13.106.2.10'
});
// const deviceInfo = ref({});
function handleShowDeviceInfo(data) {
console.log("接收到设备信息:", data);
//
showDeviceInfo.value = false;
showDeviceInfoTwo.value = false;
if (data.deviceTypeId === "22" || data.deviceTypeId === "44") {
showDeviceInfo.value = true;
} else if (data.deviceTypeId === "15" || data.deviceTypeId === "53") {
showDeviceInfoTwo.value = true;
}
deviceInfoTwo.value = data;
deviceInfo.value = data;
}
const closeDeviceInfo = () => {
showDeviceInfo.value = false;
showDeviceInfoTwo.value = false;
showMap.value = true;
};
const mapFlag = ref(true);
onMounted(() => {
emitter.on("show-device-info", handleShowDeviceInfo);
});
</script>
<style scoped lang="scss">
.center-container {
width: 100%;
// height: 100%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.RealTimeImage{
background-image: url("@/assets/img/eqm/fuwuqu.png");
background-size: 100% 100%;
}
.map-container.bg-header {
position: absolute;
top: 500px;
left: 42.5%;
// transform: translate(-50%, -50%);
z-index: 2000;
// box-shadow: 0 8px 40px rgba(0,0,0,0.3);
}
.bg-header-one {
background-image: url("@/assets/img/eqm/eqminfo.png");
background-size: 100% 100%;
}
.bg-header-two {
background-image: url("@/assets/img/eqm/eqminfo2.png");
background-size: 100% 100%;
}
}
.statistics-panel {
display: flex;
justify-content: space-around;
align-items: center;
width: 40%;
padding: 20px;
gap: 30px;
padding-top: 200px;
padding-bottom: 50px;
.box{
display: flex;
justify-content: space-around;
align-items: center;
}
}
.stat-card {
position: relative;
display: flex;
align-items: center;
padding: 15px 20px;
}
.card-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(0, 170, 255, 0.1) 0%,
transparent 100%);
border-radius: 8px;
opacity: 0.5;
transition: opacity 0.3s ease;
}
.icon-wrapper {
position: relative;
margin-right: 15px;
img {
width: 160px;
height: 160px;
}
}
.stat-content {
display: flex;
flex-direction: column;
position: relative;
}
.stat-value {
.number {
font-size: 64px !important;
font-weight: bold;
color: #1ae9ee;
}
}
.stat-label {
font-size: 32px !important;
color: rgba(255, 255, 255, 0.9);
margin-top: 8px;
letter-spacing: 1px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.map-container {
width: 78%;
margin-top: 30px;
// background: linear-gradient(360deg,
// rgba(15, 43, 89, 0.3) 0%,
// rgba(15, 43, 89, 0) 100%),
// rgba(15, 43, 89, 0.85);
// box-shadow: inset 0px 0px 57px 0px rgba(0, 149, 255, 0.41),
// inset 0px 0px 21px 0px rgba(49, 176, 255, 0.47);
// border-radius: 8px;
// border: 1px solid;
// border-image: linear-gradient(180deg,
// rgba(186, 213, 255, 0.3),
// rgba(186, 213, 255, 0)) 1 1;
// padding: 20px;
// box-sizing: border-box;
height: 1700px;
}
.map-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 15px 40px;
position: relative;
// border-bottom: 1px solid rgba(26, 233, 238, 0.2);
height: 150px;
background: repeating-linear-gradient(-65deg,
rgba(15, 43, 89, 0.3) 0px,
rgba(15, 43, 89, 0.3) 50px,
rgba(26, 233, 238, 0.05) 50px,
rgba(26, 233, 238, 0.05) 100px);
.title-content {
display: flex;
align-items: center;
}
.title-text {
font-size: 52px;
color: #fff;
font-weight: 700;
letter-spacing: 1px;
}
.close-btn {
font-size: 24px;
color: #fff;
cursor: pointer;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
&::before,
&::after {
content: "";
position: absolute;
width: 20px;
height: 2px;
background-color: #fff;
top: 50%;
left: 50%;
}
&::before {
transform: translate(-50%, -50%) rotate(45deg);
}
&::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
&:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
}
}
.map-con {
width: 100%;
height: 90%;
/* background-color: #1ae9ee; */
//
}
.eqm-info {
width: 1788px;
margin-top: 30px;
background: linear-gradient(360deg,
rgba(15, 43, 89, 0.3) 0%,
rgba(15, 43, 89, 0) 100%),
rgba(15, 43, 89, 0.85);
box-shadow: inset 0px 0px 57px 0px rgba(0, 149, 255, 0.41),
inset 0px 0px 21px 0px rgba(49, 176, 255, 0.47);
border-radius: 8px;
border: 1px solid;
border-image: linear-gradient(180deg,
rgba(186, 213, 255, 0.3),
rgba(186, 213, 255, 0)) 1 1;
padding: 20px;
box-sizing: border-box;
height: 1700px;
.eqm-title {
width: 100%;
height: 56px;
background-image: url("../../assets/img/eqm/eqmtitle.png");
}
}
.dropdown {
position: absolute;
right: 0;
bottom: -190px;
background: #112a44;
border: 1px solid #1ae9ee;
border-radius: 6px;
z-index: 100;
min-width: 220px;
min-height: 120px;
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
padding: 16px 0;
.dropdown-item {
padding: 20px 32px;
font-size: 28px;
color: #fff;
cursor: pointer;
text-align: left;
&:hover {
background: #1ae9ee;
color: #112a44;
}
}
}
</style>
<template>
<div class="center-container">
<div class="statistics-panel">
<div v-for="(stat, index) in statistics" :key="index" class="stat-card" style="position: relative;">
<div
class="box"
@mouseenter="stat.label === '服务区' && (showDropdown = true)"
@mouseleave="stat.label === '服务区' && (showDropdown = false)"
style="position: relative;"
>
<div class="icon-wrapper">
<img :src="stat.image" :alt="stat.label" />
</div>
<div class="stat-content">
<div class="stat-label">
{{ stat.label }}
</div>
<div class="stat-value">
<span class="number">{{ stat.value }}</span>
</div>
</div>
<!-- 只在服务区显示下拉选择 -->
<template v-if="stat.label === '服务区'">
<div
v-show="showDropdown"
class="dropdown large-dropdown"
@mouseenter="showDropdown = true"
@mouseleave="showDropdown = false"
>
<div
class="dropdown-item"
v-for="area in serviceAreas"
:key="area"
@click="selectServiceArea(area)"
>
{{ area }}
</div>
</div>
</template>
</div>
</div>
</div>
<!-- 下面弹框标题用当前选中的服务区 -->
<!-- <div v-if="showServiceArea" class="map-container">
<div class="map-title">
<div class="title-content">
<span class="title-text">{{ selectedServiceArea }}</span>
</div>
<div class="close-btn" @click="closeDialog">×</div>
</div>
<div class="map-con">
<RealTimeImageCenter />
</div>
</div> -->
<div v-if="showServiceArea" style="width: 5664px;" class="map-container RealTimeImage">
<div class="map-title">
<div class="title-content">
<span class="title-text">{{selectedServiceArea}}</span>
</div>
<div class="close-btn" @click="closeDialog">×</div>
</div>
<div class="map-con">
<RealTimeImageCenter />
</div>
</div>
<div class="map-container bg-header bg-header-one" style="width: 1800px;" v-if="showDeviceInfo">
<div class="map-title">
<div class="title-content">
<span class="title-text">设备信息NO{{ deviceInfo.deviceId }}</span>
</div>
<div class="close-btn" @click="closeDeviceInfo">×</div>
</div>
<div class="map-con">
<DeviceInfoComponent :deviceInfo="deviceInfo" />
</div>
</div>
<div class="map-container bg-header bg-header-two" style="width: 1800px;height: 1000px;" v-if="showDeviceInfoTwo">
<div class="map-title">
<div class="title-content">
<span class="title-text">设备信息NO{{ deviceInfo.deviceId }}</span>
</div>
<div class="close-btn" @click="closeDeviceInfo">×</div>
</div>
<div class="map-con">
<DeviceInfoComponentTwo :deviceInfo="deviceInfoTwo" />
</div>
</div>
</div>
</template>
<script setup>
import { ref, nextTick } from "vue";
import { onMounted } from 'vue';
import fee from "@/assets/img/fee.png";
import km from "@/assets/img/km.png";
import toll from "@/assets/img/toll.png";
import RealTimeImageCenter from "./RealTimeImageCenter.vue";
import DeviceInfoComponent from "./DeviceInfoComponent.vue";
import bridge from "@/assets/img/bridge.png";
import hub from "@/assets/img/hub.png";
import { getDeviceList } from "@/api/modules/index";
import DeviceInfoComponentTwo from "./DeviceInfoComponentTwo.vue";
import Map from "./map.vue"
import { emitter } from '@/utils/eventBus';
const showServiceArea = ref(false);
const showMap = ref(true);
const showDeviceInfo = ref(false);
const showDeviceInfoTwo = ref(false);
const closeDialog = () => {
showServiceArea.value = false;
};
const showDropdown = ref(false);
const serviceAreas = ["南皮服务区", "东光服务区"];
const selectedServiceArea = ref("南皮服务区");
function selectServiceArea(area) {
selectedServiceArea.value = area;
showServiceArea.value = true;
showDropdown.value = false;
}
//
const statistics = ref([
{
image: km,
value: "78.338",
label: "全里程(公里)",
},
{
image: toll,
value: "5",
label: "收费站",
},
{
image: fee,
value: "2",
label: "服务区",
},
{
image: bridge,
value: "7",
label: "桥梁(座)",
},
{
image: hub,
value: "7",
label: "隧道互通(座)",
},
]);
//
const deviceInfo = ref({
deviceType: '视频监控',
cameraType: '枪机',
deviceCode: '3_CAM1_0',
roadSection: '路段名称',
direction: '邯郸方向',
stakeNumber: 'K18+324',
longitude: '44.230145',
latitude: '45.65412',
ipAddress: '13.106.2.10'
});
const deviceInfoTwo = ref({
deviceType: '视频监控',
cameraType: '枪机',
deviceCode: '3_CAM1_0',
roadSection: '路段名称',
direction: '邯郸方向',
stakeNumber: 'K18+324',
longitude: '44.230145',
latitude: '45.65412',
ipAddress: '13.106.2.10'
});
// const deviceInfo = ref({});
function handleShowDeviceInfo(data) {
console.log("接收到设备信息:", data);
//
showDeviceInfo.value = false;
showDeviceInfoTwo.value = false;
if (data.deviceTypeId === "22" || data.deviceTypeId === "44") {
showDeviceInfo.value = true;
} else if (data.deviceTypeId === "15" || data.deviceTypeId === "53") {
showDeviceInfoTwo.value = true;
}
deviceInfoTwo.value = data;
deviceInfo.value = data;
}
const closeDeviceInfo = () => {
showDeviceInfo.value = false;
showDeviceInfoTwo.value = false;
showMap.value = true;
};
const mapFlag = ref(true);
onMounted(() => {
emitter.on("show-device-info", handleShowDeviceInfo);
});
</script>
<style scoped lang="scss">
.center-container {
width: 100%;
// height: 100%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.RealTimeImage{
background-image: url("@/assets/img/eqm/fuwuqu.png");
background-size: 100% 100%;
}
.map-container.bg-header {
position: absolute;
top: 500px;
left: 42.5%;
// transform: translate(-50%, -50%);
z-index: 2000;
// box-shadow: 0 8px 40px rgba(0,0,0,0.3);
}
.bg-header-one {
background-image: url("@/assets/img/eqm/eqminfo.png");
background-size: 100% 100%;
}
.bg-header-two {
background-image: url("@/assets/img/eqm/eqminfo2.png");
background-size: 100% 100%;
}
}
.statistics-panel {
display: flex;
justify-content: space-around;
align-items: center;
width: 40%;
padding: 20px;
gap: 30px;
padding-top: 200px;
padding-bottom: 50px;
.box{
display: flex;
justify-content: space-around;
align-items: center;
}
}
.stat-card {
position: relative;
display: flex;
align-items: center;
padding: 15px 20px;
}
.card-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(0, 170, 255, 0.1) 0%,
transparent 100%);
border-radius: 8px;
opacity: 0.5;
transition: opacity 0.3s ease;
}
.icon-wrapper {
position: relative;
margin-right: 15px;
img {
width: 160px;
height: 160px;
}
}
.stat-content {
display: flex;
flex-direction: column;
position: relative;
}
.stat-value {
.number {
font-size: 64px !important;
font-weight: bold;
color: #1ae9ee;
}
}
.stat-label {
font-size: 32px !important;
color: rgba(255, 255, 255, 0.9);
margin-top: 8px;
letter-spacing: 1px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.map-container {
width: 78%;
margin-top: 30px;
// background: linear-gradient(360deg,
// rgba(15, 43, 89, 0.3) 0%,
// rgba(15, 43, 89, 0) 100%),
// rgba(15, 43, 89, 0.85);
// box-shadow: inset 0px 0px 57px 0px rgba(0, 149, 255, 0.41),
// inset 0px 0px 21px 0px rgba(49, 176, 255, 0.47);
// border-radius: 8px;
// border: 1px solid;
// border-image: linear-gradient(180deg,
// rgba(186, 213, 255, 0.3),
// rgba(186, 213, 255, 0)) 1 1;
// padding: 20px;
// box-sizing: border-box;
height: 1700px;
}
.map-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 15px 40px;
position: relative;
// border-bottom: 1px solid rgba(26, 233, 238, 0.2);
height: 150px;
background: repeating-linear-gradient(-65deg,
rgba(15, 43, 89, 0.3) 0px,
rgba(15, 43, 89, 0.3) 50px,
rgba(26, 233, 238, 0.05) 50px,
rgba(26, 233, 238, 0.05) 100px);
.title-content {
display: flex;
align-items: center;
}
.title-text {
font-size: 52px;
color: #fff;
font-weight: 700;
letter-spacing: 1px;
}
.close-btn {
font-size: 24px;
color: #fff;
cursor: pointer;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
&::before,
&::after {
content: "";
position: absolute;
width: 20px;
height: 2px;
background-color: #fff;
top: 50%;
left: 50%;
}
&::before {
transform: translate(-50%, -50%) rotate(45deg);
}
&::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
&:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
}
}
.map-con {
width: 100%;
height: 90%;
/* background-color: #1ae9ee; */
//
}
.eqm-info {
width: 1788px;
margin-top: 30px;
background: linear-gradient(360deg,
rgba(15, 43, 89, 0.3) 0%,
rgba(15, 43, 89, 0) 100%),
rgba(15, 43, 89, 0.85);
box-shadow: inset 0px 0px 57px 0px rgba(0, 149, 255, 0.41),
inset 0px 0px 21px 0px rgba(49, 176, 255, 0.47);
border-radius: 8px;
border: 1px solid;
border-image: linear-gradient(180deg,
rgba(186, 213, 255, 0.3),
rgba(186, 213, 255, 0)) 1 1;
padding: 20px;
box-sizing: border-box;
height: 1700px;
.eqm-title {
width: 100%;
height: 56px;
background-image: url("../../assets/img/eqm/eqmtitle.png");
}
}
.dropdown {
position: absolute;
right: 0;
bottom: -190px;
background: #112a44;
border: 1px solid #1ae9ee;
border-radius: 6px;
z-index: 100;
min-width: 220px;
min-height: 120px;
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
padding: 16px 0;
.dropdown-item {
padding: 20px 32px;
font-size: 28px;
color: #fff;
cursor: pointer;
text-align: left;
&:hover {
background: #1ae9ee;
color: #112a44;
}
}
}
</style>

View File

@ -1,415 +1,415 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import EventStatus from "./EventStatus.vue";
import EventTask from "./EventTask.vue";
import RealTimeImage from "./RealTimeImage.vue";
import { weatherForecast, weatherHourly, todayStatusCount, todayHourly, getDictData } from "@/api/modules/index";
import { useTodayTime, } from "@/utils/packge";
const { todayTime, getTodayTime } = useTodayTime();
// API
const pendingCount = ref<string>('');
const processingCount = ref<string>('');
const serviceArea = ref<any[]>([]);
const Hub = ref<any[]>([]);
const Intercommunication = ref<any[]>([]);
//
const selectedServiceArea = ref("全部");
const serviceAreas = ref([]);
//
const handleServiceAreaChange = (area: string) => {
selectedServiceArea.value = area;
};
//
const areaTypes = ref([
{ id: "service", name: "服务区", selected: true },
{ id: "interchange", name: "互通", selected: false },
{ id: "hub", name: "枢纽", selected: false },
]);
//
const toggleAreaType = (id: string) => {
areaTypes.value.forEach((type) => {
type.selected = type.id === id;
});
};
onMounted(()=>{
todayStatusCount({todayTime: todayTime.value}).then((res: any) => {
if(res.code === 200){
res.data.forEach((item: any) => {
if(item.status == '0'){ //
pendingCount.value = item.count || 0
} else if(item.status == '2'){ //
processingCount.value = item.count || 0
}
});
}
})
getDictData("hb_service_area").then((res: any) => { //
if (res.code === 200) {
serviceArea.value = res.data || [];
}
});
getDictData("hb_hub").then((res: any) => { //
if (res.code === 200) {
Hub.value = res.data || [];
}
});
getDictData("hb_interchange").then((res: any) => { //
if (res.code === 200) {
Intercommunication.value = res.data || [];
}
});
})
const currentAreaList = computed(() => {
const selectedType = areaTypes.value.find((type) => type.selected);
if (selectedType?.id === "service") {
return serviceArea.value;
} else if (selectedType?.id === "interchange") {
return Intercommunication.value;
} else if (selectedType?.id === "hub") {
return Hub.value;
}
return [];
});
const isDropdownOpen = ref(false);
</script>
<template>
<div class="traffic-flow-grid">
<div class="top">
<div class="traffic-flow-item">
<div style="margin-left: 396px" class="item-title">今日事件处理</div>
<div style="margin-left: 396px" class="item-content">
<EventStatus
:pending-count="pendingCount"
:processing-count="processingCount"
/>
</div>
</div>
<div class="traffic-flow-item">
<div class="item-title">事件处置任务</div>
<div class="item-content">
<EventTask />
</div>
</div>
</div>
<div class="bottom">
<div class="item-title">
<div class="title-text">实时图像</div>
<div class="area-type-selector">
<div
v-for="type in areaTypes"
:key="type.id"
class="area-type-item"
:class="{ active: type.selected }"
@click="toggleAreaType(type.id)"
>
{{ type.name }}
</div>
</div>
<div class="service-area-selector">
<div class="selected-area" @click="isDropdownOpen = !isDropdownOpen">
{{ selectedServiceArea }} <span class="arrow-down"></span>
</div>
<div class="dropdown-menu" v-show="isDropdownOpen">
<div
v-for="(item, index) in currentAreaList"
:key="index"
class="dropdown-item"
:class="{ active: selectedServiceArea === item.dictLabel }"
@click="
handleServiceAreaChange(item.dictLabel);
isDropdownOpen = false;
"
>
{{ item.dictLabel }}
</div>
</div>
</div>
</div>
<div class="item-content">
<RealTimeImage :service-area="selectedServiceArea" />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.traffic-flow-grid {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
margin-right: 20px;
// justify-content: space-between;
// align-items: flex-start;
.top {
width: 100%;
height: 652px;
display: flex;
.traffic-flow-item {
flex: 1;
.item-title {
width: 1080px;
height: 104px;
background-image: url("../../assets/img/leftoright/title.png");
margin-left: 48px;
margin-top: 48px;
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
font-weight: 700;
font-size: 52px;
color: #ffffff;
line-height: 104px;
padding-left: 56px;
letter-spacing: 2px;
text-shadow: 0px 4px 0px #095a8c;
text-align: left;
font-style: normal;
text-transform: none;
}
.item-content {
width: 1080px;
height: 468px;
margin-left: 48px;
margin-top: 32px;
background-image: url("../../assets/img/leftoright/rightcontent.png");
}
}
}
.bottom {
flex: 1;
width: 100%;
height: 100%;
margin-top: 48px;
.item-title {
width: 2208px;
height: 104px;
margin-left: 396px;
background-image: url("../../assets/img/leftoright/bigtitle.png");
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
font-weight: 700;
font-size: 52px;
color: #ffffff;
line-height: 104px;
padding-left: 56px;
letter-spacing: 2px;
text-shadow: 0px 4px 0px #095a8c;
text-align: left;
font-style: normal;
text-transform: none;
position: relative;
display: flex;
justify-content: flex-start;
align-items: center;
padding-right: 56px;
.title-text {
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
font-weight: 700;
font-size: 52px;
color: #ffffff;
letter-spacing: 2px;
text-shadow: 0px 4px 0px #095a8c;
text-align: left;
font-style: normal;
text-transform: none;
}
.area-type-selector {
position: absolute;
right: 320px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 20px;
font-size: 32px;
font-weight: normal;
color: #ffffff;
text-shadow: none;
z-index: 99;
.area-type-item {
width: 150px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
color: #bad5ff;
position: relative;
border: 1px solid #bad5ff;
border-radius: 4px;
background-color: #304b75;
box-shadow: inset 0 0 10px rgba(0, 79, 127, 0.3),
0 0 5px rgba(0, 198, 255, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
overflow: hidden;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(0, 198, 255, 0.1) 0%,
rgba(0, 118, 170, 0.05) 40%,
transparent 100%
);
z-index: -1;
border-radius: 3px;
}
&.active {
color: #acc0df;
border: 1px solid rgba(0, 198, 255, 0.7);
box-shadow: inset 0 0 15px rgba(0, 79, 127, 0.6),
0 0 10px rgba(0, 198, 255, 0.5);
background-color: rgba(0, 67, 123, 0.7);
text-shadow: 0 0 8px rgba(0, 198, 255, 0.5);
&::before {
background: linear-gradient(
135deg,
rgba(0, 198, 255, 0.3) 0%,
rgba(0, 118, 170, 0.15) 50%,
rgba(0, 79, 127, 0.05) 100%
);
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(
90deg,
transparent 0%,
rgba(0, 198, 255, 0.8) 50%,
transparent 100%
);
}
}
&:hover:not(.active) {
border-color: rgba(0, 198, 255, 0.5);
box-shadow: inset 0 0 12px rgba(0, 79, 127, 0.4),
0 0 8px rgba(0, 198, 255, 0.3);
background-color: rgba(32, 64, 113, 0.6);
color: #d8e8ff;
}
}
}
.service-area-selector {
position: absolute;
right: 56px;
top: 50%;
transform: translateY(-50%);
font-size: 32px;
font-weight: normal;
color: #ffffff;
display: inline-block;
text-shadow: none;
z-index: 99;
.selected-area {
display: flex;
align-items: center;
justify-content: space-between;
background: #304b75;
border: 1px solid #718cb6;
border-radius: 4px;
padding: 0 20px;
height: 64px;
cursor: pointer;
width: 240px;
color: #ffffff;
position: relative;
.arrow-down {
font-size: 18px;
color: #ffffff;
margin-left: 4px;
}
}
.dropdown-menu {
position: absolute;
top: 66px;
right: 0;
background: #003366;
border: 1px solid rgba(0, 198, 255, 0.5);
border-radius: 0 0 4px 4px;
width: 240px;
z-index: 100;
max-height: 200px;
overflow-y: auto;
padding-top: 8px;
transform-origin: top right;
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 27, 61, 0.3);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 198, 255, 0.5);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 198, 255, 0.7);
}
&::before {
content: "";
position: absolute;
top: 0;
left: 10px;
right: 10px;
height: 1px;
border-top: 1px dashed rgba(0, 198, 255, 0.5);
}
.dropdown-item {
cursor: pointer;
transition: background 0.2s;
text-align: center;
&:hover,
&.active {
background: #304b75;
}
&.active {
color: #00c6ff;
}
}
}
}
}
.item-content {
width: 2208px;
height: 1428px;
margin-left: 396px;
margin-top: 32px;
background-image: url("../../assets/img/leftoright/rbcontent.png");
}
}
}
</style>
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import EventStatus from "./EventStatus.vue";
import EventTask from "./EventTask.vue";
import RealTimeImage from "./RealTimeImage.vue";
import { weatherForecast, weatherHourly, todayStatusCount, todayHourly, getDictData } from "@/api/modules/index";
import { useTodayTime, } from "@/utils/packge";
const { todayTime, getTodayTime } = useTodayTime();
// API
const pendingCount = ref<string>('');
const processingCount = ref<string>('');
const serviceArea = ref<any[]>([]);
const Hub = ref<any[]>([]);
const Intercommunication = ref<any[]>([]);
//
const selectedServiceArea = ref("全部");
const serviceAreas = ref([]);
//
const handleServiceAreaChange = (area: string) => {
selectedServiceArea.value = area;
};
//
const areaTypes = ref([
{ id: "service", name: "服务区", selected: true },
{ id: "interchange", name: "互通", selected: false },
{ id: "hub", name: "枢纽", selected: false },
]);
//
const toggleAreaType = (id: string) => {
areaTypes.value.forEach((type) => {
type.selected = type.id === id;
});
};
onMounted(()=>{
todayStatusCount({todayTime: todayTime.value}).then((res: any) => {
if(res.code === 200){
res.data.forEach((item: any) => {
if(item.status == '0'){ //
pendingCount.value = item.count || 0
} else if(item.status == '2'){ //
processingCount.value = item.count || 0
}
});
}
})
getDictData("hb_service_area").then((res: any) => { //
if (res.code === 200) {
serviceArea.value = res.data || [];
}
});
getDictData("hb_hub").then((res: any) => { //
if (res.code === 200) {
Hub.value = res.data || [];
}
});
getDictData("hb_interchange").then((res: any) => { //
if (res.code === 200) {
Intercommunication.value = res.data || [];
}
});
})
const currentAreaList = computed(() => {
const selectedType = areaTypes.value.find((type) => type.selected);
if (selectedType?.id === "service") {
return serviceArea.value;
} else if (selectedType?.id === "interchange") {
return Intercommunication.value;
} else if (selectedType?.id === "hub") {
return Hub.value;
}
return [];
});
const isDropdownOpen = ref(false);
</script>
<template>
<div class="traffic-flow-grid">
<div class="top">
<div class="traffic-flow-item">
<div style="margin-left: 396px" class="item-title">今日事件处理</div>
<div style="margin-left: 396px" class="item-content">
<EventStatus
:pending-count="pendingCount"
:processing-count="processingCount"
/>
</div>
</div>
<div class="traffic-flow-item">
<div class="item-title">事件处置任务</div>
<div class="item-content">
<EventTask />
</div>
</div>
</div>
<div class="bottom">
<div class="item-title">
<div class="title-text">实时图像</div>
<div class="area-type-selector">
<div
v-for="type in areaTypes"
:key="type.id"
class="area-type-item"
:class="{ active: type.selected }"
@click="toggleAreaType(type.id)"
>
{{ type.name }}
</div>
</div>
<div class="service-area-selector">
<div class="selected-area" @click="isDropdownOpen = !isDropdownOpen">
{{ selectedServiceArea }} <span class="arrow-down"></span>
</div>
<div class="dropdown-menu" v-show="isDropdownOpen">
<div
v-for="(item, index) in currentAreaList"
:key="index"
class="dropdown-item"
:class="{ active: selectedServiceArea === item.dictLabel }"
@click="
handleServiceAreaChange(item.dictLabel);
isDropdownOpen = false;
"
>
{{ item.dictLabel }}
</div>
</div>
</div>
</div>
<div class="item-content">
<RealTimeImage :service-area="selectedServiceArea" />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.traffic-flow-grid {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
margin-right: 20px;
// justify-content: space-between;
// align-items: flex-start;
.top {
width: 100%;
height: 652px;
display: flex;
.traffic-flow-item {
flex: 1;
.item-title {
width: 1080px;
height: 104px;
background-image: url("../../assets/img/leftoright/title.png");
margin-left: 48px;
margin-top: 48px;
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
font-weight: 700;
font-size: 52px;
color: #ffffff;
line-height: 104px;
padding-left: 56px;
letter-spacing: 2px;
text-shadow: 0px 4px 0px #095a8c;
text-align: left;
font-style: normal;
text-transform: none;
}
.item-content {
width: 1080px;
height: 468px;
margin-left: 48px;
margin-top: 32px;
background-image: url("../../assets/img/leftoright/rightcontent.png");
}
}
}
.bottom {
flex: 1;
width: 100%;
height: 100%;
margin-top: 48px;
.item-title {
width: 2208px;
height: 104px;
margin-left: 396px;
background-image: url("../../assets/img/leftoright/bigtitle.png");
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
font-weight: 700;
font-size: 52px;
color: #ffffff;
line-height: 104px;
padding-left: 56px;
letter-spacing: 2px;
text-shadow: 0px 4px 0px #095a8c;
text-align: left;
font-style: normal;
text-transform: none;
position: relative;
display: flex;
justify-content: flex-start;
align-items: center;
padding-right: 56px;
.title-text {
font-family: Alimama ShuHeiTi, Alimama ShuHeiTi;
font-weight: 700;
font-size: 52px;
color: #ffffff;
letter-spacing: 2px;
text-shadow: 0px 4px 0px #095a8c;
text-align: left;
font-style: normal;
text-transform: none;
}
.area-type-selector {
position: absolute;
right: 320px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 20px;
font-size: 32px;
font-weight: normal;
color: #ffffff;
text-shadow: none;
z-index: 99;
.area-type-item {
width: 150px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
color: #bad5ff;
position: relative;
border: 1px solid #bad5ff;
border-radius: 4px;
background-color: #304b75;
box-shadow: inset 0 0 10px rgba(0, 79, 127, 0.3),
0 0 5px rgba(0, 198, 255, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
overflow: hidden;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(0, 198, 255, 0.1) 0%,
rgba(0, 118, 170, 0.05) 40%,
transparent 100%
);
z-index: -1;
border-radius: 3px;
}
&.active {
color: #acc0df;
border: 1px solid rgba(0, 198, 255, 0.7);
box-shadow: inset 0 0 15px rgba(0, 79, 127, 0.6),
0 0 10px rgba(0, 198, 255, 0.5);
background-color: rgba(0, 67, 123, 0.7);
text-shadow: 0 0 8px rgba(0, 198, 255, 0.5);
&::before {
background: linear-gradient(
135deg,
rgba(0, 198, 255, 0.3) 0%,
rgba(0, 118, 170, 0.15) 50%,
rgba(0, 79, 127, 0.05) 100%
);
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(
90deg,
transparent 0%,
rgba(0, 198, 255, 0.8) 50%,
transparent 100%
);
}
}
&:hover:not(.active) {
border-color: rgba(0, 198, 255, 0.5);
box-shadow: inset 0 0 12px rgba(0, 79, 127, 0.4),
0 0 8px rgba(0, 198, 255, 0.3);
background-color: rgba(32, 64, 113, 0.6);
color: #d8e8ff;
}
}
}
.service-area-selector {
position: absolute;
right: 56px;
top: 50%;
transform: translateY(-50%);
font-size: 32px;
font-weight: normal;
color: #ffffff;
display: inline-block;
text-shadow: none;
z-index: 99;
.selected-area {
display: flex;
align-items: center;
justify-content: space-between;
background: #304b75;
border: 1px solid #718cb6;
border-radius: 4px;
padding: 0 20px;
height: 64px;
cursor: pointer;
width: 240px;
color: #ffffff;
position: relative;
.arrow-down {
font-size: 18px;
color: #ffffff;
margin-left: 4px;
}
}
.dropdown-menu {
position: absolute;
top: 66px;
right: 0;
background: #003366;
border: 1px solid rgba(0, 198, 255, 0.5);
border-radius: 0 0 4px 4px;
width: 240px;
z-index: 100;
max-height: 200px;
overflow-y: auto;
padding-top: 8px;
transform-origin: top right;
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 27, 61, 0.3);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 198, 255, 0.5);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(0, 198, 255, 0.7);
}
&::before {
content: "";
position: absolute;
top: 0;
left: 10px;
right: 10px;
height: 1px;
border-top: 1px dashed rgba(0, 198, 255, 0.5);
}
.dropdown-item {
cursor: pointer;
transition: background 0.2s;
text-align: center;
&:hover,
&.active {
background: #304b75;
}
&.active {
color: #00c6ff;
}
}
}
}
}
.item-content {
width: 2208px;
height: 1428px;
margin-left: 396px;
margin-top: 32px;
background-image: url("../../assets/img/leftoright/rbcontent.png");
}
}
}
</style>

View File

@ -1,13 +1,13 @@
/** @type {import('tailwindcss').Config} */
// const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts}'],
theme: {
extend: {
// fontFamily: {
// sans: ['"Inter var"', ...defaultTheme.fontFamily.sans],
// },
},
},
plugins: [],
}
/** @type {import('tailwindcss').Config} */
// const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts}'],
theme: {
extend: {
// fontFamily: {
// sans: ['"Inter var"', ...defaultTheme.fontFamily.sans],
// },
},
},
plugins: [],
}

View File

@ -1,13 +1,13 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"exclude": ["node_modules","public"],
"include": [
"vite.config.ts",
"config/**/*.ts",
"config/**/*.d.ts",
]
}
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"exclude": ["node_modules","public"],
"include": [
"vite.config.ts",
"config/**/*.ts",
"config/**/*.d.ts",
]
}

View File

@ -1,46 +1,46 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom"
],
"baseUrl": "./",
"types": [
"node",
"vite/client",
"element-plus/global"
],
"paths": {
"@/*": [
"src/*"
],
"api/*": [
"src/api/*"
],
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/env.d.ts",
"types/*.d.ts"
],
"exclude": ["node_modules","public"],
"references": [
{
"path": "./tsconfig.config.json"
}
]
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom"
],
"baseUrl": "./",
"types": [
"node",
"vite/client",
"element-plus/global"
],
"paths": {
"@/*": [
"src/*"
],
"api/*": [
"src/api/*"
],
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/env.d.ts",
"types/*.d.ts"
],
"exclude": ["node_modules","public"],
"references": [
{
"path": "./tsconfig.config.json"
}
]
}

12
types/env.d.ts vendored
View File

@ -1,6 +1,6 @@
declare module "*.vue" {
import { defineComponent } from "vue";
const Component: ReturnType<typeof defineComponent>;
export default Component;
}
declare module "*.vue" {
import { defineComponent } from "vue";
const Component: ReturnType<typeof defineComponent>;
export default Component;
}

8
types/global.d.ts vendored
View File

@ -1,5 +1,5 @@
interface Window {
$message: any
}
interface Window {
$message: any
}
type TimeProp= NodeJS.Timeout

View File

@ -1,86 +1,86 @@
/*
* @Author: 996555510 65213605+996555510@users.noreply.github.com
* @Date: 2025-05-22 10:12:42
* @LastEditors: 996555510 65213605+996555510@users.noreply.github.com
* @LastEditTime: 2025-05-23 15:55:37
* @FilePath: \IofTV-Screen-Vue3-master\vite.config.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
/// <reference types="vite/client" />
import type { UserConfig, ConfigEnv } from 'vite';
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from "path";
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
//https://github.com/element-plus/unplugin-element-plus/blob/HEAD/README.zh-CN.md
import ElementPlus from 'unplugin-element-plus/vite'
import {createHtmlPlugin} from 'vite-plugin-html';
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
const viteEnv = loadEnv(mode, process.cwd());
return {
base: '/',
plugins: [
vue(),
AutoImport({ resolvers: [ElementPlusResolver()] }),
Components({ resolvers: [ElementPlusResolver()] }),
ElementPlus(),
createHtmlPlugin({
inject: {
data: {
injectMapScript: `<script src="./public/static/map_load.js"></script>`,
},
},
minify: true,
}),
// Compression({
// algorithm: 'gzip',
// test: /\.(js|css|html|json|svg|jpe?g|png|gif)$/i,
// threshold: 1024,
// deleteOriginalAssets: false
// }),
// svgr()
],
publicDir: "public",
build: {
outDir: 'dist',
assetsDir: 'static'
},
server: {
host: '0.0.0.0',
port: 8080,
open: true,
strictPort: false,
proxy: {
[viteEnv.VITE_APP_CONTROL_BASE_API]: {
target: 'http://172.16.1.128:8090/xjIotApi',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${viteEnv.VITE_APP_CONTROL_BASE_API}`), ''),
},
[viteEnv.VITE_APP_BASE_API]: {
target: 'http://172.16.1.128:8090/iotApi',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${viteEnv.VITE_APP_BASE_API}`), ''),
}
},
},
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
"components": resolve(__dirname, "./src/components"),
"api": resolve(__dirname, "./src/api")
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "./src/assets/css/variable.scss" as *;`,
},
},
}
};
/*
* @Author: 996555510 65213605+996555510@users.noreply.github.com
* @Date: 2025-05-22 10:12:42
* @LastEditors: 996555510 65213605+996555510@users.noreply.github.com
* @LastEditTime: 2025-05-23 15:55:37
* @FilePath: \IofTV-Screen-Vue3-master\vite.config.ts
* @Description: ,`customMade`, koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
/// <reference types="vite/client" />
import type { UserConfig, ConfigEnv } from 'vite';
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from "path";
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
//https://github.com/element-plus/unplugin-element-plus/blob/HEAD/README.zh-CN.md
import ElementPlus from 'unplugin-element-plus/vite'
import {createHtmlPlugin} from 'vite-plugin-html';
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
const viteEnv = loadEnv(mode, process.cwd());
return {
base: '/',
plugins: [
vue(),
AutoImport({ resolvers: [ElementPlusResolver()] }),
Components({ resolvers: [ElementPlusResolver()] }),
ElementPlus(),
createHtmlPlugin({
inject: {
data: {
injectMapScript: `<script src="./public/static/map_load.js"></script>`,
},
},
minify: true,
}),
// Compression({
// algorithm: 'gzip',
// test: /\.(js|css|html|json|svg|jpe?g|png|gif)$/i,
// threshold: 1024,
// deleteOriginalAssets: false
// }),
// svgr()
],
publicDir: "public",
build: {
outDir: 'dist',
assetsDir: 'static'
},
server: {
host: '0.0.0.0',
port: 8080,
open: true,
strictPort: false,
proxy: {
[viteEnv.VITE_APP_CONTROL_BASE_API]: {
target: 'http://172.16.1.133:8081/xjIotApi',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${viteEnv.VITE_APP_CONTROL_BASE_API}`), ''),
},
[viteEnv.VITE_APP_BASE_API]: {
target: 'http://172.16.1.133:8081/iotApi',
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${viteEnv.VITE_APP_BASE_API}`), ''),
}
},
},
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
"components": resolve(__dirname, "./src/components"),
"api": resolve(__dirname, "./src/api")
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "./src/assets/css/variable.scss" as *;`,
},
},
}
};
});