From 93e810a17e0745c65d4ccd42a40841a5f5591502 Mon Sep 17 00:00:00 2001 From: guoapeng Date: Sat, 26 Apr 2025 17:51:47 +0800 Subject: [PATCH] feat:project init --- .env.dev | 6 + .env.pre | 6 + .env.prod | 6 + .env.test | 6 + .gitignore | 27 ++++ .npmrc | 0 .postcssrc.js | 53 +++++++ .prettierrc | 10 ++ README.md | 23 +++ commitlint.config.js | Bin 0 -> 1292 bytes eslint.config.js | 19 +++ increment-version.js | 41 ++++++ index.html | 27 ++++ package.json | 86 +++++++++++ public/favicon.ico | Bin 0 -> 4286 bytes src/app.vue | 21 +++ src/assets/images/home.svg | 1 + src/assets/styles/common.scss | 91 ++++++++++++ src/assets/styles/element.scss | 19 +++ src/assets/styles/main.scss | 12 ++ src/components/common/FTButton/index.vue | 126 ++++++++++++++++ src/components/common/FTDialog/index.vue | 79 ++++++++++ src/components/common/FTStream/index.vue | 169 +++++++++++++++++++++ src/components/common/FTTable/expand.ts | 7 + src/components/common/FTTable/index.vue | 145 ++++++++++++++++++ src/components/home/Stop/index.vue | 28 ++++ src/env.d.ts | 20 +++ src/layouts/default.vue | 76 ++++++++++ src/libs/constant.ts | 5 + src/libs/http.ts | 92 ++++++++++++ src/libs/message.ts | 37 +++++ src/libs/socket.ts | 242 +++++++++++++++++++++++++++++++ src/libs/token.ts | 13 ++ src/libs/utils.ts | 74 ++++++++++ src/main.ts | 27 ++++ src/router/index.ts | 26 ++++ src/router/routes.ts | 46 ++++++ src/stores/index.ts | 7 + src/stores/useSystemStore.ts | 22 +++ src/views/craft/index.vue | 11 ++ src/views/debug/index.vue | 3 + src/views/home/index.vue | 12 ++ src/views/login/index.vue | 12 ++ tsconfig.json | 38 +++++ vite.config.ts | 96 ++++++++++++ 45 files changed, 1867 insertions(+) create mode 100644 .env.dev create mode 100644 .env.pre create mode 100644 .env.prod create mode 100644 .env.test create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .postcssrc.js create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 commitlint.config.js create mode 100644 eslint.config.js create mode 100644 increment-version.js create mode 100644 index.html create mode 100644 package.json create mode 100644 public/favicon.ico create mode 100644 src/app.vue create mode 100644 src/assets/images/home.svg create mode 100644 src/assets/styles/common.scss create mode 100644 src/assets/styles/element.scss create mode 100644 src/assets/styles/main.scss create mode 100644 src/components/common/FTButton/index.vue create mode 100644 src/components/common/FTDialog/index.vue create mode 100644 src/components/common/FTStream/index.vue create mode 100644 src/components/common/FTTable/expand.ts create mode 100644 src/components/common/FTTable/index.vue create mode 100644 src/components/home/Stop/index.vue create mode 100644 src/env.d.ts create mode 100644 src/layouts/default.vue create mode 100644 src/libs/constant.ts create mode 100644 src/libs/http.ts create mode 100644 src/libs/message.ts create mode 100644 src/libs/socket.ts create mode 100644 src/libs/token.ts create mode 100644 src/libs/utils.ts create mode 100644 src/main.ts create mode 100644 src/router/index.ts create mode 100644 src/router/routes.ts create mode 100644 src/stores/index.ts create mode 100644 src/stores/useSystemStore.ts create mode 100644 src/views/craft/index.vue create mode 100644 src/views/debug/index.vue create mode 100644 src/views/home/index.vue create mode 100644 src/views/login/index.vue create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..4d4fdee --- /dev/null +++ b/.env.dev @@ -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 \ No newline at end of file diff --git a/.env.pre b/.env.pre new file mode 100644 index 0000000..6485b6d --- /dev/null +++ b/.env.pre @@ -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 \ No newline at end of file diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..4c0f215 --- /dev/null +++ b/.env.prod @@ -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 \ No newline at end of file diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..e45cade --- /dev/null +++ b/.env.test @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef0c6b7 --- /dev/null +++ b/.gitignore @@ -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? diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e69de29 diff --git a/.postcssrc.js b/.postcssrc.js new file mode 100644 index 0000000..ba1e50f --- /dev/null +++ b/.postcssrc.js @@ -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, + + }, + + }, + +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1120042 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "endOfLine": "lf", + "singleQuote": true, + "semi": true, + "trailingComma": "none", + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4768f5 --- /dev/null +++ b/README.md @@ -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 // 运行项目 +``` diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..6f9b78661b2966826e52fdc565b7e3d6ee59ca83 GIT binary patch literal 1292 zcmZuxO=}ZT6uq;kwAPSaP*)=^#zk!hcPU*|Tu2ZzHYvpz)0#|QK5R3IX(nkB+SC#$ zl1f?xLBWrmy7NZ}uDTS&g&PrFNET8Ql^V~T$(W=LLq6Vn=bn4cz4uM$kDyh`Q;rH$ zB$*aTP?F+g(z3O#(NXNmG=WE^&+GL>qxAajiIBa^u|-4}h+CmaAflwuH0j{W;>qB2 z8Q%teC#+oq>={bY+3u=jMHjJ`11b)*LNOW}$T|&ffrt6iU@Af{XJbunna1fR&c`iJ zDPT_HQ$TzYAO1BEC7_^SxfHQ%Bl4BBRiMPTBHiRuohNjqGAqT55>rH|0=>OJCM`!N6BZX?C~L*cIp8GSV|kutG2N#%T86L)6rRsoaLru*f_5F!fs0VcCgQW z{O|0uP|Nh@E@j^~eTi?ekre;)ix=HnHP|y&mqC>l2RIt3U-Rd)qg6@uMZWAj*5>YU zm4`coTS(k7<%=C@iUM%`arkih$ER`mfUxPA=~n^+3(trH+jKifx7q6zIlD z>dD(hF!$b&k-7I?aEsP)zqwz76Y(?3Zr`*jY*2~bRSb1i9 q_WpdMw4d|~XHIsEE%7}lsa(r5W>Y)ttoK&o`u0|9*4 + + + + + + 石墨消解仪 + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..1839ef2 --- /dev/null +++ b/package.json @@ -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" + ] + } +} \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c20ae0ab4ecacd5cba1f3e997c69a7773277d4eb GIT binary patch literal 4286 zcmc(iKWGzC9LHaX7_e%iR2_uc!NKCLk_6M7yC@D0T|{tn6vxy>&_NtT#M;Gz)LsMb zieym~!O=|!{uvw;|Dd!Qdw#xWuMMY{-o1OF37@>Xd++_e-+T9a?_G>3@Esd7`ZaT9 zV|E*34gip`_ zxOUX9@eJIJL5BO6{(CqC@nN(+&!{gCce($PD-Ra>-{^Bn`Yt?J=xYpK!T1L1%flTQ z%Ikstie>XU!I(8UbJ%?ToI#zCI-*^r;;VjfaxiYG~>=@(2@g#hKcz@UDnlIns z19X%idZ&R+oHs#!U27)BixLz~<%!;Jra>MP`?#!&^i>j9Xzt8{=F`AE^y>F`*oF3O zSRAy6xc-`l>K3%Gg7%q#jl=)+*F4nZ;R1Xdl!qk!RS&IjJwJ~AGjN`l{q#c*EppNi z^KvixH{dHcdmiiQNA;}b_^571`vmB}h}QYsKBD+(O7x;Rx`*S7@B)5T=Jk6rlukBwiVSbFGe;#hZLwEy; z>sY^-wsj4jPn~xkbhm=`*-5wzEqDpDF+9aj ddxZ|=XIO+TEJN6^P1yk|0xkx@6m+0#{s4A#hl~IK literal 0 HcmV?d00001 diff --git a/src/app.vue b/src/app.vue new file mode 100644 index 0000000..a61bf6e --- /dev/null +++ b/src/app.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/src/assets/images/home.svg b/src/assets/images/home.svg new file mode 100644 index 0000000..a627913 --- /dev/null +++ b/src/assets/images/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/styles/common.scss b/src/assets/styles/common.scss new file mode 100644 index 0000000..b634e27 --- /dev/null +++ b/src/assets/styles/common.scss @@ -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; + } +} diff --git a/src/assets/styles/element.scss b/src/assets/styles/element.scss new file mode 100644 index 0000000..4f256b5 --- /dev/null +++ b/src/assets/styles/element.scss @@ -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; +} \ No newline at end of file diff --git a/src/assets/styles/main.scss b/src/assets/styles/main.scss new file mode 100644 index 0000000..d8adc5e --- /dev/null +++ b/src/assets/styles/main.scss @@ -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; +} diff --git a/src/components/common/FTButton/index.vue b/src/components/common/FTButton/index.vue new file mode 100644 index 0000000..8ccabb1 --- /dev/null +++ b/src/components/common/FTButton/index.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/common/FTDialog/index.vue b/src/components/common/FTDialog/index.vue new file mode 100644 index 0000000..7b4f098 --- /dev/null +++ b/src/components/common/FTDialog/index.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/components/common/FTStream/index.vue b/src/components/common/FTStream/index.vue new file mode 100644 index 0000000..40ffb65 --- /dev/null +++ b/src/components/common/FTStream/index.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/components/common/FTTable/expand.ts b/src/components/common/FTTable/expand.ts new file mode 100644 index 0000000..cf617d2 --- /dev/null +++ b/src/components/common/FTTable/expand.ts @@ -0,0 +1,7 @@ +export default { + props: ['row', 'render', 'index', 'column'], + inheritAttrs: false, + setup(props: any) { + return () => props.render(props.row) + }, +} diff --git a/src/components/common/FTTable/index.vue b/src/components/common/FTTable/index.vue new file mode 100644 index 0000000..e01852d --- /dev/null +++ b/src/components/common/FTTable/index.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/components/home/Stop/index.vue b/src/components/home/Stop/index.vue new file mode 100644 index 0000000..955f320 --- /dev/null +++ b/src/components/home/Stop/index.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..91c2d53 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,20 @@ +/// + +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 + export default component +} diff --git a/src/layouts/default.vue b/src/layouts/default.vue new file mode 100644 index 0000000..f7b901f --- /dev/null +++ b/src/layouts/default.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/src/libs/constant.ts b/src/libs/constant.ts new file mode 100644 index 0000000..3023ca9 --- /dev/null +++ b/src/libs/constant.ts @@ -0,0 +1,5 @@ +// 请求头里token的名称 +export const HEADER_TOKEN_KEY = 'Authorization' + +// sessionStorage里token的名称 +export const SESSIONSTORAGE_TOKEN_KEY = 'web_token' diff --git a/src/libs/http.ts b/src/libs/http.ts new file mode 100644 index 0000000..15b740c --- /dev/null +++ b/src/libs/http.ts @@ -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(url: string, params?: any): Promise { + return http.get(url, { params }) +} + +// 封装 POST 请求 +export function post(url: string, data?: any): Promise { + return http.post(url, data) +} + +// 封装 PUT 请求 +export function put(url: string, data?: any): Promise { + return http.put(url, data) +} + +// 封装 DELETE 请求 +export function del(url: string, params?: any): Promise { + return http.delete(url, { params }) +} + +export default http diff --git a/src/libs/message.ts b/src/libs/message.ts new file mode 100644 index 0000000..68d017e --- /dev/null +++ b/src/libs/message.ts @@ -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, + }) + }, + +} diff --git a/src/libs/socket.ts b/src/libs/socket.ts new file mode 100644 index 0000000..4b309ab --- /dev/null +++ b/src/libs/socket.ts @@ -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 + }) + }, +} diff --git a/src/libs/token.ts b/src/libs/token.ts new file mode 100644 index 0000000..c017873 --- /dev/null +++ b/src/libs/token.ts @@ -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) +} diff --git a/src/libs/utils.ts b/src/libs/utils.ts new file mode 100644 index 0000000..fdac27c --- /dev/null +++ b/src/libs/utils.ts @@ -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) +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e9f6e16 --- /dev/null +++ b/src/main.ts @@ -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') diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..4693f6c --- /dev/null +++ b/src/router/index.ts @@ -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 diff --git a/src/router/routes.ts b/src/router/routes.ts new file mode 100644 index 0000000..d73496b --- /dev/null +++ b/src/router/routes.ts @@ -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 diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..e952ed8 --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,7 @@ +import { createPinia } from 'pinia' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' + +const pinia = createPinia() +pinia.use(piniaPluginPersistedstate) + +export default pinia diff --git a/src/stores/useSystemStore.ts b/src/stores/useSystemStore.ts new file mode 100644 index 0000000..e87ff03 --- /dev/null +++ b/src/stores/useSystemStore.ts @@ -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) + }, + }, +}) diff --git a/src/views/craft/index.vue b/src/views/craft/index.vue new file mode 100644 index 0000000..de16030 --- /dev/null +++ b/src/views/craft/index.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/src/views/debug/index.vue b/src/views/debug/index.vue new file mode 100644 index 0000000..b05790e --- /dev/null +++ b/src/views/debug/index.vue @@ -0,0 +1,3 @@ + diff --git a/src/views/home/index.vue b/src/views/home/index.vue new file mode 100644 index 0000000..9028204 --- /dev/null +++ b/src/views/home/index.vue @@ -0,0 +1,12 @@ + + + + + diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 0000000..a0f6163 --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,12 @@ + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5d6b883 --- /dev/null +++ b/tsconfig.json @@ -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": [ + ] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..639962b --- /dev/null +++ b/vite.config.ts @@ -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'), + }, + }, + }, +})