Browse Source

feat:project init

feature/three
guoapeng 3 months ago
commit
93e810a17e
  1. 6
      .env.dev
  2. 6
      .env.pre
  3. 6
      .env.prod
  4. 6
      .env.test
  5. 27
      .gitignore
  6. 0
      .npmrc
  7. 53
      .postcssrc.js
  8. 10
      .prettierrc
  9. 23
      README.md
  10. BIN
      commitlint.config.js
  11. 19
      eslint.config.js
  12. 41
      increment-version.js
  13. 27
      index.html
  14. 86
      package.json
  15. BIN
      public/favicon.ico
  16. 21
      src/app.vue
  17. 1
      src/assets/images/home.svg
  18. 91
      src/assets/styles/common.scss
  19. 19
      src/assets/styles/element.scss
  20. 12
      src/assets/styles/main.scss
  21. 126
      src/components/common/FTButton/index.vue
  22. 79
      src/components/common/FTDialog/index.vue
  23. 169
      src/components/common/FTStream/index.vue
  24. 7
      src/components/common/FTTable/expand.ts
  25. 145
      src/components/common/FTTable/index.vue
  26. 28
      src/components/home/Stop/index.vue
  27. 20
      src/env.d.ts
  28. 76
      src/layouts/default.vue
  29. 5
      src/libs/constant.ts
  30. 92
      src/libs/http.ts
  31. 37
      src/libs/message.ts
  32. 242
      src/libs/socket.ts
  33. 13
      src/libs/token.ts
  34. 74
      src/libs/utils.ts
  35. 27
      src/main.ts
  36. 26
      src/router/index.ts
  37. 46
      src/router/routes.ts
  38. 7
      src/stores/index.ts
  39. 22
      src/stores/useSystemStore.ts
  40. 11
      src/views/craft/index.vue
  41. 3
      src/views/debug/index.vue
  42. 12
      src/views/home/index.vue
  43. 12
      src/views/login/index.vue
  44. 38
      tsconfig.json
  45. 96
      vite.config.ts

6
.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

6
.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

6
.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

6
.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

27
.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?

0
.npmrc

53
.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,
},
},
}

10
.prettierrc

@ -0,0 +1,10 @@
{
"printWidth": 120,
"tabWidth": 2,
"endOfLine": "lf",
"singleQuote": true,
"semi": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid"
}

23
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 // 运行项目
```

BIN
commitlint.config.js

19
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' },
})

41
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);
}

27
index.html

@ -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>

86
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"
]
}
}

BIN
public/favicon.ico

21
src/app.vue

@ -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>

1
src/assets/images/home.svg

@ -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>

91
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;
}
}

19
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;
}

12
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;
}

126
src/components/common/FTButton/index.vue

@ -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>

79
src/components/common/FTDialog/index.vue

@ -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>

169
src/components/common/FTStream/index.vue

@ -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>

7
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)
},
}

145
src/components/common/FTTable/index.vue

@ -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 // mountedgetDataFn
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>

28
src/components/home/Stop/index.vue

@ -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>

20
src/env.d.ts

@ -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
}

76
src/layouts/default.vue

@ -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>

5
src/libs/constant.ts

@ -0,0 +1,5 @@
// 请求头里token的名称
export const HEADER_TOKEN_KEY = 'Authorization'
// sessionStorage里token的名称
export const SESSIONSTORAGE_TOKEN_KEY = 'web_token'

92
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<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

37
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,
})
},
}

242
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
})
},
}

13
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)
}

74
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)
}

27
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')

26
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

46
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

7
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

22
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)
},
},
})

11
src/views/craft/index.vue

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

3
src/views/debug/index.vue

@ -0,0 +1,3 @@
<template>
调试
</template>

12
src/views/home/index.vue

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

12
src/views/login/index.vue

@ -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>

38
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": [
]
}

96
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'),
},
},
},
})
Loading…
Cancel
Save