commit 93e810a17e0745c65d4ccd42a40841a5f5591502 Author: guoapeng Date: Sat Apr 26 17:51:47 2025 +0800 feat:project init 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 0000000..6f9b786 Binary files /dev/null and b/commitlint.config.js differ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6304f0a --- /dev/null +++ b/eslint.config.js @@ -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' }, +}) diff --git a/increment-version.js b/increment-version.js new file mode 100644 index 0000000..57b3e92 --- /dev/null +++ b/increment-version.js @@ -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); +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..c6805e6 --- /dev/null +++ b/index.html @@ -0,0 +1,27 @@ + + + + + + + 石墨消解仪 + + + +
+ + + 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 0000000..c20ae0a Binary files /dev/null and b/public/favicon.ico differ 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'), + }, + }, + }, +})