commit
93e810a17e
45 changed files with 1867 additions and 0 deletions
-
6.env.dev
-
6.env.pre
-
6.env.prod
-
6.env.test
-
27.gitignore
-
0.npmrc
-
53.postcssrc.js
-
10.prettierrc
-
23README.md
-
BINcommitlint.config.js
-
19eslint.config.js
-
41increment-version.js
-
27index.html
-
86package.json
-
BINpublic/favicon.ico
-
21src/app.vue
-
1src/assets/images/home.svg
-
91src/assets/styles/common.scss
-
19src/assets/styles/element.scss
-
12src/assets/styles/main.scss
-
126src/components/common/FTButton/index.vue
-
79src/components/common/FTDialog/index.vue
-
169src/components/common/FTStream/index.vue
-
7src/components/common/FTTable/expand.ts
-
145src/components/common/FTTable/index.vue
-
28src/components/home/Stop/index.vue
-
20src/env.d.ts
-
76src/layouts/default.vue
-
5src/libs/constant.ts
-
92src/libs/http.ts
-
37src/libs/message.ts
-
242src/libs/socket.ts
-
13src/libs/token.ts
-
74src/libs/utils.ts
-
27src/main.ts
-
26src/router/index.ts
-
46src/router/routes.ts
-
7src/stores/index.ts
-
22src/stores/useSystemStore.ts
-
11src/views/craft/index.vue
-
3src/views/debug/index.vue
-
12src/views/home/index.vue
-
12src/views/login/index.vue
-
38tsconfig.json
-
96vite.config.ts
@ -0,0 +1,6 @@ |
|||
# 开发环境 |
|||
|
|||
FT_NODE_ENV=dev |
|||
|
|||
FT_WS_URL=ws://192.168.1.199:8080/ws |
|||
FT_PROXY=http://192.168.1.199:8080 |
@ -0,0 +1,6 @@ |
|||
# 预发环境 |
|||
|
|||
FT_NODE_ENV=pre |
|||
|
|||
FT_WS_URL=ws://192.168.1.140:8080/ws |
|||
FT_PROXY=http://192.168.1.140 |
@ -0,0 +1,6 @@ |
|||
# 生产环境 |
|||
|
|||
FT_NODE_ENV=prod |
|||
|
|||
FT_WS_URL=ws://192.168.100.168:8080/ws |
|||
FT_PROXY=http://192.168.1.168 |
@ -0,0 +1,6 @@ |
|||
# 测试环境 |
|||
|
|||
FT_NODE_ENV=test |
|||
|
|||
FT_WS_URL=ws://192.168.1.200:8080/ws |
|||
FT_PROXY=http://192.168.1.200:8080 |
@ -0,0 +1,27 @@ |
|||
# Logs |
|||
logs |
|||
*.log |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
pnpm-debug.log* |
|||
lerna-debug.log* |
|||
auto-imports.d.ts |
|||
.eslintrc-auto-import.json |
|||
pnpm-lock.yaml |
|||
|
|||
node_modules |
|||
dist |
|||
dist-* |
|||
*.local |
|||
|
|||
# Editor directories and files |
|||
.vscode/* |
|||
!.vscode/extensions.json |
|||
.idea |
|||
.DS_Store |
|||
*.suo |
|||
*.ntvs* |
|||
*.njsproj |
|||
*.sln |
|||
*.sw? |
@ -0,0 +1,53 @@ |
|||
export default { |
|||
|
|||
plugins: { |
|||
|
|||
'postcss-import': {}, |
|||
|
|||
'postcss-url': {}, |
|||
|
|||
'postcss-aspect-ratio-mini': {}, |
|||
|
|||
'postcss-write-svg': { |
|||
|
|||
utf8: false, |
|||
|
|||
}, |
|||
|
|||
'postcss-px-to-viewport': { |
|||
|
|||
viewportWidth: 1120, |
|||
|
|||
viewportHeight: 736, |
|||
|
|||
unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
|
|||
|
|||
viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
|
|||
|
|||
selectorBlackList: ['.ignore', '.hairlines', ':after'], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
|
|||
|
|||
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
|
|||
|
|||
mediaQuery: false, // 允许在媒体查询中转换`px`
|
|||
|
|||
}, |
|||
|
|||
'postcss-viewport-units': { |
|||
filterRule: rule => |
|||
!rule.selector.includes('::after') |
|||
&& !rule.selector.includes('::before') |
|||
&& !rule.selector.includes(':after') |
|||
&& !rule.selector.includes(':before'), |
|||
}, |
|||
|
|||
'cssnano': { |
|||
|
|||
'autoprefixer': false, |
|||
|
|||
'postcss-zindex': false, |
|||
|
|||
}, |
|||
|
|||
}, |
|||
|
|||
} |
@ -0,0 +1,10 @@ |
|||
{ |
|||
"printWidth": 120, |
|||
"tabWidth": 2, |
|||
"endOfLine": "lf", |
|||
"singleQuote": true, |
|||
"semi": true, |
|||
"trailingComma": "none", |
|||
"bracketSpacing": true, |
|||
"arrowParens": "avoid" |
|||
} |
@ -0,0 +1,23 @@ |
|||
### 前端项目模板 |
|||
##### 文件目录 |
|||
- src |
|||
- components 组件目录 |
|||
- apis 接口文件目录 |
|||
- assets 静态资源目录 |
|||
- hooks hooks目录 |
|||
- libs 公共工具类 |
|||
- router 页面路由 |
|||
- stores 项目公共状态管理 |
|||
- types 类型申明 |
|||
- views 页面目录 |
|||
##### 项目运行 |
|||
项目采用pnpm 进行依赖构建,请先安装pnpm |
|||
[pnpm - 速度快、节省磁盘空间的软件包管理器](https://pnpm.io/) |
|||
```bash |
|||
npm install pnpm -g |
|||
``` |
|||
|
|||
```bash |
|||
pnpm install // 安装依赖 |
|||
pnpm dev // 运行项目 |
|||
``` |
@ -0,0 +1,19 @@ |
|||
// import eslint_js from '@eslint/js'
|
|||
// import eslint_ts from 'typescript-eslint';
|
|||
// import eslint_vue from 'eslint-plugin-vue';
|
|||
// import vue_parser from 'vue-eslint-parser';
|
|||
|
|||
import lintConfig from '@antfu/eslint-config' |
|||
|
|||
export default lintConfig({ |
|||
vue: true, |
|||
markdown: true, |
|||
ignores: [], |
|||
rules: { |
|||
'no-console': 0, |
|||
'antfu/top-level-function': 0, |
|||
'ts/no-use-before-define': 0, |
|||
'no-alert': 0, |
|||
}, |
|||
globals: { process: 'readonly' }, |
|||
}) |
@ -0,0 +1,41 @@ |
|||
import fs from 'node:fs'; |
|||
import path, { dirname } from 'node:path'; |
|||
import { fileURLToPath } from 'node:url'; |
|||
import semver from 'semver'; |
|||
import { execSync } from 'child_process'; // 引入 child_process 模块用于执行 Git 命令
|
|||
|
|||
const __filename = fileURLToPath(import.meta.url); |
|||
const __dirname = dirname(__filename); |
|||
|
|||
const packagePath = path.resolve(__dirname, 'package.json'); |
|||
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8')); |
|||
|
|||
// 读取命令行参数(默认使用 'patch')
|
|||
const versionType = process.argv[2] || 'patch'; |
|||
|
|||
// 递增版本
|
|||
const newVersion = semver.inc(packageJson.version, versionType); |
|||
if (!newVersion) { |
|||
throw new Error(`Invalid version type: ${versionType}`); |
|||
} |
|||
|
|||
packageJson.version = newVersion; |
|||
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2)); |
|||
console.log(`Version updated to: ${newVersion}`); |
|||
|
|||
// 新增:自动提交 package.json 到远程仓库
|
|||
try { |
|||
// 将 package.json 添加到暂存区
|
|||
execSync('git add package.json'); |
|||
console.log('Added package.json to staging area.'); |
|||
|
|||
// 提交更改
|
|||
execSync(`git commit -m "fix: Update version to V${newVersion}"`); |
|||
console.log(`Committed changes with message: Update version to ${newVersion}`); |
|||
|
|||
// 推送到远程仓库
|
|||
execSync('git push'); |
|||
console.log('Pushed changes to remote repository.'); |
|||
} catch (error) { |
|||
console.error('Failed to commit and push changes:', error.message); |
|||
} |
@ -0,0 +1,27 @@ |
|||
<!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>石墨消解仪</title> |
|||
<style> |
|||
html,body{ |
|||
height:100%; |
|||
width:100%; |
|||
margin: 0; |
|||
padding: 0; |
|||
} |
|||
#app { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div id="app"></div> |
|||
<script type="module" src="/src/main.ts"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,86 @@ |
|||
{ |
|||
"name": "matrix-spray-web", |
|||
"type": "module", |
|||
"version": "1.0.2", |
|||
"description": "", |
|||
"author": "", |
|||
"license": "ISC", |
|||
"keywords": [], |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"dev": "vite --mode dev", |
|||
"dev:test": "vite --mode test", |
|||
"dev:prod": "vite --mode prod", |
|||
"build": "vite build --mode dev", |
|||
"build:test": "vite build --mode test", |
|||
"build:prod:patch": "node increment-version.js patch && vite build --mode prod", |
|||
"build:prod:minor": "node increment-version.js minor && vite build --mode prod", |
|||
"build:prod:major": "node increment-version.js major && vite build --mode prod", |
|||
"build:pre": "vite build --mode pre", |
|||
"build:dev": "vite build --mode dev", |
|||
"prepare": "husky", |
|||
"lint:lint-staged": "lint-staged", |
|||
"lint": "vue-tsc --noEmit --skipLibCheck && eslint", |
|||
"eslint": "eslint --fix --ext .ts,.vue src", |
|||
"prettier": "prettier --write ." |
|||
}, |
|||
"dependencies": { |
|||
"@element-plus/icons-vue": "^2.3.1", |
|||
"autoprefixer": "^10.4.20", |
|||
"axios": "^1.8.1", |
|||
"cssnano": "^7.0.6", |
|||
"element-plus": "^2.9.5", |
|||
"konva": "^9.3.18", |
|||
"lodash": "^4.17.21", |
|||
"pinia": "^3.0.1", |
|||
"pinia-plugin-persistedstate": "^4.2.0", |
|||
"postcss": "^8.5.3", |
|||
"postcss-aspect-ratio-mini": "^1.1.0", |
|||
"postcss-import": "^16.1.0", |
|||
"postcss-px-to-viewport": "^1.1.1", |
|||
"postcss-url": "^10.1.3", |
|||
"postcss-viewport-units": "^0.1.6", |
|||
"postcss-write-svg": "^3.0.1", |
|||
"tailwindcss": "^4.0.12", |
|||
"vue": "^3.5.13", |
|||
"vue-router": "^4.5.0" |
|||
}, |
|||
"devDependencies": { |
|||
"@antfu/eslint-config": "^4.3.0", |
|||
"@commitlint/cli": "^19.7.1", |
|||
"@commitlint/config-conventional": "^19.7.1", |
|||
"@types/node": "^22.13.5", |
|||
"@typescript-eslint/eslint-plugin": "^8.25.0", |
|||
"@typescript-eslint/parser": "^8.25.0", |
|||
"@vitejs/plugin-vue": "^5.2.1", |
|||
"@vue/eslint-config-prettier": "^10.2.0", |
|||
"eslint": "^9.21.0", |
|||
"eslint-config-prettier": "^10.0.2", |
|||
"eslint-plugin-prettier": "^5.2.3", |
|||
"eslint-plugin-vue": "^9.32.0", |
|||
"husky": "^9.1.7", |
|||
"lint-staged": "^15.4.3", |
|||
"prettier": "^3.5.2", |
|||
"sass": "^1.85.1", |
|||
"semver": "^7.7.1", |
|||
"typescript": "^5.7.3", |
|||
"unplugin-auto-import": "^19.1.1", |
|||
"unplugin-vue-components": "^28.4.1", |
|||
"vite": "^6.2.0", |
|||
"vite-plugin-compression": "^0.5.1", |
|||
"vite-plugin-eslint": "^1.8.1", |
|||
"vue-tsc": "^2.2.4" |
|||
}, |
|||
"husky": { |
|||
"hooks": { |
|||
"pre-commit": "lint-staged" |
|||
} |
|||
}, |
|||
"lint-staged": { |
|||
"*.{js,jsx,vue,ts,tsx}": [ |
|||
"eslint --fix", |
|||
"prettier --write", |
|||
"lint-staged" |
|||
] |
|||
} |
|||
} |
@ -0,0 +1,21 @@ |
|||
<script setup lang="ts"> |
|||
// socket.init(() => {}) |
|||
</script> |
|||
|
|||
<template> |
|||
<router-view v-slot="{ Component }" class="main-content"> |
|||
<transition> |
|||
<component :is="Component" /> |
|||
</transition> |
|||
</router-view> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
.main-content { |
|||
width: 100%; |
|||
height: 100%; |
|||
//background: url("assets/images/background.jpg") no-repeat center; |
|||
background-size: cover; |
|||
overflow: hidden; |
|||
} |
|||
</style> |
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="80" height="80" viewBox="0 0 80 80"><g><g><ellipse cx="40" cy="40" rx="40" ry="40" fill="#479CF1" fill-opacity="1"/></g><g><path d="M20.0370799,43.6256C20.866486,47.6246,22.91091,51.222300000000004,25.94731,54.0277C26.3526,54.4028,26.9106,54.5752,27.46181,54.4956C28.0134,54.4165,28.49784,54.0943,28.77694,53.6208L31.1482,49.591300000000004C31.5442,48.9178,31.451999999999998,48.070499999999996,30.92,47.4942C29.68039,46.1482,28.77662,44.537800000000004,28.28009,42.7903C28.06083,42.015,27.34183,41.4784,26.52231,41.4784L21.82395,41.4784C21.27636,41.4781,20.757754,41.7198,20.411853,42.1363C20.0647341,42.552,19.9270634,43.0991,20.0370799,43.6256ZM44.1657,51.909C43.7696,51.233000000000004,42.9726,50.8869,42.1963,51.053799999999995C40.231899999999996,51.4761,38.1899,51.3965,36.2658,50.8227C35.453900000000004,50.580600000000004,34.579,50.9191,34.1541,51.6398L31.8262,55.5982C31.5578,56.055,31.5084,56.6046,31.691000000000003,57.1004C31.872999999999998,57.5967,32.2697,57.9884,32.7742,58.1703C34.9795,58.9695,37.3089,59.375,39.695,59.375C41.6678,59.375,43.6156,59.0954,45.482600000000005,58.543C46.0156,58.3847,46.446,57.9968,46.652,57.4891C46.8572,56.9806,46.814099999999996,56.408,46.535,55.9346L44.1657,51.909ZM52.5476,24.57127C52.2201,24.3027,51.806799999999996,24.15552,51.379999999999995,24.15542C51.2742,24.15542,51.1663,24.166330000000002,51.060500000000005,24.18424C50.531,24.2771,50.0703,24.59457,49.8016,25.05176L47.4685,29.01324C47.045100000000005,29.73472,47.183499999999995,30.6473,47.802800000000005,31.2165C49.39,32.6707,50.5347,34.5286,51.1096,36.5833C51.3275,37.3604,52.0476,37.8989,52.8691,37.899100000000004L57.568,37.899100000000004C58.1145,37.8988,58.6321,37.658,58.9784,37.2432C59.3258,36.8281,59.464,36.2815,59.3546,35.7553C58.4579,31.3953,56.0407,27.42316,52.5476,24.57127ZM46.652,21.88784C46.446600000000004,21.37975,46.0159,20.99153,45.482600000000005,20.833661C43.6053,20.279568,41.6553,19.99869145,39.695,20.00000417001C37.3089,20.00000417001,34.9795,20.405219,32.7742,21.20446C31.714199999999998,21.58978,31.262,22.8179,31.8262,23.77903L34.1541,27.73687C34.579,28.457549999999998,35.453900000000004,28.79609,36.2658,28.55402C37.3775,28.22171,38.5332,28.05317,39.6953,28.05394C40.530100000000004,28.05394,41.372299999999996,28.14349,42.1963,28.32119C42.9721,28.48767,43.7685,28.14262,44.1657,27.46794L46.5353,23.44182C46.8143,22.96858,46.857299999999995,22.39608,46.652,21.88784ZM57.568,41.4781L52.8691,41.4781C52.0475,41.477599999999995,51.327,42.0163,51.1096,42.7936C50.5345,44.848299999999995,49.3898,46.7063,47.802800000000005,48.160799999999995C47.1834,48.729,47.0449,49.6411,47.4685,50.3618L49.8016,54.3249C50.07,54.7824,50.5309,55.1001,51.060500000000005,55.1927C51.1663,55.2106,51.2742,55.2196,51.379999999999995,55.2196C51.8066,55.2202,52.2199,55.0737,52.5476,54.8057C56.0407,51.9518,58.4579,47.9814,59.3546,43.622C59.4641,43.0957,59.3259,42.549099999999996,58.9784,42.134C58.6326,41.718599999999995,58.1147,41.4778,57.568,41.4781ZM20.411568,37.241C20.757316,37.6573,21.27588,37.8989,21.82367,37.8989L26.52259,37.8989C27.34233,37.8986,28.06128,37.361999999999995,28.280369999999998,36.587C28.776699999999998,34.8389,29.68048,33.2279,30.920299999999997,31.8814C31.4524,31.3057,31.544600000000003,30.4588,31.1485,29.785890000000002L28.77722,25.75641C28.49772,25.28303,28.01361,24.96039,27.46209,24.87994C27.37433,24.86765,27.28578,24.86166,27.19713,24.86203C26.73679,24.86203,26.28729,25.03413,25.9476,25.349510000000002C22.91091,28.15468,20.866486,31.7521,20.0370799,35.7513C19.9271883,36.2779,20.0647273,36.825,20.411568,37.241Z" fill="#FFFFFF" fill-opacity="1"/></g></g></svg> |
@ -0,0 +1,91 @@ |
|||
/* CSS Document */ |
|||
html { |
|||
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; |
|||
height: 100vh; |
|||
font-size: 14px; |
|||
} |
|||
html, body { |
|||
overflow: hidden; |
|||
} |
|||
html, |
|||
body, |
|||
ol, |
|||
dl, |
|||
dd, |
|||
dt, |
|||
p, |
|||
h1, |
|||
h2, |
|||
h3, |
|||
h4, |
|||
h5, |
|||
h6, |
|||
form, |
|||
fieldset, |
|||
legend, |
|||
img { |
|||
margin: 0; |
|||
padding: 0; |
|||
} |
|||
fieldset { |
|||
border: none; |
|||
} |
|||
img { |
|||
display: block; |
|||
} |
|||
address, |
|||
caption, |
|||
cite, |
|||
code, |
|||
dfn, |
|||
th, |
|||
var { |
|||
font-style: normal; |
|||
font-weight: normal; |
|||
} |
|||
ul, |
|||
ol, |
|||
li { |
|||
list-style: none; |
|||
} |
|||
a { |
|||
color: #666; |
|||
text-decoration: none; |
|||
} |
|||
* { |
|||
box-sizing: border-box !important; |
|||
} |
|||
|
|||
a { |
|||
&:visited { |
|||
color: inherit; |
|||
} |
|||
} |
|||
|
|||
input, |
|||
button, |
|||
select, |
|||
textarea { |
|||
outline: none; |
|||
} |
|||
|
|||
textarea { |
|||
resize: none; |
|||
} |
|||
|
|||
input[type='number'] { |
|||
&::-webkit-outer-spin-button, |
|||
&::-webkit-inner-spin-button { |
|||
-webkit-appearance: none; |
|||
} |
|||
-moz-appearance: textfield; |
|||
} |
|||
.clear { |
|||
clear: both; |
|||
} |
|||
|
|||
.konvajs-content{ |
|||
canvas{ |
|||
border: 1px solid #ccc !important; |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
:root { |
|||
--el-font-size-base: 50px; |
|||
--el-button-size: 80px; |
|||
|
|||
|
|||
|
|||
--el-color-primary: #26509C; |
|||
//--el-button-active-bg-color: linear-gradient(90deg, #0657C0 24%, #096AE0 101%); |
|||
//--text-color-primary: #17213c; |
|||
//--el-color-success: rgba(88, 162, 95, 1); |
|||
//--text-color-info: #838b99; |
|||
//--el-input-border: #dae0f2; |
|||
//--el-font-weight-primary: 400; |
|||
//--color-red: #f56c6c; |
|||
//--color-green: #67c23a; |
|||
//--color-yellow: #e6a23c; |
|||
//--color-blue: --el-color-primary; |
|||
--el-font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; |
|||
} |
@ -0,0 +1,12 @@ |
|||
$primary-color: #0a57ea; |
|||
$success-color: #67c23a; |
|||
$danger-color: #f56c6c; |
|||
$warn-color: #e6a23c; |
|||
$info-color: #909399; |
|||
|
|||
@use './common.scss'; |
|||
@use './element.scss'; |
|||
|
|||
.mt-20 { |
|||
margin-top: 20px; |
|||
} |
@ -0,0 +1,126 @@ |
|||
<script setup lang="ts"> |
|||
import { ref } from 'vue' |
|||
|
|||
const props = defineProps({ |
|||
type: { |
|||
type: String, |
|||
default: 'default', |
|||
}, |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
loading: { // 添加 loading 属性 |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
clickHandle: { |
|||
type: Function, |
|||
required: false, |
|||
}, |
|||
}) |
|||
|
|||
const isLoading = ref(false) |
|||
|
|||
async function handleClick() { |
|||
if (!props.clickHandle || isLoading.value) |
|||
return |
|||
isLoading.value = true // 进入 loading |
|||
try { |
|||
await props.clickHandle() // 执行异步操作 |
|||
} |
|||
finally { |
|||
isLoading.value = false // 结束 loading |
|||
} |
|||
} |
|||
|
|||
const setLoading = (loading: boolean) => { |
|||
isLoading.value = loading |
|||
} |
|||
|
|||
defineExpose({ |
|||
setLoading, |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="ft-button" :class="{ 'ft-button-disabled': disabled || isLoading }" @click="handleClick"> |
|||
<!-- 添加 loading 判断 --> |
|||
<div v-show="disabled || isLoading" class="my-button-shadow" /> <!-- 添加 loading 判断 --> |
|||
<div |
|||
class="my-button" :class="{ |
|||
[`my-button-${type}`]: true, |
|||
'button-disabled': disabled || isLoading, // 添加 loading 判断 |
|||
}" |
|||
> |
|||
<el-icon v-if="isLoading" :color="type === 'default' ? '#26509C' : '#fff'"> |
|||
<!-- 添加 loading 判断 --> |
|||
<Loading class="rotate-loading" /> <!-- 添加旋转类 --> |
|||
</el-icon> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
.ft-button { |
|||
position: relative; |
|||
display: inline-block; |
|||
margin-right: 20px; |
|||
} |
|||
.ft-button-disabled { |
|||
pointer-events: none; |
|||
} |
|||
.my-button-shadow { |
|||
position: absolute; |
|||
width: 100%; |
|||
height: 100%; |
|||
z-index: 100; |
|||
} |
|||
.my-button { |
|||
height: var(--el-button-size); |
|||
padding: 0 50px; |
|||
border-radius: 10px; |
|||
display: flex; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
font-size: 40px; |
|||
width: fit-content; |
|||
position: relative; |
|||
.el-icon { |
|||
position: absolute; |
|||
left: 5px; |
|||
svg { |
|||
width: 35px; |
|||
} |
|||
} |
|||
} |
|||
.button-disabled { |
|||
opacity: 0.5; |
|||
} |
|||
.my-button-default { |
|||
background: linear-gradient(180deg, #D8E3F8 0%, #FBFCFE 100%); |
|||
color: #26509C; |
|||
border: 1px solid #D8E3F8; |
|||
} |
|||
.my-button-primary { |
|||
background: linear-gradient(90deg, #0657C0 24%, #096AE0 101%); |
|||
color: #fff; |
|||
border: 1px solid #D8E3F8; |
|||
} |
|||
|
|||
.my-button-info { |
|||
background: #335AA5; |
|||
color: #fff; |
|||
border: 1px solid #335AA5; |
|||
} |
|||
|
|||
.rotate-loading { |
|||
animation: spin 1s linear infinite; |
|||
} |
|||
|
|||
@keyframes spin { |
|||
0% { transform: rotate(0deg); } |
|||
100% { transform: rotate(360deg); } |
|||
} |
|||
</style> |
@ -0,0 +1,79 @@ |
|||
<script setup lang="ts"> |
|||
import { ref, watch } from 'vue' |
|||
|
|||
const props = defineProps({ |
|||
title: { |
|||
type: String, |
|||
default: '', |
|||
}, |
|||
visible: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
width: { |
|||
type: String, |
|||
default: '50%', |
|||
}, |
|||
okLoading: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
}) |
|||
const emits = defineEmits(['update:visible', 'ok', 'cancel']) |
|||
|
|||
const cancel = () => { |
|||
show.value = false |
|||
emits('cancel') |
|||
} |
|||
const ok = () => { |
|||
emits('ok') |
|||
} |
|||
|
|||
const show = ref(false) |
|||
|
|||
watch( |
|||
() => props.visible, |
|||
(newVal) => { |
|||
show.value = newVal |
|||
}, |
|||
{ |
|||
// 对于对象需要深度监听 |
|||
deep: true, |
|||
immediate: true, |
|||
}, |
|||
) |
|||
</script> |
|||
|
|||
<template> |
|||
<el-dialog |
|||
v-model="show" |
|||
center |
|||
:close-on-click-modal="false" |
|||
:close-on-press-escape="false" |
|||
:show-close="false" |
|||
destroy-on-close |
|||
append-to-body |
|||
:title="title" |
|||
:width="width" |
|||
:before-close="cancel" |
|||
> |
|||
<slot /> |
|||
<template #footer> |
|||
<div v-if="$slots.footer" class="dialog-footer"> |
|||
<slot name="footer" /> |
|||
</div> |
|||
<div v-else class="dialog-footer"> |
|||
<ft-button @click="cancel"> |
|||
取消 |
|||
</ft-button> |
|||
<ft-button type="primary" :loading="okLoading" @click="ok"> |
|||
确认 |
|||
</ft-button> |
|||
</div> |
|||
</template> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
|
|||
</style> |
@ -0,0 +1,169 @@ |
|||
<script setup lang="ts"> |
|||
import { cmdNameMap } from 'libs/utils' |
|||
import { useSystemStore } from 'stores/useSystemStore' |
|||
import { computed, nextTick, ref, watch } from 'vue' |
|||
|
|||
defineProps({ |
|||
visible: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
}) |
|||
|
|||
const systemStore = useSystemStore() |
|||
const title = computed(() => { |
|||
return cmdNameMap[systemStore.systemList[0]?.cmdCode as keyof typeof cmdNameMap] || systemStore.systemList[0]?.cmdCode |
|||
}) |
|||
|
|||
const maskBodyRef = ref<HTMLElement | null>(null) |
|||
const maskRef = ref<HTMLElement | null>(null) |
|||
const maskHeaderRef = ref<HTMLElement | null>(null) |
|||
|
|||
const statusMap = { |
|||
fail: 'danger', |
|||
error: 'danger', |
|||
success: 'success', |
|||
finish: 'primary', |
|||
SEND: 'primary', |
|||
receive: 'primary', |
|||
start: 'primary', |
|||
result: 'primary', |
|||
} |
|||
|
|||
watch( |
|||
() => systemStore.systemList, |
|||
async () => { |
|||
await nextTick() |
|||
if (maskBodyRef.value) { |
|||
maskBodyRef.value.scrollTop = maskBodyRef.value.scrollHeight |
|||
} |
|||
}, |
|||
{ deep: true }, |
|||
) |
|||
|
|||
// 拖拽相关逻辑 |
|||
let isDragging = false |
|||
let offsetX = 0 |
|||
let offsetY = 0 |
|||
|
|||
const handleMouseDown = (event: MouseEvent | TouchEvent) => { |
|||
if (maskRef.value && maskHeaderRef.value) { |
|||
isDragging = true |
|||
const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX |
|||
const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY |
|||
offsetX = clientX - maskRef.value.offsetLeft |
|||
offsetY = clientY - maskRef.value.offsetTop |
|||
document.addEventListener('mousemove', handleMouseMove) |
|||
document.addEventListener('mouseup', handleMouseUp) |
|||
document.addEventListener('touchmove', handleMouseMove) |
|||
document.addEventListener('touchend', handleMouseUp) |
|||
} |
|||
} |
|||
|
|||
const handleMouseMove = (event: MouseEvent | TouchEvent) => { |
|||
if (maskRef.value && isDragging) { |
|||
const clientX = 'clientX' in event ? event.clientX : event.touches[0].clientX |
|||
const clientY = 'clientY' in event ? event.clientY : event.touches[0].clientY |
|||
|
|||
// 获取 body 的宽度和高度 |
|||
const bodyWidth = document.body.clientWidth |
|||
const bodyHeight = document.body.clientHeight |
|||
|
|||
// 计算新的位置,并确保不会超出 body 的范围 |
|||
const newLeft = Math.max(0, Math.min(clientX - offsetX, bodyWidth - maskRef.value.offsetWidth)) |
|||
const newTop = Math.max(0, Math.min(clientY - offsetY, bodyHeight - maskRef.value.offsetHeight)) |
|||
|
|||
maskRef.value.style.left = `${newLeft}px` |
|||
maskRef.value.style.top = `${newTop}px` |
|||
} |
|||
} |
|||
|
|||
const handleMouseUp = () => { |
|||
isDragging = false |
|||
document.removeEventListener('mousemove', handleMouseMove) |
|||
document.removeEventListener('mouseup', handleMouseUp) |
|||
document.removeEventListener('touchmove', handleMouseMove) |
|||
document.removeEventListener('touchend', handleMouseUp) |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<teleport to="body"> |
|||
<!-- 使用 transition 组件包裹 mask 元素 --> |
|||
<transition name="mask-fade"> |
|||
<div |
|||
v-if="visible && systemStore.isDebug" |
|||
ref="maskRef" |
|||
class="mask" |
|||
> |
|||
<div |
|||
ref="maskHeaderRef" class="mask-header" @mousedown="handleMouseDown" |
|||
@touchstart="handleMouseDown" |
|||
> |
|||
<p>{{ title }}</p> |
|||
<el-icon @click="systemStore.updateStreamVisible(false)"> |
|||
<Close /> |
|||
</el-icon> |
|||
</div> |
|||
<div ref="maskBodyRef" class="mask-body"> |
|||
<el-timeline> |
|||
<el-timeline-item |
|||
v-for="item in systemStore.systemList" :key="item" |
|||
:timestamp="JSON.stringify(item.content)" |
|||
> |
|||
<el-tag :type="statusMap[item.status]" class="mask-tag"> |
|||
{{ item.title }} |
|||
</el-tag> |
|||
</el-timeline-item> |
|||
</el-timeline> |
|||
</div> |
|||
</div> |
|||
</transition> |
|||
</teleport> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
.mask { |
|||
width: 800px; |
|||
height: 500px; |
|||
padding: 20px; |
|||
background: #fff; |
|||
box-shadow: var(--el-box-shadow-light); |
|||
position: absolute; |
|||
bottom: 130px; |
|||
right: 100px; |
|||
border-radius: 10px; |
|||
font-size: 30px; |
|||
.mask-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
height: 50px; |
|||
font-size: 30px; |
|||
border-bottom: 1px solid #ddd; |
|||
cursor: move; // 添加鼠标指针样式 |
|||
.el-icon svg{ |
|||
width: 30px; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
.mask-body { |
|||
padding: 10px; |
|||
height: calc(100% - 41px); |
|||
overflow: auto; |
|||
} |
|||
} |
|||
|
|||
/* 定义过渡效果 */ |
|||
.mask-fade-enter-active, .mask-fade-leave-active { |
|||
transition: transform 0.5s ease; |
|||
} |
|||
|
|||
.mask-fade-enter-from { |
|||
transform: translateX(100%); |
|||
} |
|||
|
|||
.mask-fade-leave-to { |
|||
transform: translateX(100%); |
|||
} |
|||
</style> |
@ -0,0 +1,7 @@ |
|||
export default { |
|||
props: ['row', 'render', 'index', 'column'], |
|||
inheritAttrs: false, |
|||
setup(props: any) { |
|||
return () => props.render(props.row) |
|||
}, |
|||
} |
@ -0,0 +1,145 @@ |
|||
<script setup lang="ts"> |
|||
import type { VNode } from 'vue' |
|||
import { onMounted, reactive } from 'vue' |
|||
import Expand from './expand' |
|||
|
|||
defineOptions({ |
|||
name: 'FtTable', |
|||
}) |
|||
const props = withDefaults(defineProps<TableProp>(), { |
|||
columns: () => [], |
|||
mustInit: true, |
|||
hasHeader: true, |
|||
}) |
|||
const emits = defineEmits([]) |
|||
enum ColumnType { |
|||
index = 'index', |
|||
selection = 'selection', |
|||
expand = 'expand', |
|||
} |
|||
interface TableColumn { |
|||
title: string |
|||
key: string |
|||
type?: ColumnType |
|||
width?: number // 列宽 |
|||
fixed?: 'left' | 'right' | undefined // 是否固定列 |
|||
render?: (row: any) => VNode // 内容自定义 |
|||
} |
|||
|
|||
interface Btn { |
|||
name: string |
|||
icon?: string |
|||
type?: string |
|||
serverUrl: string |
|||
} |
|||
|
|||
interface TableProp { |
|||
columns: TableColumn[] |
|||
getDataFn: (params: any) => Promise<any> // 表格数据的接口 |
|||
mustInit?: boolean // 是否在mounted里执行getDataFn |
|||
hasHeader?: boolean |
|||
btnList?: Btn[] |
|||
} |
|||
|
|||
// const attrs = useAttrs() |
|||
|
|||
async function methodParent(fn: any) { |
|||
const newFn = fn[0] === '/' ? fn.slice(1) : fn |
|||
emits(newFn as never) |
|||
} |
|||
|
|||
onMounted(() => { |
|||
if (props.mustInit) { |
|||
initData() |
|||
} |
|||
}) |
|||
|
|||
const state = reactive({ |
|||
loading: false, |
|||
dataTotal: 0, |
|||
tableData: [], |
|||
}) |
|||
|
|||
function initData() { |
|||
state.loading = true |
|||
props |
|||
.getDataFn({}) |
|||
.then((data) => { |
|||
console.log(data) |
|||
state.tableData = data |
|||
state.loading = false |
|||
}) |
|||
.finally(() => { |
|||
state.loading = false |
|||
}) |
|||
} |
|||
defineExpose({ |
|||
initData, |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<el-main> |
|||
<div v-if="hasHeader" class="header"> |
|||
<div v-for="btn in btnList" :key="btn.serverUrl"> |
|||
<el-button :icon="btn.icon" :type="btn.type" @click="methodParent(btn.serverUrl)"> |
|||
{{ btn.name }} |
|||
</el-button> |
|||
</div> |
|||
<div class="search"> |
|||
<el-input> |
|||
<template #suffix> |
|||
<el-icon class="el-input__icon"> |
|||
<search /> |
|||
</el-icon> |
|||
</template> |
|||
</el-input> |
|||
</div> |
|||
</div> |
|||
<el-table |
|||
v-loading="state.loading" |
|||
:="$attrs" |
|||
:data="state.tableData" |
|||
style="width: 100%" |
|||
height="calc(100% - 100px)" |
|||
:highlight-current-row="true" |
|||
class="container-table" |
|||
header-row-class-name="header-row-class" |
|||
> |
|||
<template v-for="(column, index) in columns" :key="column.key"> |
|||
<el-table-column |
|||
show-overflow-tooltip |
|||
:prop="column.key" |
|||
:label="column.title" |
|||
:width="column.width" |
|||
:type="column.type" |
|||
:fixed="column.fixed" |
|||
> |
|||
<template v-if="column.render" #default="scope"> |
|||
<Expand :column="column" :row="scope.row" :render="column.render" :index="index" /> |
|||
</template> |
|||
</el-table-column> |
|||
</template> |
|||
</el-table> |
|||
</el-main> |
|||
</template> |
|||
|
|||
<style lang="scss" scoped> |
|||
.header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
height: 100px; |
|||
.search { |
|||
width: 200px; |
|||
} |
|||
} |
|||
:deep(.header-row-class) { |
|||
th { |
|||
background: #f7f8fa; |
|||
font-weight: 900; |
|||
//border-bottom: none; |
|||
color: #000; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,28 @@ |
|||
<script setup lang="ts"> |
|||
|
|||
</script> |
|||
|
|||
<template> |
|||
<Teleport to="body"> |
|||
<div class="mask-box"> |
|||
设备急停中 |
|||
</div> |
|||
</Teleport> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
.mask-box { |
|||
position: absolute; |
|||
width: 100%; |
|||
height: 100%; |
|||
background: rgba(0, 0, 0, 0.7); |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
color: var(--el-color-danger); |
|||
font-size: 100px; |
|||
z-index: 10000; |
|||
top: 0; |
|||
left: 0; |
|||
} |
|||
</style> |
@ -0,0 +1,20 @@ |
|||
/// <reference types="vite/client" />
|
|||
|
|||
interface ImportMetaEnv { |
|||
readonly FT_NODE_ENV: string |
|||
readonly FT_WS_URL: string |
|||
readonly FT_PROXY: string |
|||
} |
|||
|
|||
interface ImportMeta { |
|||
readonly env: ImportMetaEnv |
|||
} |
|||
|
|||
declare const __APP_VERSION__: string; |
|||
|
|||
declare module '*.vue' { |
|||
import type { DefineComponent } from 'vue' |
|||
|
|||
const component: DefineComponent<object, object, any> |
|||
export default component |
|||
} |
@ -0,0 +1,76 @@ |
|||
<script setup lang="ts"> |
|||
import { authRoutes } from 'router/routes' |
|||
import { useRouter } from 'vue-router' |
|||
|
|||
const router = useRouter() |
|||
</script> |
|||
|
|||
<template> |
|||
<el-container class="main"> |
|||
<el-header class="header" /> |
|||
<el-container> |
|||
<el-aside class="aside"> |
|||
<div v-for="item in authRoutes" :key="item.path" class="aside-item" :class="{ 'aside-item-active': router.currentRoute.value.path === item.path }" @click="router.push(item.path)"> |
|||
<img src="../assets/images/home.svg" alt=""> |
|||
<span>{{ item.meta!.title }}</span> |
|||
</div> |
|||
</el-aside> |
|||
<el-container> |
|||
<el-main> |
|||
<div class="content"> |
|||
<router-view /> |
|||
</div> |
|||
</el-main> |
|||
</el-container> |
|||
</el-container> |
|||
<el-footer class="footer" /> |
|||
</el-container> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
.main { |
|||
box-sizing: border-box; |
|||
height: 100%; |
|||
background: #F6F6F6; |
|||
.header { |
|||
height: 60px; |
|||
} |
|||
} |
|||
.aside { |
|||
width: 150px; |
|||
overflow: auto; |
|||
padding-left: 10px; |
|||
.aside-item { |
|||
width: 100%; |
|||
height: 50px; |
|||
background: #fff; |
|||
border-radius: 10px; |
|||
color: #1989FA; |
|||
margin: 10px 0; |
|||
padding: 0 10px; |
|||
display: flex; |
|||
align-items: center; |
|||
img { |
|||
height: 80%; |
|||
margin-right: 20px; |
|||
} |
|||
} |
|||
.aside-item-active { |
|||
background: #1989FA; |
|||
color: #fff; |
|||
} |
|||
} |
|||
.el-main { |
|||
padding: 0 15px; |
|||
} |
|||
.content { |
|||
height: 100%; |
|||
background: #fff; |
|||
border-radius: 10px; |
|||
box-shadow: 0 0 1px rgba(0, 0, 0, 0.1); |
|||
padding: 10px; |
|||
} |
|||
.footer { |
|||
height: 60px; |
|||
} |
|||
</style> |
@ -0,0 +1,5 @@ |
|||
// 请求头里token的名称
|
|||
export const HEADER_TOKEN_KEY = 'Authorization' |
|||
|
|||
// sessionStorage里token的名称
|
|||
export const SESSIONSTORAGE_TOKEN_KEY = 'web_token' |
@ -0,0 +1,92 @@ |
|||
import { HEADER_TOKEN_KEY } from '@/libs/constant' |
|||
import axios from 'axios' |
|||
import { FtMessage } from 'libs/message' |
|||
import { getToken } from 'libs/token' |
|||
|
|||
const http = axios.create({ |
|||
baseURL: `/api`, |
|||
timeout: 1000 * 60, |
|||
}) |
|||
|
|||
// 请求拦截器
|
|||
http.interceptors.request.use( |
|||
(config) => { |
|||
if (getToken()) { |
|||
config.headers![HEADER_TOKEN_KEY] = getToken() |
|||
} |
|||
return config |
|||
}, |
|||
(error: any) => { |
|||
return Promise.reject(error) |
|||
}, |
|||
) |
|||
|
|||
// 响应拦截器
|
|||
http.interceptors.response.use( |
|||
(response) => { |
|||
if ( |
|||
response.status === 200 |
|||
&& response.data.code !== '00000' |
|||
) { |
|||
// 返回错误拦截
|
|||
FtMessage.error(response.data.msg) |
|||
return Promise.reject(response) |
|||
} |
|||
else if ( |
|||
response.config.url?.includes('/files/download') |
|||
|| response.config.url?.includes('downloadStream') |
|||
) { |
|||
return response.data |
|||
} |
|||
else if (response.data instanceof Blob) { |
|||
return response.data |
|||
} |
|||
return response.data.data // 返回数据体
|
|||
}, |
|||
(error: any) => { |
|||
console.log(error) |
|||
if (error.response && error.response.status === 401) { |
|||
FtMessage.error('账号权限过期') |
|||
// TODO 登出
|
|||
} |
|||
else { |
|||
if (error.message.includes('timeout')) { |
|||
FtMessage.error('请求超时') |
|||
} |
|||
else if (error.message.includes('Network')) { |
|||
FtMessage.error('网络连接错误') |
|||
} |
|||
else { |
|||
FtMessage.error('接口请求失败') |
|||
} |
|||
error.response = { |
|||
data: { |
|||
res: false, |
|||
}, |
|||
} |
|||
return Promise.reject(error.response) |
|||
} |
|||
}, |
|||
) |
|||
|
|||
// 封装 GET 请求
|
|||
export function get<T>(url: string, params?: any): Promise<T> { |
|||
return http.get(url, { params }) |
|||
} |
|||
|
|||
// 封装 POST 请求
|
|||
export function post<T>(url: string, data?: any): Promise<T> { |
|||
return http.post(url, data) |
|||
} |
|||
|
|||
// 封装 PUT 请求
|
|||
export function put<T>(url: string, data?: any): Promise<T> { |
|||
return http.put(url, data) |
|||
} |
|||
|
|||
// 封装 DELETE 请求
|
|||
export function del<T>(url: string, params?: any): Promise<T> { |
|||
return http.delete(url, { params }) |
|||
} |
|||
|
|||
export default http |
@ -0,0 +1,37 @@ |
|||
import { ElMessage } from 'element-plus' |
|||
|
|||
export const FtMessage = { |
|||
info: (message: string) => { |
|||
ElMessage({ |
|||
message, |
|||
type: 'info', |
|||
grouping: true, |
|||
plain: true, |
|||
}) |
|||
}, |
|||
success: (message: string) => { |
|||
ElMessage({ |
|||
message, |
|||
type: 'success', |
|||
grouping: true, |
|||
plain: true, |
|||
}) |
|||
}, |
|||
warning: (message: string) => { |
|||
ElMessage({ |
|||
message, |
|||
type: 'warning', |
|||
grouping: true, |
|||
plain: true, |
|||
}) |
|||
}, |
|||
error: (message: string) => { |
|||
ElMessage({ |
|||
message, |
|||
type: 'error', |
|||
grouping: true, |
|||
plain: true, |
|||
}) |
|||
}, |
|||
|
|||
} |
@ -0,0 +1,242 @@ |
|||
/* |
|||
* @description: 封装socket方法 |
|||
* @date: 2023-01-20 |
|||
* @author: 郭安鹏 |
|||
*/ |
|||
import { ElMessage } from 'element-plus' |
|||
import { ref } from 'vue' |
|||
|
|||
export const isClose = ref(true) |
|||
|
|||
interface socket { |
|||
appKey: any |
|||
websocket: any |
|||
connectURL: string |
|||
socket_open: boolean |
|||
hearBeat_timer: any |
|||
hearBeat_interval: number |
|||
is_reconnect: boolean |
|||
reconnect_count: number |
|||
reconnect_current: number |
|||
reconnect_number: number |
|||
reconnect_timer: any |
|||
reconnect_interval: number |
|||
receiveMessageCallBackObj: { [key: string]: any[] } |
|||
initCallBacks: any |
|||
// eslint-disable-next-line ts/no-unsafe-function-type
|
|||
receiveMessage: Function |
|||
// eslint-disable-next-line ts/no-unsafe-function-type
|
|||
registerCallback: Function |
|||
// eslint-disable-next-line ts/no-unsafe-function-type
|
|||
unregisterCallback: Function |
|||
// eslint-disable-next-line ts/no-unsafe-function-type
|
|||
registerInitCallback: Function |
|||
// eslint-disable-next-line ts/no-unsafe-function-type
|
|||
init: (receiveMessage?: Function | null, type?: string, connectURL?: string) => any |
|||
heartbeat: () => void |
|||
heartSend: () => void |
|||
send: (data: any, callback?: any) => void |
|||
close: () => void |
|||
reconnect: () => void |
|||
sendAppJoin: () => void |
|||
} |
|||
|
|||
// eslint-disable-next-line ts/no-redeclare
|
|||
export const socket: socket = { |
|||
appKey: null, |
|||
websocket: null, |
|||
connectURL: import.meta.env.FT_WS_URL, |
|||
// 开启标识
|
|||
socket_open: false, |
|||
// 心跳timer
|
|||
hearBeat_timer: null, |
|||
// 心跳发送频率
|
|||
hearBeat_interval: 5000, |
|||
// 是否需要重连
|
|||
is_reconnect: true, |
|||
// 重连次数
|
|||
reconnect_count: 10, |
|||
// 已发起重连次数
|
|||
reconnect_current: 1, |
|||
// 网络错误提示此时
|
|||
reconnect_number: 0, |
|||
// 重连timer
|
|||
reconnect_timer: null, |
|||
// 重连频率
|
|||
reconnect_interval: 1000, |
|||
|
|||
receiveMessageCallBackObj: {}, |
|||
initCallBacks: [], |
|||
// eslint-disable-next-line ts/no-unsafe-function-type
|
|||
registerInitCallback: (fn: Function, ...args: any) => { |
|||
// socket 连接成功后的回调
|
|||
socket.initCallBacks.push({ |
|||
fn, |
|||
args, |
|||
}) |
|||
}, |
|||
// 接收消息的方法
|
|||
receiveMessage: (e: any) => { |
|||
const message = JSON.parse(e.data) |
|||
const callbacks = socket.receiveMessageCallBackObj[message.type] |
|||
if (callbacks) { |
|||
callbacks.forEach((fn) => { |
|||
fn(message.data) |
|||
}) |
|||
} |
|||
else { |
|||
// console.error('请注册当前类型的回调函数', message)
|
|||
} |
|||
}, |
|||
|
|||
// 修改 registerCallback 方法
|
|||
registerCallback: (fn: any, type: any) => { |
|||
if (!socket.receiveMessageCallBackObj[type]) { |
|||
socket.receiveMessageCallBackObj[type] = [] |
|||
} |
|||
socket.receiveMessageCallBackObj[type].push(fn) |
|||
}, |
|||
|
|||
// 添加 unregisterCallback 方法
|
|||
unregisterCallback: (fn: any, type: any) => { |
|||
if (socket.receiveMessageCallBackObj[type]) { |
|||
const index = socket.receiveMessageCallBackObj[type].indexOf(fn) |
|||
if (index !== -1) { |
|||
socket.receiveMessageCallBackObj[type].splice(index, 1) |
|||
} |
|||
} |
|||
}, |
|||
|
|||
init: async ( |
|||
receiveMessageCallBack: any, |
|||
type?: string, |
|||
connectURL?: string, |
|||
reconnection?: boolean, |
|||
) => { |
|||
if (!('WebSocket' in window)) { |
|||
ElMessage.warning('浏览器不支持WebSocket') |
|||
return null |
|||
} |
|||
|
|||
// 注册回调函数
|
|||
if (receiveMessageCallBack && type) { |
|||
socket.registerCallback(receiveMessageCallBack, type) |
|||
} |
|||
// 已经创建过连接无需重复创建
|
|||
if (socket.websocket && !reconnection) { |
|||
return socket.websocket |
|||
} |
|||
|
|||
await new Promise((rs) => { |
|||
socket.websocket = new WebSocket(connectURL || socket.connectURL) |
|||
// 消息接收
|
|||
socket.websocket.onmessage = (e: any) => { |
|||
socket.receiveMessage(e) |
|||
} |
|||
// socket关闭
|
|||
socket.websocket.onclose = () => { |
|||
console.error('onclose') |
|||
|
|||
clearInterval(socket.hearBeat_interval) |
|||
socket.socket_open = false |
|||
isClose.value = true |
|||
// 需要重新连接
|
|||
if (socket.is_reconnect) { |
|||
socket.reconnect_timer = setTimeout(() => { |
|||
// 超过重连次数
|
|||
// if (socket.reconnect_current > socket.reconnect_count) {
|
|||
// clearTimeout(socket.reconnect_timer);
|
|||
// socket.is_reconnect = false;
|
|||
// console.error('超出重连次数,不再重连', new Date());
|
|||
// return;
|
|||
// }
|
|||
// 记录重连次数
|
|||
socket.reconnect_current++ |
|||
socket.reconnect() |
|||
}, socket.reconnect_interval) |
|||
} |
|||
} |
|||
// 连接发生错误
|
|||
socket.websocket.onerror = function () { |
|||
console.error('onerror') |
|||
isClose.value = true |
|||
socket.socket_open = false |
|||
} |
|||
// 连接成功
|
|||
socket.websocket.onopen = function () { |
|||
socket.socket_open = true |
|||
socket.is_reconnect = true |
|||
// 开启心跳
|
|||
socket.heartbeat() |
|||
// 连接成功后发起app加入消息
|
|||
socket.sendAppJoin() |
|||
|
|||
for (const fnItem of socket.initCallBacks) { |
|||
fnItem.fn(...fnItem.args) |
|||
} |
|||
|
|||
isClose.value = false |
|||
|
|||
rs(true) // socket 已连接
|
|||
} |
|||
}) |
|||
}, |
|||
|
|||
send: (data, callback = null) => { |
|||
// 开启状态直接发送
|
|||
if (socket.websocket.readyState === socket.websocket.OPEN) { |
|||
socket.websocket.send(JSON.stringify(data)) |
|||
if (callback) { |
|||
callback() |
|||
} |
|||
} |
|||
else { |
|||
clearInterval(socket.hearBeat_timer) |
|||
socket.reconnect_number++ |
|||
} |
|||
}, |
|||
|
|||
heartbeat: () => { |
|||
if (socket.hearBeat_timer) { |
|||
clearInterval(socket.hearBeat_timer) |
|||
} |
|||
socket.hearBeat_timer = setInterval(() => { |
|||
socket.heartSend() |
|||
}, socket.hearBeat_interval) |
|||
}, |
|||
heartSend: () => { |
|||
socket.send({ |
|||
type: 'ping', // ping
|
|||
}) |
|||
}, |
|||
close: () => { |
|||
clearInterval(socket.hearBeat_timer) |
|||
socket.is_reconnect = false |
|||
socket.websocket && socket.websocket.close() |
|||
socket.websocket = null |
|||
}, |
|||
|
|||
/** |
|||
* 重新连接 |
|||
*/ |
|||
reconnect: () => { |
|||
if (socket.websocket) { |
|||
// 需要重连
|
|||
if (socket.is_reconnect) { |
|||
socket.websocket.close() |
|||
socket.websocket = null |
|||
socket.init() |
|||
} |
|||
else { |
|||
socket.close() |
|||
} |
|||
} |
|||
}, |
|||
|
|||
sendAppJoin: () => { |
|||
socket.send({ |
|||
appKey: socket.appKey, |
|||
type: 1, // appJoin
|
|||
}) |
|||
}, |
|||
} |
@ -0,0 +1,13 @@ |
|||
import { SESSIONSTORAGE_TOKEN_KEY } from './constant' |
|||
|
|||
export function getToken() { |
|||
return sessionStorage[SESSIONSTORAGE_TOKEN_KEY] |
|||
} |
|||
|
|||
export function setToken(token: string) { |
|||
sessionStorage[SESSIONSTORAGE_TOKEN_KEY] = token |
|||
} |
|||
|
|||
export function delToken() { |
|||
sessionStorage.removeItem(SESSIONSTORAGE_TOKEN_KEY) |
|||
} |
@ -0,0 +1,74 @@ |
|||
import { FtMessage } from 'libs/message' |
|||
import { socket } from 'libs/socket' |
|||
import { useSystemStore } from 'stores/useSystemStore' |
|||
|
|||
export const sendControl = async (params: any) => { |
|||
if (!params.cmdId) { |
|||
params.cmdId = Date.now().toString() |
|||
} |
|||
const systemStore = useSystemStore() |
|||
|
|||
systemStore.systemList = [] |
|||
const cmdName = cmdNameMap[params.cmdCode as keyof typeof cmdNameMap] || params.cmdCode |
|||
|
|||
socket.init((data: any) => { |
|||
if (data.cmdId === params.cmdId) { |
|||
systemStore.pushSystemList(data) |
|||
} |
|||
}, 'cmd_debug') |
|||
socket.init((data: any) => { |
|||
if (data.cmdId === params.cmdId) { |
|||
systemStore.pushSystemList(data) |
|||
} |
|||
}, 'cmd_response') |
|||
// TODO 接口调用
|
|||
// await (type === 'debug' ? debugControl(params) : control(params))
|
|||
systemStore.updateStreamVisible(true) |
|||
FtMessage.success(`[${cmdName}]已发送`) |
|||
} |
|||
|
|||
export const cmdNameMap = { |
|||
|
|||
} |
|||
|
|||
export const generateColors = (count: number): string[] => { |
|||
const colors: string[] = [] |
|||
for (let i = 0; i < count; i++) { |
|||
// Increase hue step to make colors more distinct
|
|||
const hue = (i * 360) / count |
|||
// Introduce variation in saturation and lightness with larger steps
|
|||
const saturation = 30 + (i % 5) * 20 // Alternate between 30, 50, 70, 90, 110
|
|||
const lightness = 30 + (i % 4) * 20 // Alternate between 30, 50, 70, 90
|
|||
// Convert HSL to RGB
|
|||
const rgb = hslToRgb(hue, saturation, lightness) |
|||
// Convert RGB to hex
|
|||
const hex = rgbToHex(rgb.r, rgb.g, rgb.b) |
|||
colors.push(hex) |
|||
} |
|||
return colors |
|||
} |
|||
|
|||
const hslToRgb = (h: number, s: number, l: number): { r: number, g: number, b: number } => { |
|||
s /= 100 |
|||
l /= 100 |
|||
const k = (n: number) => (n + h / 30) % 12 |
|||
const a = s * Math.min(l, 1 - l) |
|||
const f = (n: number) => |
|||
l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))) |
|||
return { |
|||
r: Math.round(f(0) * 255), |
|||
g: Math.round(f(8) * 255), |
|||
b: Math.round(f(4) * 255), |
|||
} |
|||
} |
|||
|
|||
const rgbToHex = (r: number, g: number, b: number): string => { |
|||
const toHex = (c: number) => `0${c.toString(16)}`.slice(-2) |
|||
return `#${toHex(r)}${toHex(g)}${toHex(b)}` |
|||
} |
|||
|
|||
export const colors = generateColors(100) |
|||
|
|||
export function isNumber(value: any) { |
|||
return typeof value === 'number' && !Number.isNaN(value) |
|||
} |
@ -0,0 +1,27 @@ |
|||
import * as ElementPlusIconsVue from '@element-plus/icons-vue' // 引入 ElementPlusIconsVue
|
|||
import FtButton from 'components/common/FTButton/index.vue' |
|||
import FtDialog from 'components/common/FTDialog/index.vue' |
|||
import FtStream from 'components/common/FTStream/index.vue' |
|||
import FtTable from 'components/common/FTTable/index.vue' |
|||
import ElementPlus from 'element-plus' |
|||
import locale from 'element-plus/es/locale/lang/zh-cn' |
|||
import pinia from 'stores/index' |
|||
import { createApp } from 'vue' |
|||
import App from './app.vue' |
|||
import router from './router' |
|||
import 'element-plus/dist/index.css' |
|||
import 'assets/styles/main.scss' |
|||
|
|||
const app = createApp(App) |
|||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { |
|||
app.component(key, component) |
|||
} |
|||
app.use(pinia) |
|||
app.component('FtTable', FtTable) |
|||
app.component('FtButton', FtButton) |
|||
app.component('FtDialog', FtDialog) |
|||
app.component('FtStream', FtStream) |
|||
app |
|||
.use(router) |
|||
.use(ElementPlus, { locale, zIndex: 3000 }) |
|||
.mount('#app') |
@ -0,0 +1,26 @@ |
|||
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router' |
|||
import { getToken } from '@/libs/token' |
|||
import { createRouter, createWebHashHistory } from 'vue-router' |
|||
import routes from './routes' |
|||
|
|||
const router = createRouter({ |
|||
history: createWebHashHistory(), |
|||
routes, |
|||
}) |
|||
|
|||
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => { |
|||
if (getToken()) { |
|||
next() |
|||
} |
|||
else { |
|||
// 未登录
|
|||
if (to.name === 'login') { |
|||
next() |
|||
} |
|||
else { |
|||
next({ name: 'login' }) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
export default router |
@ -0,0 +1,46 @@ |
|||
import type { RouteRecordRaw } from 'vue-router' |
|||
|
|||
const authRoutes: RouteRecordRaw[] = [ |
|||
{ |
|||
path: '/home', |
|||
name: 'home', |
|||
component: () => import('views/home/index.vue'), |
|||
meta: { |
|||
isDefault: true, |
|||
title: '首页', |
|||
}, |
|||
}, |
|||
{ |
|||
path: '/craft', |
|||
name: 'craft', |
|||
component: () => import('views/craft/index.vue'), |
|||
meta: { |
|||
isDefault: true, |
|||
title: '工艺', |
|||
}, |
|||
}, |
|||
{ |
|||
path: '/debug', |
|||
name: 'debug', |
|||
component: () => import('views/debug/index.vue'), |
|||
meta: { |
|||
title: '调试', |
|||
}, |
|||
}, |
|||
] |
|||
const routes: RouteRecordRaw[] = [ |
|||
{ |
|||
path: '/login', |
|||
name: 'login', |
|||
component: () => import('../views/login/index.vue'), |
|||
}, |
|||
{ |
|||
path: '/', |
|||
component: () => import('../layouts/default.vue'), |
|||
redirect: '/home', |
|||
children: authRoutes, |
|||
}, |
|||
] |
|||
|
|||
export { authRoutes } |
|||
export default routes |
@ -0,0 +1,7 @@ |
|||
import { createPinia } from 'pinia' |
|||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' |
|||
|
|||
const pinia = createPinia() |
|||
pinia.use(piniaPluginPersistedstate) |
|||
|
|||
export default pinia |
@ -0,0 +1,22 @@ |
|||
import { defineStore } from 'pinia' |
|||
|
|||
export const useSystemStore = defineStore('system', { |
|||
state: () => ({ |
|||
systemStatus: { |
|||
}, |
|||
systemSensor: { |
|||
humidity: 0, |
|||
}, |
|||
isDebug: import.meta.env.FT_NODE_ENV === 'dev', |
|||
streamVisible: false, |
|||
systemList: [{ cmdCode: '' }], |
|||
}), |
|||
actions: { |
|||
updateStreamVisible(bool: boolean) { |
|||
this.streamVisible = bool |
|||
}, |
|||
pushSystemList(text: any) { |
|||
this.systemList.push(text) |
|||
}, |
|||
}, |
|||
}) |
@ -0,0 +1,11 @@ |
|||
<script setup lang="ts"> |
|||
|
|||
</script> |
|||
|
|||
<template> |
|||
<div>工艺</div> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
|
|||
</style> |
@ -0,0 +1,3 @@ |
|||
<template> |
|||
调试 |
|||
</template> |
@ -0,0 +1,12 @@ |
|||
<script setup lang="ts"> |
|||
|
|||
</script> |
|||
|
|||
<template> |
|||
<div> |
|||
首页 |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
</style> |
@ -0,0 +1,12 @@ |
|||
<script setup lang="ts"> |
|||
import { setToken } from 'libs/token' |
|||
|
|||
setToken('111') |
|||
</script> |
|||
|
|||
<template> |
|||
<div>登录</div> |
|||
</template> |
|||
|
|||
<style scoped lang="scss"> |
|||
</style> |
@ -0,0 +1,38 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"types": ["node"], |
|||
"composite": true, |
|||
"target": "esnext", |
|||
"jsx": "preserve", |
|||
"jsxFactory": "h", |
|||
"jsxFragmentFactory": "Fragment", |
|||
"lib": ["esnext", "dom"], |
|||
"useDefineForClassFields": true, |
|||
"baseUrl": ".", |
|||
"module": "esnext", |
|||
"moduleResolution": "node", |
|||
"paths": { |
|||
"@/*": ["src/*"], |
|||
"apis/*": ["src/apis/*"], |
|||
"assets/*": ["src/assets/*"], |
|||
"components/*": ["src/components/*"], |
|||
"hooks/*": ["src/hooks/*"], |
|||
"languages/*": ["src/languages/*"], |
|||
"libs/*": ["src/libs/*"], |
|||
"stores/*": ["src/stores/*"], |
|||
"views/*": ["src/views/*"], |
|||
"router/*": ["src/router/*"] |
|||
}, |
|||
"resolveJsonModule": true, |
|||
"strict": true, |
|||
"strictPropertyInitialization": false, |
|||
"noImplicitThis": false, |
|||
"sourceMap": true, |
|||
"esModuleInterop": true, |
|||
"skipLibCheck": true |
|||
}, |
|||
"types": ["vite/client", "jest", "node", "element-plus/global.d.ts", "lodash"], |
|||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/**/*.vue"], |
|||
"exclude": [ |
|||
] |
|||
} |
@ -0,0 +1,96 @@ |
|||
import { resolve } from 'node:path' |
|||
import vue from '@vitejs/plugin-vue' |
|||
import AutoImport from 'unplugin-auto-import/vite' |
|||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' |
|||
import { defineConfig } from 'vite' |
|||
import viteCompression from 'vite-plugin-compression' |
|||
import eslintPlugin from 'vite-plugin-eslint' |
|||
import packageJson from './package.json' |
|||
|
|||
const Timestamp = new Date().getTime() |
|||
export default defineConfig({ |
|||
define: { |
|||
__APP_VERSION__: JSON.stringify(packageJson.version), |
|||
}, |
|||
base: './', |
|||
envPrefix: 'FT_', |
|||
esbuild: { |
|||
drop: process.env.NODE_ENV === 'production' ? ['console'] : [], |
|||
}, |
|||
build: { |
|||
sourcemap: false, |
|||
outDir: `dist-v${packageJson.version}`, |
|||
rollupOptions: { |
|||
output: { |
|||
manualChunks(id) { |
|||
if (id.includes('node_modules')) { |
|||
return id.toString().split('node_modules/')[1].split('/')[0].toString() |
|||
} |
|||
}, |
|||
chunkFileNames: (chunkInfo) => { |
|||
const facadeModuleId = chunkInfo.facadeModuleId |
|||
? chunkInfo.facadeModuleId.split('/') |
|||
: [] |
|||
const fileName = facadeModuleId.slice(-2)[0] || '[name]' |
|||
return `js/${fileName}/[name].[hash].${Timestamp}.js` |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
plugins: [ |
|||
vue(), |
|||
AutoImport({ |
|||
imports: ['vue', 'vue-router', { |
|||
vue: ['withModifiers'], |
|||
}, { |
|||
from: 'element-plus/es', |
|||
imports: ['TabPaneName'], |
|||
type: true, |
|||
}], |
|||
dts: true, |
|||
eslintrc: { |
|||
enabled: true, |
|||
}, |
|||
resolvers: [ElementPlusResolver({ importStyle: 'sass' })], |
|||
}), |
|||
eslintPlugin({ |
|||
cache: false, // 禁用缓存,以确保每次修改后都能及时生效
|
|||
}), |
|||
viteCompression({ |
|||
verbose: true, |
|||
disable: false, |
|||
threshold: 10240, |
|||
algorithm: 'gzip', |
|||
ext: '.gz', |
|||
}), |
|||
], |
|||
resolve: { |
|||
alias: { |
|||
'@': resolve(__dirname, './src'), |
|||
'apis': resolve(__dirname, 'src/apis'), |
|||
'assets': resolve(__dirname, 'src/assets'), |
|||
'components': resolve(__dirname, 'src/components'), |
|||
'hooks': resolve(__dirname, 'src/hooks'), |
|||
'libs': resolve(__dirname, 'src/libs'), |
|||
'stores': resolve(__dirname, 'src/stores'), |
|||
'views': resolve(__dirname, 'src/views'), |
|||
'router': resolve(__dirname, 'src/router'), |
|||
}, |
|||
}, |
|||
// 本地化配置
|
|||
server: { |
|||
// open: true,
|
|||
hmr: true, |
|||
port: 3010, |
|||
host: '0.0.0.0', |
|||
proxy: { |
|||
'/api': { |
|||
target: 'http://192.168.1.199:8080', |
|||
// target: 'http://192.168.1.200:8080',
|
|||
// secure: false,
|
|||
changeOrigin: true, // 是否跨域
|
|||
rewrite: path => path.replace(/^\/api/, 'api'), |
|||
}, |
|||
}, |
|||
}, |
|||
}) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue