refactor: 更新commit,从头做起

This commit is contained in:
daidai 2024-08-20 15:25:39 +08:00
commit 252e3bacb2
129 changed files with 107564 additions and 0 deletions

0
.env.development Normal file
View File

0
.env.production Normal file
View File

29
.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
components.d.ts
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

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

21
LICENSE Normal file
View File

@ -0,0 +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.

322
README.md Normal file
View File

@ -0,0 +1,322 @@
## 项目描述
[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可显示南海
```
## 全局参数
### filter
监测数据项统一过滤,保留两位小数。
```vue
{{10.23123|montionFilter }}
```
## 大屏交流反馈(面条的群)
### 大屏QQ群
QQ群号
一群713105837 (已满)
二群495755841

9
auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
}

1
env.d.ts vendored Normal file
View File

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

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!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>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

4016
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"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",
"mockjs": "^1.1.0",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-echarts": "^6.6.9",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@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"
}
}

6
postcss.config.js Normal file
View File

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

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"adcode":710000,"name":"台湾省","center":[121.509062,25.044332],"centroid":[120.971485,23.749452],"childrenNum":0,"level":"province","acroutes":[100000],"parent":{"adcode":100000}},"geometry":{"type":"MultiPolygon","coordinates":[[[[120.443558,22.441245],[120.517584,22.408536],[120.569903,22.361728],[120.640505,22.241347],[120.659209,22.15432],[120.662001,22.066983],[120.651464,22.033165],[120.667691,21.983168],[120.70157,21.927065],[120.743246,21.915569],[120.78155,21.923957],[120.85468,21.883333],[120.87291,21.897387],[120.866482,21.98436],[120.907315,22.033208],[120.904154,22.119757],[120.914955,22.302718],[120.981658,22.528305],[121.015009,22.584168],[121.033292,22.650725],[121.078498,22.669656],[121.170544,22.723133],[121.210481,22.770665],[121.237931,22.836327],[121.324708,22.945666],[121.354687,23.01006],[121.370388,23.084347],[121.409535,23.102669],[121.430294,23.137196],[121.415015,23.195973],[121.440358,23.272096],[121.479558,23.3223],[121.497788,23.419789],[121.521497,23.483198],[121.523078,23.538708],[121.587778,23.76102],[121.621604,23.92075],[121.659381,24.006893],[121.639992,24.064276],[121.643838,24.097713],[121.678085,24.133906],[121.689044,24.174401],[121.809172,24.339055],[121.826717,24.423579],[121.867498,24.478978],[121.885464,24.529677],[121.892524,24.617912],[121.862598,24.671515],[121.837993,24.76015],[121.845053,24.836269],[121.932883,24.938645],[122.012178,25.001469],[121.980776,25.03079],[121.947425,25.031955],[121.917077,25.137908],[121.842155,25.135332],[121.782407,25.160425],[121.750531,25.160716],[121.707327,25.191493],[121.700319,25.226913],[121.655324,25.241859],[121.623026,25.294694],[121.584986,25.308926],[121.535038,25.307515],[121.444415,25.270624],[121.413487,25.238912],[121.371864,25.159885],[121.319281,25.140691],[121.209322,25.127104],[121.133135,25.078728],[121.102102,25.075153],[121.024704,25.040479],[121.009688,24.993649],[120.960899,24.940227],[120.908475,24.852012],[120.892299,24.767526],[120.823753,24.688321],[120.762371,24.658335],[120.688661,24.600678],[120.64277,24.490172],[120.589187,24.432354],[120.546299,24.370413],[120.521009,24.312038],[120.470534,24.24259],[120.451461,24.182691],[120.392029,24.11824],[120.316158,23.984881],[120.278276,23.927798],[120.245768,23.840553],[120.175377,23.807385],[120.102773,23.700981],[120.094817,23.587466],[120.121741,23.504664],[120.107831,23.341264],[120.081434,23.29191],[120.018947,23.073115],[120.029537,23.048623],[120.131382,23.002118],[120.149138,22.896715],[120.200403,22.721101],[120.274272,22.560181],[120.297191,22.531315],[120.443558,22.441245]]],[[[124.542984,25.903911],[124.586346,25.913777],[124.572805,25.93974],[124.541825,25.931031],[124.542984,25.903911]]],[[[123.445286,25.725966],[123.472104,25.713024],[123.508933,25.723237],[123.514834,25.751226],[123.483063,25.768587],[123.444496,25.746514],[123.445286,25.725966]]],[[[119.64597,23.55091],[119.701081,23.550657],[119.678057,23.600041],[119.610089,23.603953],[119.594388,23.577245],[119.566306,23.584732],[119.562565,23.530377],[119.573788,23.505885],[119.609141,23.503864],[119.64597,23.55091]]],[[[123.667207,25.914066],[123.707092,25.916873],[123.678008,25.938667],[123.667207,25.914066]]],[[[119.506031,23.625567],[119.505241,23.575814],[119.472416,23.557136],[119.523207,23.563699],[119.525578,23.624895],[119.506031,23.625567]]],[[[119.49739,23.386683],[119.495125,23.350156],[119.516885,23.349903],[119.49739,23.386683]]],[[[119.557454,23.666474],[119.604083,23.616989],[119.615516,23.660925],[119.586485,23.675974],[119.557454,23.666474]]],[[[121.46823,22.676644],[121.476502,22.64166],[121.513541,22.631833],[121.5147,22.67639],[121.46823,22.676644]]],[[[121.510538,22.087185],[121.507693,22.048523],[121.534089,22.022146],[121.594522,21.995382],[121.604586,22.022699],[121.575028,22.037122],[121.575607,22.084421],[121.510538,22.087185]]],[[[122.097533,25.500168],[122.093581,25.47183],[122.124825,25.475932],[122.097533,25.500168]]],[[[119.421467,23.216684],[119.421309,23.18935],[119.453396,23.217697],[119.421467,23.216684]]],[[[120.355042,22.327259],[120.395454,22.342287],[120.383072,22.355573],[120.355042,22.327259]]]]}}]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

98604
public/map-geojson/china.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

12
src/App.vue Normal file
View File

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

253
src/api/api.ts Normal file
View File

@ -0,0 +1,253 @@
/*
* @LastEditors: daidaibg@163.com
* @LastEditTime: 2024-03-28 16:52:31
*/
import axios from "axios";
import type { AxiosRequestConfig, AxiosResponse } from "axios";
import { StorageEnum, RequestEnum } from "@/enums";
import { getLocalStorage } from "@/utils";
import UtilVar from "../config/UtilVar";
let baseUrl = UtilVar.baseUrl;
const CancelToken = axios.CancelToken;
export { baseUrl };
// axios.defaults.withCredentials = true;
// 添加请求拦截器
axios.interceptors.request.use(
function (config: AxiosRequestConfig): any {
// 在发送请求之前做些什么 传token
let token: any = getLocalStorage(StorageEnum.GB_TOKEN_STORE);
if (token) {
// @ts-ignore
config.headers.common[RequestEnum.GB_TOKEN_KEY] = token;
}
// @ts-ignore
config.headers["Content-Type"] = "application/json;charset=utf-8";
return config;
},
function (error: any) {
// 对请求错误做些什么
console.log(error);
return Promise.reject(error);
}
);
export type Params = { [key: string]: string | number };
export type FileConfig = {
setCancel?: Function;
onProgress?: Function;
[key: string]: any;
};
/**
* @响应拦截
*/
axios.interceptors.response.use(
(response: AxiosResponse) => {
// console.log("response", response);
if (response.status !== 200) {
return Promise.reject(response);
}
/**
* @code token验证失败
*/
if (response.data.code == UtilVar.code) {
// router.push("/login")
return Promise.resolve(response);
}
return Promise.resolve(response);
},
(error: any) => {
console.log("error", error);
let err = {
success: false,
msg: "未知异常,请联系管理员!",
code: 400,
};
if (JSON.stringify(error).indexOf("Network Error") != -1) {
err.msg = "网络错误或服务错误!";
}
if (error.message == "canceled") {
err.msg = "取消请求";
err.code = 488;
}
// console.log(err);
return Promise.reject(err);
}
);
//判断是否是加密参数,是的话处理
let isEncryptionParam = (params: Params) => {
return params;
};
/**
* @description: get
* @param {string} url
* @param {Params} params
* @return {*}
*/
export const GET = async (url: string, params: Params): Promise<any> => {
try {
params = isEncryptionParam(params);
const data = await axios.get(`${baseUrl}${url}`, {
params: params,
});
return data.data;
} catch (error: any) {
return Promise.reject(error.msg);
}
};
/**
* @description: post请求方法
* @param {any} url
* @param {any} params
* @return {any}
*/
export const POST = async (url: string, params: Params): Promise<any> => {
try {
params = isEncryptionParam(params);
const data = await axios.post(`${baseUrl}${url}`, params);
return data.data;
} catch (error) {
return Promise.reject(error);
}
};
/**
* @description: 访
* @param {string} url
* @param {Params} params
* @return {*}
*/
export const GETNOBASE = async (url: string, params?: Params): Promise<any> => {
try {
const data = await axios.get(url, {
params: params,
});
return data.data;
} catch (error) {
return Promise.reject(error);
}
};
// 定义文件类型提交方法
interface fileconfigs {
[headers: string]: {
"Content-Type": string;
};
}
let configs: fileconfigs = {
headers: { "Content-Type": "multipart/form-data" },
};
/**
* @description: @文件类型提交方法
* @param {string} url
* @param {Params} params
* @param {FileConfig} config
* @return {*}
*/
export const FILEPOST = async (url: string, params: Params, config: FileConfig = {}): Promise<any> => {
try {
const data = await axios.post(`${baseUrl}${url}`, params, {
...configs,
cancelToken: new CancelToken(function executor(c: any) {
config.setCancel && config.setCancel(c);
}),
// 上传进度
onUploadProgress: (e: any) => {
if (e.total > 0) {
e.percent = (e.loaded / e.total) * 100;
}
config.onProgress && config.onProgress(e);
},
});
return data;
} catch (error) {
return Promise.reject(error);
}
};
/**
*
* @param {config.responseType} arraybuffer || blod
*/
export const FILE = async (config: FileConfig = {}) => {
try {
const data = await axios({
method: config.method || "get",
url: `${baseUrl}${config.url}`,
data: config.body || {},
params: config.param || {},
responseType: config.responseType || "blod",
onDownloadProgress: (e: any) => {
// console.log(e,e.currentTarget)
// if (e.currentTarget.response.size > 0) {
// e.percent = e.loaded / e.currentTarget.response.size * 100;
// }
// event.srcElement.getResponseHeader('content-length')
config.onProgress && config.onProgress(e);
},
});
return data;
} catch (error) {
return Promise.reject(error);
}
};
export const PUT = async (url: string, params: Params) => {
try {
params = isEncryptionParam(params);
const data = await axios.put(`${baseUrl}${url}`, params);
return data.data;
} catch (error) {
return Promise.reject(error);
}
};
export const DELETE = async (url: string, params: Params) => {
// console.log(params)
try {
params = isEncryptionParam(params);
const data = await axios.delete(`${baseUrl}${url}`, { data: params });
return data.data;
} catch (error) {
return Promise.reject(error);
}
};
// switch (error.response?.status) {
// case 400:
// error.message = '请求错误(400)';
// break;
// case 401:
// error.message = '未授权(401)';
// break;
// case 403:
// error.message = '拒绝访问(403)';
// break;
// case 404:
// error.message = '请求出错(404)';
// break;
// case 408:
// error.message = '请求超时(408)';
// break;
// case 500:
// error.message = '服务器错误(500)';
// break;
// case 501:
// error.message = '服务未实现(501)';
// break;
// case 502:
// error.message = '网络错误(502)';
// break;
// case 503:
// error.message = '服务不可用(503)';
// break;
// case 504:
// error.message = '网络超时(504)';
// break;
// case 505:
// error.message = 'HTTP版本不受支持(505)';
// break;
// default:
// error.message = `连接出错(${error.response?.status})!`;
// }

11
src/api/index.ts Normal file
View File

@ -0,0 +1,11 @@
/*
* @Author: daidai
* @Date: 2021-12-23 11:18:37
* @LastEditTime: 2024-03-28 16:07:20
* @FilePath: \web-pc-svn\src\api\modules\index.js
*/
import {GETNOBASE} from "./api";
export * from "./modules/index"
export {GETNOBASE}

54
src/api/modules/index.ts Normal file
View File

@ -0,0 +1,54 @@
import {GET,POST,FILE,FILEPOST,PUT,GETNOBASE} from "../api";
const indexUrl= {
'leftTop':'/bigscreen/countDeviceNum',//左上
'leftCenter':'/bigscreen/countUserNum',//左中
"centerMap":"/bigscreen/centerMap",
"centerBottom":"/bigscreen/installationPlan",
'leftBottom':"/bigscreen/leftBottom", //坐下
'rightTop':"/bigscreen/alarmNum", //报警次数
'rightBottom':'/bigscreen/rightBottom',//右下
'rightCenter':'/bigscreen/ranking',// 报警排名
}
export default indexUrl
/**左上--设备内总览 */
export const countDeviceNum=(param:any={})=>{
return GET(indexUrl.leftTop,param)
}
/**左中--用户总览 */
export const countUserNum=(param:any={})=>{
return GET(indexUrl.leftCenter,param)
}
/**左下--设备提醒 */
export const leftBottom=(param:any={})=>{
return GET(indexUrl.leftBottom,param)
}
/**中上--地图 */
export const centerMap=(param:any={})=>{
return GET(indexUrl.centerMap,param)
}
/**中下--安装计划 */
export const installationPlan=(param:any={})=>{
return GET(indexUrl.centerBottom,param)
}
/**右上--报警次数 */
export const alarmNum=(param:any={})=>{
return GET(indexUrl.rightTop,param)
}
/**右中--报警排名 */
export const ranking=(param:any={})=>{
return GET(indexUrl.rightCenter,param)
}
/**右下--设备状态 */
export const rightBottom=(param:any={})=>{
return GET(indexUrl.rightBottom,param)
}

98
src/assets/css/main.scss Normal file
View File

@ -0,0 +1,98 @@
html,body{
height: 100%;
width: 100%;
}
#app{
.content_wrap{
color: #d3d6dd;
}
}
html .el-message {
--yh-bg-color-container:#242424;
--yh-shadow-3: 0 16px 24px rgba(0, 0, 0, .14), 0 6px 30px rgba(0, 0, 0, 12%), 0 8px 10px rgba(0, 0, 0, 20%);
--yh-shadow-inset-top: inset 0 .5px 0 #5e5e5e;
--yh-shadow-inset-right: inset .5px 0 0 #5e5e5e;
--yh-shadow-inset-bottom: inset 0 -.5px 0 #5e5e5e;
--yh-shadow-inset-left: inset -.5px 0 0 #5e5e5e;
--yh-text-color-primary:rgba(255, 255, 255, .9);
--yh-brand-color: #0052d9;
--yh-success-color: #059465;
--yh-error-color: #c64751;
--yh-warning-color: #cf6e2d;
background-color: var(--yh-bg-color-container) ;
box-shadow: var(--yh-shadow-3), var(--yh-shadow-inset-top),
var(--yh-shadow-inset-right), var(--yh-shadow-inset-bottom),
var(--yh-shadow-inset-left);
border: none ;
color: var(--yh-text-color-primary) ;
margin-top: 90px;
.el-message__icon {
font-size: 20px;
}
&.el-message--info .el-message__icon {
color: var(--yh-brand-color);
}
&.el-message--success .el-message__icon {
color: var(--yh-success-color);
}
&.el-message--warning .el-message__icon {
color: var(--yh-warning-color);
}
&.el-message--error .el-message__icon {
color: var(--yh-error-color);
}
.el-message__content {
color: var(--yh-text-color-primary);
}
}
.beautify-scroll-def {
overflow-y: auto;
&::-webkit-scrollbar-thumb {
//滚动条的设置
background-color: rgba(14, 59, 150, 0);
background-clip: padding-box;
border-radius: 4px;
}
&:hover {
&::-webkit-scrollbar-thumb {
//滚动条的设置
background-color: rgba(14, 59, 150, 0.5);
background-clip: padding-box;
border-radius: 4px;
}
}
&::-webkit-scrollbar-track-piece {
//滚动条凹槽的颜色还可以设置边框属性
background-color: transparent;
}
&::-webkit-scrollbar {
//滚动条的宽度
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(14, 59, 150, .8);
}
}
.text-content {
width: 100%;
display: flex;
min-height: calc(100% - 60px);
justify-content: space-between;
}

View File

@ -0,0 +1,7 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
/* @tailwind base;
@tailwind components;
@tailwind utilities; */

View File

@ -0,0 +1 @@
$primary-color: #1890ff;

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
src/assets/img/guang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
src/assets/img/pageBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src/assets/img/top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
src/assets/img/xieyou.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
src/assets/img/xiezuo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

View File

@ -0,0 +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>

View File

@ -0,0 +1,146 @@
<script lang="ts">
export type { CountUp as ICountUp, CountUpOptions } from 'countup.js'
export default {
name: 'CountUp'
}
</script>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { CountUp } from 'countup.js'
import type { CountUpOptions } from 'countup.js'
const props = withDefaults(
defineProps<{
//
endVal: number | string
//
startVal?: number | string
// s
duration?: number | string
//
autoplay?: boolean
// /
loop?: boolean | number | string
// s
delay?: number
// countup
options?: CountUpOptions
}>(),
{
startVal: 0,
duration: 2.5,
autoplay: true,
loop: false,
delay: 0,
options: undefined
}
)
const emits = defineEmits<{
// countup init complete
(event: 'init', countup: CountUp): void
// count complete
(event: 'finished'): void
}>()
let elRef = ref<HTMLElement>()
let countUp = ref<CountUp>()
const initCountUp = () => {
if (!elRef.value) return
const startVal = Number(props.startVal)
const endVal = Number(props.endVal)
const duration = Number(props.duration)
countUp.value = new CountUp(elRef.value, endVal, {
startVal,
duration,
...props.options
})
if (countUp.value.error) {
console.error(countUp.value.error)
return
}
emits('init', countUp.value)
}
const startAnim = (cb?: () => void) => {
countUp.value?.start(cb)
}
// endVal change & autoplay: true, restart animate
watch(
() => props.endVal,
(value) => {
if (props.autoplay) {
countUp.value?.update(value)
}
}
)
// loop animation
const finished = ref(false)
let loopCount = 0
const loopAnim = () => {
loopCount++
startAnim(() => {
const isTruely = typeof props.loop === 'boolean' && props.loop
if (isTruely || props.loop > loopCount) {
delay(() => {
countUp.value?.reset()
loopAnim()
}, props.delay)
} else {
finished.value = true
}
})
}
watch(finished, (flag) => {
if (flag) {
emits('finished')
}
})
onMounted(() => {
initCountUp()
if (props.autoplay) {
loopAnim()
}
})
onUnmounted(() => {
cancelAnimationFrame(dalayRafId)
countUp.value?.reset()
})
let dalayRafId: number
// delay to execute callback function
const delay = (cb: () => unknown, seconds = 1) => {
let startTime: number
function count(timestamp: number) {
if (!startTime) startTime = timestamp
const diff = timestamp - startTime
if (diff < seconds * 1000) {
dalayRafId = requestAnimationFrame(count)
} else {
cb()
}
}
dalayRafId = requestAnimationFrame(count)
}
const restart = () => {
initCountUp()
startAnim()
}
defineExpose({
init: initCountUp,
restart
})
</script>
<template>
<div class="countup-wrap">
<slot name="prefix"></slot>
<span ref="elRef"> </span>
<slot name="suffix"></slot>
</div>
</template>

View File

@ -0,0 +1,3 @@
import CountUp from "./count-up.vue"
export default CountUp

View File

@ -0,0 +1,90 @@
<script setup lang="ts">
import { computed, ref ,onBeforeUpdate, nextTick} from "vue";
import merge from "lodash/merge";
import { useElementSize } from "@vueuse/core";
import type { PropType } from "vue";
const props = defineProps({
color: {
type: Array as unknown as PropType<[string, string]>,
default: () => [],
},
backgroundColor: {
type: String,
default: "transparent",
},
});
const defaultColor = ["#6586ec", "#2cf7fe"];
const domRef = ref(null);
const { width, height } = useElementSize(domRef,{width:0,height:0}, { box: 'border-box' });
const mergedColor = computed<[string, string]>(() => {
return merge(defaultColor, props.color);
});
</script>
<template>
<div class="dv-border-box-13 dv-border-box" ref="domRef">
<svg :width="width" :height="height" class="dv-border-svg-container">
<path
:fill="backgroundColor"
:stroke="mergedColor[0]"
:d="`
M 5 20 L 5 10 L 12 3 L 60 3 L 68 10
L ${width - 20} 10 L ${width - 5} 25
L ${width - 5} ${height - 5} L 20 ${height - 5}
L 5 ${height - 20} L 5 20
`"
/>
<path
fill="transparent"
stroke-width="3"
stroke-linecap="round"
stroke-dasharray="10, 5"
:stroke="mergedColor[0]"
:d="`M 16 9 L 61 9`"
/>
<path
fill="transparent"
stroke="{mergedColor[1]}"
:d="`M 5 20 L 5 10 L 12 3 L 60 3 L 68 10`"
/>
<path
fill="transparent"
:stroke="mergedColor[1]"
:d="`M ${width - 5} ${height - 30} L ${width - 5} ${height - 5} L ${
width - 30
} ${height - 5}`"
/>
</svg>
<div class="dv-border-box-content">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="scss">
.dv-border-box {
position: relative;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.dv-border-svg-container {
position: absolute;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
display: block;
}
.dv-border-box-content {
position: relative;
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,3 @@
import BorderBox13 from "./border-box-13.vue"
export default BorderBox13

View File

@ -0,0 +1,186 @@
<script setup lang="ts">
import { onMounted, reactive, ref, watch } from "vue";
import type { DefaultConfigType } from "./index.d";
import cloneDeep from "lodash/cloneDeep";
import merge from "lodash/merge";
const mergedConfig = ref<any>(null);
const capsuleLength = ref<any>([]);
const capsuleValue = ref<any>([]);
const labelData = ref<any>([]);
// const labelDataLength = ref<any>([]);
const defaultConfig = reactive<DefaultConfigType>({
// Colors (hex|rgb|rgba|color keywords) ['#000', 'rgb(0, 0, 0)', 'rgba(0, 0, 0, 1)', 'red']
colors: [
"#37a2da",
"#32c5e9",
"#67e0e3",
"#9fe6b8",
"#ffdb5c",
"#ff9f7f",
"#fb7293",
],
unit: "",
showValue: false, // Show item value
});
const props = withDefaults(
defineProps<{
config: object | any;
data: Array<{
name: string;
value: string | number;
}>;
}>(),
{
config: () => { },
data: () => [],
}
);
const calcData = () => {
mergeConfig();
calcCapsuleLengthAndLabelData();
};
const mergeConfig = () => {
mergedConfig.value = merge(cloneDeep(defaultConfig), props.config || {});
};
const calcCapsuleLengthAndLabelData = () => {
if (!props.data.length) return;
const newcapsuleValue = props.data.map((item: any) => item.value);
const maxValue = Math.max(...newcapsuleValue);
capsuleValue.value = newcapsuleValue;
capsuleLength.value = newcapsuleValue.map((v: any) =>
maxValue ? v / maxValue : 0
);
const oneFifth = maxValue / 5;
const newlabelData = Array.from(
new Set(new Array(6).fill(0).map((v, i) => Math.ceil(i * oneFifth)))
);
labelData.value = newlabelData;
// labelDataLength.value = Array.from(newlabelData).map((v) =>
// maxValue ? v / maxValue : 0
// );
// console.log(labelDataLength.value);
};
watch(
() => props.data,
(newval: any) => {
calcData();
},
);
watch(
() => props.config,
(newval: any) => {
calcData();
},
);
onMounted(() => {
calcData();
});
</script>
<template>
<div class="dv-capsule-chart">
<template v-if="mergedConfig">
<div class="label-column">
<div v-for="item in data" :key="item.name">
{{ item.name }}
</div>
<div>&nbsp;</div>
</div>
<div class="capsule-container">
<div class="capsule-item" v-for="(capsule, index) in capsuleLength" :key="index">
<div class="capsule-item-column" :style="`width: ${capsule * 100}%; background-color: ${mergedConfig.colors[index % mergedConfig.colors.length]
};`">
<div v-if="mergedConfig.showValue" class="capsule-item-value">
{{ capsuleValue[index] }}
</div>
</div>
</div>
<div class="unit-label">
<div v-for="(label, index) in labelData" :key="label + index">
{{ label }}
</div>
</div>
</div>
<div class="unit-text" v-if="mergedConfig.unit">
{{ mergedConfig.unit }}
</div>
</template>
</div>
</template>
<style scoped lang="scss">
.dv-capsule-chart {
position: relative;
display: flex;
flex-direction: row;
box-sizing: border-box;
padding: 10px;
color: #fff;
.label-column {
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
padding-right: 10px;
text-align: right;
font-size: 12px;
div {
height: 20px;
line-height: 20px;
}
}
.capsule-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.capsule-item {
box-shadow: 0 0 3px #999;
height: 10px;
margin: 5px 0px;
border-radius: 5px;
.capsule-item-column {
position: relative;
height: 8px;
margin-top: 1px;
border-radius: 5px;
transition: all 0.3s;
display: flex;
justify-content: flex-end;
align-items: center;
.capsule-item-value {
font-size: 12px;
transform: translateX(100%);
}
}
}
.unit-label {
height: 20px;
font-size: 12px;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
}
.unit-text {
text-align: right;
display: flex;
align-items: flex-end;
font-size: 12px;
line-height: 20px;
margin-left: 10px;
}
}
</style>

View File

@ -0,0 +1,6 @@
export interface DefaultConfigType {
colors: Array<String>;
unit:string,
showValue:Boolean
}

View File

@ -0,0 +1,3 @@
import CapsuleChart from "./capsule-chart.vue"
export * from "./index.d"
export default CapsuleChart

View File

@ -0,0 +1,9 @@
<script setup lang="ts"></script>
<template>
<div>
<slot></slot>
</div>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,2 @@
import EmptyCom from "./empty-com.vue"
export default EmptyCom

View File

@ -0,0 +1,2 @@
import ItemWrap from "./item-wrap.vue"
export default ItemWrap

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import BorderBox13 from "@/components/datav/border-box-13";
const props = withDefaults(
defineProps<{
//
title: number | string;
}>(),
{
title: "",
}
);
</script>
<template>
<BorderBox13>
<div class="item_title" v-if="title !== ''">
<div class="zuo"></div>
<span class="title-inner"> &nbsp;&nbsp;{{ title }}&nbsp;&nbsp; </span>
<div class="you"></div>
</div>
<div
:class="title !== '' ? 'item_title_content' : 'item_title_content_def'"
>
<slot></slot></div
></BorderBox13>
</template>
<style scoped lang="scss">
$item-title-height: 38px;
$item_title_content-height: calc(100% - 38px);
.item_title {
height: $item-title-height;
line-height: $item-title-height;
width: 100%;
color: #31abe3;
text-align: center;
// background: linear-gradient(to right, transparent, #0f0756, transparent);
position: relative;
display: flex;
align-items: center;
justify-content: center;
.zuo,
.you {
width: 58px;
height: 14px;
background-image: url("@/assets/img/titles/zuo.png");
}
.you {
transform: rotate(180deg);
}
.title-inner {
font-weight: 900;
letter-spacing: 2px;
background: linear-gradient(
92deg,
#0072ff 0%,
#00eaff 48.8525390625%,
#01aaff 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
:deep(.dv-border-box-content) {
box-sizing: border-box;
padding: 6px 16px 0px;
}
.item_title_content {
height: $item_title_content-height;
}
.item_title_content_def {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,3 @@
import ScaleScreen from './scale-screen.vue'
export default ScaleScreen

View File

@ -0,0 +1,246 @@
<template>
<section
:style="{ ...styles.box, ...boxStyle }"
class="v-screen-box"
ref="box"
>
<div
:style="{ ...styles.wrapper, ...wrapperStyle }"
class="screen-wrapper"
ref="screenWrapper"
>
<slot></slot>
</div>
</section>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import type { CSSProperties, PropType } from "vue";
/**
* 防抖函数
* @param {Function} fn
* @param {number} delay
* @returns {() => void}
*/
function debounce(fn: Function, delay: number): () => void {
// let timer: NodeJS.Timer;
let timer: any;
return function (...args: any[]): void {
if (timer) clearTimeout(timer);
timer = setTimeout(
() => {
typeof fn === "function" && fn.apply(null, args);
clearTimeout(timer);
},
delay > 0 ? delay : 100
);
};
}
interface IState {
originalWidth: string | number;
originalHeight: string | number;
width?: string | number;
height?: string | number;
observer: null | MutationObserver;
}
type IAutoScale =
| boolean
| {
x?: boolean;
y?: boolean;
};
const props = defineProps({
width: {
type: [String, Number] as PropType<string | number>,
default: 1920,
},
height: {
type: [String, Number] as PropType<string | number>,
default: 1080,
},
fullScreen: {
type: Boolean as PropType<boolean>,
default: false,
},
autoScale: {
type: [Object, Boolean] as PropType<IAutoScale>,
default: true,
},
delay: {
type: Number as PropType<number>,
default: 500,
},
boxStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({}),
},
wrapperStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({}),
},
});
const state = reactive<IState>({
width: 0,
height: 0,
originalWidth: 0,
originalHeight: 0,
observer: null,
});
const styles: Record<string, CSSProperties> = {
box: {
overflow: "hidden",
backgroundSize: `100% 100%`,
background: `#000`,
width: `100vw`,
height: `100vh`,
},
wrapper: {
transitionProperty: `all`,
transitionTimingFunction: `cubic-bezier(0.4, 0, 0.2, 1)`,
transitionDuration: `500ms`,
position: `relative`,
overflow: `hidden`,
zIndex: 100,
transformOrigin: `left top`,
},
};
const screenWrapper = ref<HTMLElement>();
const box = ref<HTMLElement>();
watch(
() => props.autoScale,
async (newVal: any) => {
if (newVal) {
onResize();
addListener();
} else {
clearListener();
clearScreenWrapperStyle();
}
}
);
/**
* 初始化大屏容器宽高
*/
const initSize = () => {
return new Promise<void>((resolve) => {
box.value!.scrollLeft = 0;
box.value!.scrollTop = 0;
nextTick(() => {
// region
if (props.width && props.height) {
state.width = props.width;
state.height = props.height;
} else {
state.width = screenWrapper.value?.clientWidth;
state.height = screenWrapper.value?.clientHeight;
}
// endregion
// region
if (!state.originalHeight || !state.originalWidth) {
state.originalWidth = window.screen.width;
state.originalHeight = window.screen.height;
}
// endregion
resolve();
});
});
};
/**
* 更新大屏容器宽高
*/
const updateSize = () => {
if (state.width && state.height) {
screenWrapper.value!.style.width = `${state.width}px`;
screenWrapper.value!.style.height = `${state.height}px`;
} else {
screenWrapper.value!.style.width = `${state.originalWidth}px`;
screenWrapper.value!.style.height = `${state.originalHeight}px`;
}
};
const clearScreenWrapperStyle = () => {
screenWrapper.value!.style.transform = "";
screenWrapper.value!.style.margin = "";
};
const autoScale = (scale: number) => {
if (!props.autoScale) {
return;
}
const domWidth = screenWrapper.value!.clientWidth;
const domHeight = screenWrapper.value!.clientHeight;
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
screenWrapper.value!.style.transform = `scale(${scale},${scale})`;
let mx = Math.max((currentWidth - domWidth * scale) / 2, 0);
let my = Math.max((currentHeight - domHeight * scale) / 2, 0);
if (typeof props.autoScale === "object") {
!props.autoScale.x && (mx = 0);
!props.autoScale.y && (my = 0);
}
screenWrapper.value!.style.margin = `${my}px ${mx}px`;
};
const updateScale = () => {
//
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
//
const realWidth = state.width || state.originalWidth;
const realHeight = state.height || state.originalHeight;
//
const widthScale = currentWidth / +realWidth;
const heightScale = currentHeight / +realHeight;
//
if (props.fullScreen) {
screenWrapper.value!.style.transform = `scale(${widthScale},${heightScale})`;
return false;
}
//
const scale = Math.min(widthScale, heightScale);
autoScale(scale);
};
const onResize = debounce(async () => {
await initSize();
updateSize();
updateScale();
}, props.delay);
const initMutationObserver = () => {
const observer = (state.observer = new MutationObserver(() => {
onResize();
}));
observer.observe(screenWrapper.value!, {
attributes: true,
attributeFilter: ["style"],
attributeOldValue: true,
});
};
const clearListener = () => {
window.removeEventListener("resize", onResize);
// state.observer?.disconnect();
};
const addListener = () => {
window.addEventListener("resize", onResize);
// initMutationObserver();
};
onMounted(() => {
nextTick(async () => {
await initSize();
updateSize();
updateScale();
addListener();
// initMutationObserver();
});
});
onUnmounted(() => {
clearListener();
// state.observer?.disconnect();
});
</script>

View File

@ -0,0 +1,2 @@
import SeamlessScroll from "./seamless-scroll.vue"
export default SeamlessScroll

View File

@ -0,0 +1,407 @@
<script setup lang="ts">
import {
computed,
defineComponent,
onBeforeMount,
onMounted,
ref,
watch,
nextTick,
} from "vue";
import type { CSSProperties } from "vue";
import throttle from "lodash/throttle";
type propsType = {
modelValue?: boolean;
list: Array<any>;
step?: number;
limitScrollNum?: number;
hover?: boolean;
direction?: string;
singleHeight?: number;
singleWidth?: number;
singleWaitTime?: number;
isRemUnit?: boolean;
isWatch?: boolean;
delay?: number;
ease?: any;
count?: number;
copyNum?: number;
wheel?: boolean;
singleLine?: boolean;
};
const props = withDefaults(defineProps<propsType>(), {
//
modelValue: true,
//
list: () => [],
// step
step: 1,
//
limitScrollNum: 3,
//
hover: false,
//
direction: "up",
//
singleHeight: 0,
//
singleWidth: 0,
// ( 1000ms)
singleWaitTime: 1000,
// rem
isRemUnit: false,
//
isWatch: true,
//
delay: 0,
//
ease: "ease-in",
// -1
count: -1,
//
copyNum: 1,
//
wheel: false,
//
singleLine: false,
});
interface Emits {
(event: "count", _count: number): void;
(event: "stop", _count: number): void;
}
const emit = defineEmits<Emits>();
const scrollRef = ref(null);
const slotListRef = ref<HTMLDivElement | null>(null);
const realBoxRef = ref<HTMLDivElement | null>(null);
const reqFrame = ref<number | null>(null);
const singleWaitTimeout = ref<TimeProp | null>(null);
const realBoxWidth = ref(0);
const realBoxHeight = ref(0);
const xPos = ref(0);
const yPos = ref(0);
const isHover = ref(false);
const _count = ref(0);
const isScroll = computed(() =>
props.list ? props.list.length >= props.limitScrollNum : false
);
const realBoxStyle = computed(() => {
return {
width: realBoxWidth.value ? `${realBoxWidth.value}px` : "auto",
transform: `translate(${xPos.value}px,${yPos.value}px)`,
transition: `all ${
typeof props.ease === "string"
? props.ease
: "cubic-bezier(" +
props.ease.x1 +
"," +
props.ease.y1 +
"," +
props.ease.x2 +
"," +
props.ease.y2 +
")"
} ${props.delay}ms`,
overflow: "hidden",
display: props.singleLine ? "flex" : "block",
};
});
const isHorizontal = computed(
() => props.direction == "left" || props.direction == "right"
);
function dataWarm(list: any) {
if (list && typeof list !== "boolean" && list.length > 100) {
console.warn(
`数据达到了${list.length}条有点多哦~,可能会造成部分老旧浏览器卡顿。`
);
}
}
const floatStyle = computed<CSSProperties>(() => {
return isHorizontal.value
? {
float: "left",
overflow: "hidden",
display: props.singleLine ? "flex" : "block",
flexShrink: props.singleLine ? 0 : 1,
}
: { overflow: "hidden" };
});
const baseFontSize = computed(() => {
return props.isRemUnit
? parseInt(
globalThis.window.getComputedStyle(
globalThis.document.documentElement,
null
).fontSize
)
: 1;
});
const realSingleStopWidth = computed(
() => props.singleWidth * baseFontSize.value
);
const realSingleStopHeight = computed(
() => props.singleHeight * baseFontSize.value
);
const step = computed(() => {
let singleStep: number;
let _step = props.step;
if (isHorizontal.value) {
singleStep = realSingleStopWidth.value;
} else {
singleStep = realSingleStopHeight.value;
}
if (singleStep > 0 && singleStep % _step > 0) {
console.error(
"如果设置了单步滚动step 需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确。~~~~~"
);
}
return _step;
});
const cancle = () => {
cancelAnimationFrame(reqFrame.value as number);
reqFrame.value = null;
};
const animation = (
_direction: "up" | "down" | "left" | "right",
_step: number,
isWheel?: boolean
) => {
// console.log("animation",_direction,_step,isWheel);
reqFrame.value = requestAnimationFrame(function () {
const h = realBoxHeight.value / 2;
const w = realBoxWidth.value / 2;
if (_direction === "up") {
if (Math.abs(yPos.value) >= h) {
yPos.value = 0;
_count.value += 1;
emit("count", _count.value);
}
yPos.value -= _step;
} else if (_direction === "down") {
if (yPos.value >= 0) {
yPos.value = h * -1;
_count.value += 1;
emit("count", _count.value);
}
yPos.value += _step;
} else if (_direction === "left") {
if (Math.abs(xPos.value) >= w) {
xPos.value = 0;
_count.value += 1;
emit("count", _count.value);
}
xPos.value -= _step;
} else if (_direction === "right") {
if (xPos.value >= 0) {
xPos.value = w * -1;
_count.value += 1;
emit("count", _count.value);
}
xPos.value += _step;
}
if (isWheel) {
return;
}
let { singleWaitTime } = props;
if (singleWaitTimeout.value) {
clearTimeout(singleWaitTimeout.value);
}
if (!!realSingleStopHeight.value) {
if (Math.abs(yPos.value) % realSingleStopHeight.value < _step) {
singleWaitTimeout.value = setTimeout(() => {
move();
}, singleWaitTime);
} else {
move();
}
} else if (!!realSingleStopWidth.value) {
if (Math.abs(xPos.value) % realSingleStopWidth.value < _step) {
singleWaitTimeout.value = setTimeout(() => {
move();
}, singleWaitTime);
} else {
move();
}
} else {
move();
}
});
};
const move = () => {
cancle();
if (isHover.value || !isScroll.value || _count.value === props.count) {
emit("stop", _count.value);
_count.value = 0;
return;
}
animation(
props.direction as "up" | "down" | "left" | "right",
step.value,
false
);
};
const initMove = () => {
dataWarm(props.list);
if (isHorizontal.value) {
let slotListWidth = (slotListRef.value as HTMLDivElement).offsetWidth;
slotListWidth = slotListWidth * 2 + 1;
realBoxWidth.value = slotListWidth;
}
if (isScroll.value) {
realBoxHeight.value = (realBoxRef.value as HTMLDivElement).offsetHeight;
if (props.modelValue) {
move();
}
} else {
cancle();
yPos.value = xPos.value = 0;
}
// console.log("initMove","isHorizontal",isHorizontal.value,"isScroll",isScroll.value,realBoxRef.value?.offsetHeight);
};
const startMove = () => {
isHover.value = false;
move();
};
const stopMove = () => {
isHover.value = true;
if (singleWaitTimeout.value) {
clearTimeout(singleWaitTimeout.value);
}
cancle();
};
const hoverStop = computed(
() => props.hover && props.modelValue && isScroll.value
);
const throttleFunc = throttle((e: WheelEvent) => {
cancle();
const singleHeight = !!realSingleStopHeight.value
? realSingleStopHeight.value
: 15;
if (e.deltaY < 0) {
animation("down", singleHeight, true);
}
if (e.deltaY > 0) {
animation("up", singleHeight, true);
}
}, 30);
const onWheel = (e: WheelEvent) => {
throttleFunc(e);
};
const reset = () => {
cancle();
isHover.value = false;
initMove();
};
const Reset = () => {
reset();
};
defineExpose({
Reset,
});
watch(
() => props.list,
() => {
if (props.isWatch) {
nextTick(() => {
reset();
});
}
},
{
deep: true,
}
);
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
startMove();
} else {
stopMove();
}
}
);
watch(
() => props.count,
(newValue) => {
if (newValue !== 0) {
startMove();
}
}
);
onBeforeMount(() => {
cancle();
clearTimeout(singleWaitTimeout.value as unknown as number);
});
onMounted(() => {
if (isScroll.value) {
initMove();
}
});
</script>
<template>
<div
v-if="props.wheel && props.hover"
ref="realBoxRef"
:style="realBoxStyle"
@mouseenter="
() => {
hoverStop && stopMove();
}
"
@mouseleave="
() => {
hoverStop && startMove();
}
"
@wheel="
(e) => {
hoverStop && onWheel(e);
}
"
>
<div ref="slotListRef" :style="floatStyle">
<slot></slot>
</div>
<div :style="floatStyle">
<slot></slot>
</div>
</div>
<div
v-else
:style="realBoxStyle"
ref="realBoxRef"
@mouseenter="
() => {
hoverStop && stopMove();
}
"
@mouseleave="
() => {
hoverStop && startMove();
}
"
>
<div ref="slotListRef" :style="floatStyle">
<slot></slot>
</div>
<div :style="floatStyle">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="scss"></style>

32
src/config/UtilVar.ts Normal file
View File

@ -0,0 +1,32 @@
interface UtilVarType {
baseUrl:string,
code:string|number,
noContentCode:number,
ENC:boolean,//是否进行加密
}
const UtilVar:UtilVarType = {
baseUrl:"",
code:401, //登陆过期
noContentCode:204, //请求成功但没有内容
ENC:false,
}
const runtimeType:any = {
production: () => {
},
//开发环境
development: () => {
// UtilVar.baseUrl= `http://www.xihuanmantou.cn:19527`
},
hash:()=>{
}
}
// console.log(import.meta.env)
runtimeType[import.meta.env.MODE]&&runtimeType[import.meta.env.MODE]()
export default UtilVar

2
src/enums/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./storage-enum"
export * from "./request-enums"

View File

@ -0,0 +1,12 @@
export enum RequestEnum {
// token key
GB_TOKEN_KEY = 'auth-token',
// 验签key
GB_SIGN_KEY = "sign",
// 时间戳 key
GB_TIMESTAMP_KEY = "timestamp"
}
export enum ReqCodeEnum {
Unauthorized = 401,// token过期
Success = 200,//成功
}

View File

@ -0,0 +1,9 @@
export enum StorageEnum {
// token
GB_TOKEN_STORE = 'GB_TOKEN_STORE',
// 语言
YH_LANG_STORE = 'YH_LANG',
//皮肤
YH_THEME_STORE = 'YH_THEME',
}

20
src/main.ts Normal file
View File

@ -0,0 +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')

10
src/mock/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export interface MockParams {
url: string;
type: string;
data?: any;
params?: any;
response(option?: any): Record<string, unknown>;
}
export function mockXHR(): any;

Some files were not shown because too many files have changed in this diff Show More