ADD
This commit is contained in:
parent
ab6da25601
commit
5a255f7068
45
.devcontainer/devcontainer.json
Normal file
45
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||||
|
{
|
||||||
|
"name": "Node.js & TypeScript",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-18-buster",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"antfu.goto-alias",
|
||||||
|
"mikestead.dotenv",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"Lokalise.i18n-ally",
|
||||||
|
"Vue.volar",
|
||||||
|
"steoates.autoimport",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"usernamehw.errorlens",
|
||||||
|
"shd101wyy.markdown-preview-enhanced",
|
||||||
|
"voorjaar.windicss-intellisense",
|
||||||
|
"yoavbls.pretty-ts-errors",
|
||||||
|
"bodil.prettier-toml",
|
||||||
|
"bungcip.better-toml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "yarn install",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
23
.editorConfig
Normal file
23
.editorConfig
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
# 匹配全部文件
|
||||||
|
[*]
|
||||||
|
# 设置字符集
|
||||||
|
charset = utf-8
|
||||||
|
# 缩进风格,可选 space、tab
|
||||||
|
indent_style = tab
|
||||||
|
# 缩进的空格数,当 indent_style = tab 将使用 tab_width
|
||||||
|
# 否则使用 indent_size
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
# 结尾换行符,可选 lf、cr、crlf
|
||||||
|
end_of_line = crlf
|
||||||
|
# 在文件结尾插入新行
|
||||||
|
insert_final_newline = true
|
||||||
|
# 删除一行中的前后空格
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# 匹配 md 结尾的文件
|
||||||
|
[*.md]
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
32
.env
Normal file
32
.env
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# 通用环境变量
|
||||||
|
|
||||||
|
# api baseURL
|
||||||
|
VITE_API_BASE_URL = localhost:8000
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
VITE_APP_TITLE = tov
|
||||||
|
|
||||||
|
|
||||||
|
# markdown 渲染支持
|
||||||
|
VITE_APP_MARKDOWN = true
|
||||||
|
|
||||||
|
|
||||||
|
# 开发时的开发面板
|
||||||
|
VITE_APP_DEV_TOOLS = true
|
||||||
|
|
||||||
|
|
||||||
|
# 生产时 mock 支持
|
||||||
|
VITE_APP_MOCK_IN_PRODUCTION = false
|
||||||
|
|
||||||
|
|
||||||
|
# 生产时压缩算法,可选 gzip, brotliCompress, deflate, deflateRaw
|
||||||
|
VITE_APP_COMPRESSINON_ALGORITHM = gzip
|
||||||
|
|
||||||
|
|
||||||
|
# api 自动按需引入
|
||||||
|
# 注意设置关闭时,其他的 api 自动按需引入也将自动关闭
|
||||||
|
VITE_APP_API_AUTO_IMPORT = true
|
||||||
|
|
||||||
|
|
||||||
|
# 项目级 api 自动按需导入
|
||||||
|
VITE_APP_DIR_API_AUTO_IMPORT = true
|
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# 忽略 eslint 检查
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
presets/types
|
36
.eslintrc.json
Normal file
36
.eslintrc.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"root": true, // 禁止接着往上找
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es2021": true,
|
||||||
|
"browser": true // 浏览器
|
||||||
|
},
|
||||||
|
"parser": "vue-eslint-parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true // 启用 jsx
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": ["prettier"],
|
||||||
|
"extends": [
|
||||||
|
"@unocss",
|
||||||
|
"eslint:recommended", // 内置规则
|
||||||
|
"plugin:vue/vue3-recommended", // 支持 vue sfc
|
||||||
|
"prettier",
|
||||||
|
"./presets/eslint/.eslintrc-auto-import.json"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
// 允许相同组件名
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
// prettier 作为规则
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
// 禁止使用 var,而应该用 let 或 const
|
||||||
|
"no-var": "error"
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"defineOptions": true,
|
||||||
|
"definePage": true
|
||||||
|
}
|
||||||
|
}
|
173
.gitignore
vendored
173
.gitignore
vendored
@ -1,162 +1,11 @@
|
|||||||
# ---> Python
|
dist
|
||||||
# Byte-compiled / optimized / DLL files
|
.nitro
|
||||||
__pycache__/
|
.output
|
||||||
*.py[cod]
|
env.d.ts
|
||||||
*$py.class
|
node_modules
|
||||||
|
.eslintcache
|
||||||
# C extensions
|
components.d.ts
|
||||||
*.so
|
type-router.d.ts
|
||||||
|
auto-imports.d.ts
|
||||||
# Distribution / packaging
|
.eslintrc-auto-import.json
|
||||||
.Python
|
vite.config.ts.timestamp*
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
|
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
presets/types
|
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
4
netlify.toml
Normal file
4
netlify.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[[redirects]]
|
||||||
|
to = "/index.html"
|
||||||
|
from = "/*"
|
||||||
|
status = 200
|
135
package.json
Normal file
135
package.json
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"name": "tov-template",
|
||||||
|
"version": "1.19.0",
|
||||||
|
"description": "vite + vue3 + ts 开箱即用现代开发模板 | vite + vue3 + ts out-of-the-box modern development template",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"test": "vitest",
|
||||||
|
"build": "vite build",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"dev:host": "vite --host",
|
||||||
|
"dev:open": "vite --open",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"coverage": "vitest --coverage",
|
||||||
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
"typecheck": "npx vue-tsc --noEmit",
|
||||||
|
"preview:host": "vite preview --host",
|
||||||
|
"preview:open": "vite preview --open",
|
||||||
|
"lint": "eslint --ext .ts,.js,.jsx,.vue .",
|
||||||
|
"release": "plop --plopfile scripts/release.cjs",
|
||||||
|
"auto:remove": "plop --plopfile scripts/remove.cjs",
|
||||||
|
"auto:create": "plop --plopfile scripts/create.cjs",
|
||||||
|
"build:debug": "cross-env NODE_ENV=debug vite build",
|
||||||
|
"safe:init": "plop --plopfile scripts/safe-init.cjs",
|
||||||
|
"deps:fresh": "plop --plopfile scripts/deps-fresh.cjs",
|
||||||
|
"lint:fix": "eslint --fix --ext .ts,.js,.jsx,.vue,.cjs ."
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.17.0"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@8.15.9",
|
||||||
|
"devDependencies": {
|
||||||
|
"@intlify/unplugin-vue-i18n": "^3.0.1",
|
||||||
|
"@types/ityped": "^1.0.3",
|
||||||
|
"@types/node": "^20.16.5",
|
||||||
|
"@typescript-eslint/parser": "7.18.0",
|
||||||
|
"@unocss/eslint-config": "0.62.3",
|
||||||
|
"@unocss/reset": "^0.62.3",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.3",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
|
"@vueuse/components": "^10.11.1",
|
||||||
|
"@vueuse/core": "^10.11.1",
|
||||||
|
"@vueuse/integrations": "^10.11.1",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"browserslist": "^4.23.3",
|
||||||
|
"c8": "^9.1.0",
|
||||||
|
"changelogen": "^0.5.5",
|
||||||
|
"consola": "^3.2.3",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
|
"eslint": "8.57.0",
|
||||||
|
"eslint-config-prettier": "9.1.0",
|
||||||
|
"eslint-plugin-prettier": "5.2.1",
|
||||||
|
"eslint-plugin-vue": "^9.28.0",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"husky": "^9.1.5",
|
||||||
|
"ityped": "^1.0.3",
|
||||||
|
"kolorist": "^1.8.0",
|
||||||
|
"lightningcss": "^1.26.0",
|
||||||
|
"lint-staged": "^15.2.10",
|
||||||
|
"local-pkg": "^0.5.0",
|
||||||
|
"markdown-it-prism": "^2.3.0",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"pinia": "^2.2.2",
|
||||||
|
"pinia-plugin-persistedstate": "^3.2.3",
|
||||||
|
"plop": "^4.0.1",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"prism-theme-vars": "^0.2.5",
|
||||||
|
"simple-git": "^3.26.0",
|
||||||
|
"taze": "^0.16.7",
|
||||||
|
"terser": "^5.31.6",
|
||||||
|
"typescript": "^5.5.4",
|
||||||
|
"unocss": "^0.62.3",
|
||||||
|
"unplugin-auto-import": "^0.18.2",
|
||||||
|
"unplugin-vue-components": "^0.27.4",
|
||||||
|
"unplugin-vue-markdown": "^0.26.2",
|
||||||
|
"unplugin-vue-router": "^0.10.8",
|
||||||
|
"vite": "^5.4.3",
|
||||||
|
"vite-auto-import-resolvers": "^3.2.1",
|
||||||
|
"vite-layers": "^0.5.2",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-env-types": "^0.1.4",
|
||||||
|
"vite-plugin-legacy-swc": "^1.2.1",
|
||||||
|
"vite-plugin-mock": "2.9.8",
|
||||||
|
"vite-plugin-removelog": "^0.2.2",
|
||||||
|
"vite-plugin-use-modules": "^1.4.8",
|
||||||
|
"vite-plugin-vue-devtools": "^7.4.4",
|
||||||
|
"vite-plugin-vue-meta-layouts": "^0.4.3",
|
||||||
|
"vitest": "^1.6.0",
|
||||||
|
"vue": "^3.5.3",
|
||||||
|
"vue-dark-switch": "^1.0.6",
|
||||||
|
"vue-echarts": "^6.7.3",
|
||||||
|
"vue-request": "2.0.4",
|
||||||
|
"vue-router": "^4.4.3",
|
||||||
|
"vue-toastification": "2.0.0-rc.5"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx,vue}": "eslint --cache --fix"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@latest",
|
||||||
|
"array-includes": "npm:@nolyfill/array-includes@latest",
|
||||||
|
"array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@latest",
|
||||||
|
"array.prototype.flat": "npm:@nolyfill/array.prototype.flat@latest",
|
||||||
|
"array.prototype.flatmap": "npm:@nolyfill/array.prototype.flatmap@latest",
|
||||||
|
"arraybuffer.prorotype.slice": "npm:@nolyfill/arraybuffer.prorotype.slice@latest",
|
||||||
|
"function.prototype.name": "npm:@nolyfill/function.prototype.name@latest",
|
||||||
|
"has": "npm:@nolyfill/has@latest",
|
||||||
|
"is-regex": "npm:@nolyfill/is-regex@latest",
|
||||||
|
"object-keys": "npm:@nolyfill/object-keys@latest",
|
||||||
|
"object.assign": "npm:@nolyfill/object.assign@latest",
|
||||||
|
"object.entries": "npm:@nolyfill/object.entries@latest",
|
||||||
|
"object.fromentries": "npm:@nolyfill/object.fromentries@latest",
|
||||||
|
"object.values": "npm:@nolyfill/object.values@latest",
|
||||||
|
"vue-demi": "npm:vue-demi@latest"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/dishait/tov-template"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
">= 0.25%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"Android >= 4.0",
|
||||||
|
"iOS >= 8"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"element-plus": "^2.10.3"
|
||||||
|
}
|
||||||
|
}
|
9077
pnpm-lock.yaml
generated
Normal file
9077
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
presets/autoprefixer.ts
Normal file
33
presets/autoprefixer.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import type { Preset } from 'unocss'
|
||||||
|
import browserslist from 'browserslist'
|
||||||
|
import { defaultBuildTargets } from './shared/detect'
|
||||||
|
import { browserslistToTargets, transformStyleAttribute } from 'lightningcss'
|
||||||
|
|
||||||
|
export default function autoprefixerPreset(
|
||||||
|
targets: string[] = defaultBuildTargets,
|
||||||
|
): Preset {
|
||||||
|
return {
|
||||||
|
name: 'unocss-preset-autoprefixer',
|
||||||
|
postprocess: (util) => {
|
||||||
|
const entries = util.entries
|
||||||
|
const { code } = transformStyleAttribute({
|
||||||
|
code: Buffer.from(
|
||||||
|
entries
|
||||||
|
.filter((item) => !item[0].startsWith('--un'))
|
||||||
|
.map((x) => x.join(':'))
|
||||||
|
.join(';'),
|
||||||
|
),
|
||||||
|
targets: browserslistToTargets(browserslist(targets)),
|
||||||
|
minify: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
util.entries = [
|
||||||
|
...entries.filter((item) => item[0].startsWith('--un')),
|
||||||
|
...(code
|
||||||
|
.toString()
|
||||||
|
.split(';')
|
||||||
|
.map((i) => i.split(':')) as [string, string | number][]),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
275
presets/index.ts
Normal file
275
presets/index.ts
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import Prism from 'markdown-it-prism'
|
||||||
|
import UnoCss from 'unocss/vite'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import {
|
||||||
|
AntDesignVueResolver,
|
||||||
|
ArcoResolver,
|
||||||
|
DevUiResolver,
|
||||||
|
ElementPlusResolver,
|
||||||
|
HeadlessUiResolver,
|
||||||
|
IduxResolver,
|
||||||
|
InklineResolver,
|
||||||
|
LayuiVueResolver,
|
||||||
|
NaiveUiResolver,
|
||||||
|
PrimeVueResolver,
|
||||||
|
QuasarResolver,
|
||||||
|
TDesignResolver,
|
||||||
|
VantResolver,
|
||||||
|
VarletUIResolver,
|
||||||
|
ViewUiResolver,
|
||||||
|
VueUseComponentsResolver,
|
||||||
|
Vuetify3Resolver,
|
||||||
|
} from 'unplugin-vue-components/resolvers'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import Markdown from 'unplugin-vue-markdown/vite'
|
||||||
|
import { VueRouterAutoImports } from 'unplugin-vue-router'
|
||||||
|
import Router from 'unplugin-vue-router/vite'
|
||||||
|
|
||||||
|
import { AutoGenerateImports, vue3Presets } from 'vite-auto-import-resolvers'
|
||||||
|
import Compression from 'vite-plugin-compression'
|
||||||
|
import EnvTypes from 'vite-plugin-env-types'
|
||||||
|
import { viteMockServe as Mock } from 'vite-plugin-mock'
|
||||||
|
import Removelog from 'vite-plugin-removelog'
|
||||||
|
import Modules from 'vite-plugin-use-modules'
|
||||||
|
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import Layouts from 'vite-plugin-vue-meta-layouts'
|
||||||
|
|
||||||
|
import I18N from '@intlify/unplugin-vue-i18n/vite'
|
||||||
|
import Legacy from 'vite-plugin-legacy-swc'
|
||||||
|
import Vue from '@vitejs/plugin-vue'
|
||||||
|
import Jsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
|
||||||
|
// 内置插件
|
||||||
|
import {
|
||||||
|
Alias,
|
||||||
|
Lightningcss,
|
||||||
|
Restart,
|
||||||
|
Warmup,
|
||||||
|
Layers,
|
||||||
|
Optimize,
|
||||||
|
} from './plugins'
|
||||||
|
import { defaultBuildTargets, detectResolvers, useEnv } from './shared/detect'
|
||||||
|
import { r } from './shared/path'
|
||||||
|
import type { PluginOption } from 'vite'
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const env = useEnv()
|
||||||
|
const safelist =
|
||||||
|
'prose px-2 sm:px-0 md:prose-lg lg:prose-lg dark:prose-invert text-left w-screen prose-slate prose-img:rounded-xl prose-headings:underline prose-a:text-blue-600'
|
||||||
|
|
||||||
|
const plugins: PluginOption[] = [
|
||||||
|
/**
|
||||||
|
* vite 配置层
|
||||||
|
* 通过 mode 区分 vite 配置文件 (experimental)
|
||||||
|
*/
|
||||||
|
Layers(),
|
||||||
|
/**
|
||||||
|
* 兼容不支持 esmModule 的浏览器
|
||||||
|
* https://www.npmjs.com/package/@vitejs/plugin-legacy
|
||||||
|
*/
|
||||||
|
Legacy({
|
||||||
|
targets: defaultBuildTargets,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 智能启动 lightningcss
|
||||||
|
*/
|
||||||
|
Lightningcss(),
|
||||||
|
/**
|
||||||
|
* 启动优化
|
||||||
|
*/
|
||||||
|
Optimize(),
|
||||||
|
/**
|
||||||
|
* 环境变量类型提示
|
||||||
|
* https://github.com/dishait/vite-plugin-env-types
|
||||||
|
*/
|
||||||
|
EnvTypes({
|
||||||
|
dts: r('presets/types/env.d.ts'),
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 内置的预热,可以加快冷启动
|
||||||
|
*/
|
||||||
|
Warmup(),
|
||||||
|
/**
|
||||||
|
* 文件路由
|
||||||
|
* https://github.com/posva/unplugin-vue-router
|
||||||
|
*/
|
||||||
|
Router({
|
||||||
|
routesFolder: r('src/pages'),
|
||||||
|
dts: r('presets/types/type-router.d.ts'),
|
||||||
|
extensions: ['.md', '.vue', '.tsx', '.jsx'],
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 自动安装 vue 插件
|
||||||
|
* https://github.com/dishait/vite-plugin-use-modules
|
||||||
|
*/
|
||||||
|
Modules({
|
||||||
|
auto: true,
|
||||||
|
// 内部使用虚拟模块,运行在前端,所以不需要 r 重写路径
|
||||||
|
target: 'src/plugins',
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* vue 官方插件,用来解析 sfc 单文件组件
|
||||||
|
* https://www.npmjs.com/package/@vitejs/plugin-vue
|
||||||
|
*/
|
||||||
|
Vue({
|
||||||
|
include: [/\.vue$/, /\.md$/],
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 布局系统
|
||||||
|
* https://github.com/dishait/vite-plugin-vue-meta-layouts
|
||||||
|
*/
|
||||||
|
Layouts({
|
||||||
|
skipTopLevelRouteLayout: true,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* mock 服务
|
||||||
|
* https://github.com/vbenjs/vite-plugin-mock
|
||||||
|
*/
|
||||||
|
Mock({
|
||||||
|
prodEnabled: env.VITE_APP_MOCK_IN_PRODUCTION,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 组件自动按需引入
|
||||||
|
* https://github.com/antfu/unplugin-vue-components
|
||||||
|
*/
|
||||||
|
Components({
|
||||||
|
directoryAsNamespace: true,
|
||||||
|
include: [/\.vue$/, /\.vue\?vue/, /\.[tj]sx$/, /\.md$/],
|
||||||
|
extensions: ['md', 'vue', 'tsx', 'jsx'],
|
||||||
|
dts: r('presets/types/components.d.ts'),
|
||||||
|
types: [
|
||||||
|
{
|
||||||
|
from: 'vue-router',
|
||||||
|
names: ['RouterLink', 'RouterView'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resolvers: detectResolvers({
|
||||||
|
onlyExist: [
|
||||||
|
[VantResolver(), 'vant'],
|
||||||
|
[QuasarResolver(), 'quasar'],
|
||||||
|
[DevUiResolver(), 'vue-devui'],
|
||||||
|
[NaiveUiResolver(), 'naive-ui'],
|
||||||
|
[Vuetify3Resolver(), 'vuetify'],
|
||||||
|
[PrimeVueResolver(), 'primevue'],
|
||||||
|
[ViewUiResolver(), 'view-design'],
|
||||||
|
[LayuiVueResolver(), 'layui-vue'],
|
||||||
|
[VarletUIResolver(), '@varlet/ui'],
|
||||||
|
[IduxResolver(), '@idux/components'],
|
||||||
|
[InklineResolver(), '@inkline/inkline'],
|
||||||
|
[ElementPlusResolver(), 'element-plus'],
|
||||||
|
[HeadlessUiResolver(), '@headlessui/vue'],
|
||||||
|
[ArcoResolver(), '@arco-design/web-vue'],
|
||||||
|
[AntDesignVueResolver({ importStyle: false }), 'ant-design-vue'],
|
||||||
|
[VueUseComponentsResolver(), '@vueuse/components'],
|
||||||
|
[TDesignResolver({ library: 'vue-next' }), 'tdesign-vue-next'],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* i18n 国际化支持
|
||||||
|
* https://www.npmjs.com/package/@intlify/unplugin-vue-i18n
|
||||||
|
*/
|
||||||
|
I18N({
|
||||||
|
runtimeOnly: false,
|
||||||
|
compositionOnly: true,
|
||||||
|
include: ['locales/**'],
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* jsx 和 tsx 支持
|
||||||
|
* https://www.npmjs.com/package/@vitejs/plugin-vue-jsx
|
||||||
|
*/
|
||||||
|
Jsx(),
|
||||||
|
/**
|
||||||
|
* 生产环境资源压缩
|
||||||
|
* https://github.com/vbenjs/vite-plugin-compression
|
||||||
|
*/
|
||||||
|
Compression({
|
||||||
|
// @ts-ignore
|
||||||
|
algorithm: env.VITE_APP_COMPRESSINON_ALGORITHM,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 别名插件 (内置)
|
||||||
|
* 支持 `~` 和 `@` 别名到 `src`
|
||||||
|
*/
|
||||||
|
Alias(),
|
||||||
|
/**
|
||||||
|
* 强制重启 (内置)
|
||||||
|
* 如果 package.json 或 pnpm-lock.yaml 更新的话,强制重启
|
||||||
|
*/
|
||||||
|
Restart(),
|
||||||
|
/**
|
||||||
|
* css 原子引擎
|
||||||
|
* https://github.com/unocss/unocss
|
||||||
|
*/
|
||||||
|
UnoCss({
|
||||||
|
safelist: env.VITE_APP_MARKDOWN ? safelist.split(' ') : undefined,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
/**
|
||||||
|
* 开发面板
|
||||||
|
* https://github.com/webfansplz/vite-plugin-vue-devtools
|
||||||
|
*/
|
||||||
|
if (env.VITE_APP_DEV_TOOLS) {
|
||||||
|
plugins.push(VueDevTools())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生产环境下移除 console.log, console.warn, console.error
|
||||||
|
* https://github.com/dishait/vite-plugin-removelog
|
||||||
|
*/
|
||||||
|
if (process.env.NODE_ENV !== 'debug') {
|
||||||
|
plugins.push(Removelog())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* markdown 渲染插件
|
||||||
|
* https://github.com/mdit-vue/unplugin-vue-markdown
|
||||||
|
*/
|
||||||
|
if (env.VITE_APP_MARKDOWN) {
|
||||||
|
plugins.push(
|
||||||
|
Markdown({
|
||||||
|
wrapperClasses: safelist,
|
||||||
|
markdownItSetup(md) {
|
||||||
|
md.use(Prism)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* api 自动按需引入
|
||||||
|
* https://github.com/antfu/unplugin-auto-import
|
||||||
|
*/
|
||||||
|
if (env.VITE_APP_API_AUTO_IMPORT) {
|
||||||
|
const dirs = env.VITE_APP_DIR_API_AUTO_IMPORT
|
||||||
|
? ['src/stores/**', 'src/composables/**', 'src/api/**']
|
||||||
|
: []
|
||||||
|
plugins.push(
|
||||||
|
AutoImport({
|
||||||
|
dirs,
|
||||||
|
vueTemplate: true,
|
||||||
|
dts: r('presets/types/auto-imports.d.ts'),
|
||||||
|
imports: [
|
||||||
|
...AutoGenerateImports({
|
||||||
|
include: [...vue3Presets],
|
||||||
|
exclude: ['vue-router'],
|
||||||
|
}),
|
||||||
|
VueRouterAutoImports,
|
||||||
|
],
|
||||||
|
resolvers: detectResolvers({
|
||||||
|
onlyExist: [
|
||||||
|
[ElementPlusResolver(), 'element-plus'],
|
||||||
|
[TDesignResolver({ library: 'vue-next' }), 'tdesign-vue-next'],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
eslintrc: {
|
||||||
|
enabled: true,
|
||||||
|
globalsPropValue: true,
|
||||||
|
filepath: r('presets/eslint/.eslintrc-auto-import.json'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
}
|
27
presets/plugins/alias.ts
Normal file
27
presets/plugins/alias.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import { r } from '../shared/path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 别名插件
|
||||||
|
* @description 支持 `~` 和 `@` 别名到 `src`
|
||||||
|
*/
|
||||||
|
export function Alias(): Plugin {
|
||||||
|
const src = r('./src')
|
||||||
|
return {
|
||||||
|
name: 'vite-alias',
|
||||||
|
enforce: 'pre',
|
||||||
|
config(config) {
|
||||||
|
config.resolve ??= {}
|
||||||
|
config.resolve.alias = [
|
||||||
|
{
|
||||||
|
find: /^~/,
|
||||||
|
replacement: src,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@\//,
|
||||||
|
replacement: src + '/',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
6
presets/plugins/index.ts
Normal file
6
presets/plugins/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { Alias } from './alias'
|
||||||
|
export { Layers } from './layers'
|
||||||
|
export { Warmup } from './warmup'
|
||||||
|
export { Restart } from './restart'
|
||||||
|
export { Optimize } from './optimize'
|
||||||
|
export { Lightningcss } from './lightningcss'
|
41
presets/plugins/layers.ts
Normal file
41
presets/plugins/layers.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { gray } from 'kolorist'
|
||||||
|
import { basename } from 'path'
|
||||||
|
import { r } from '../shared/path'
|
||||||
|
import { Restart } from './restart'
|
||||||
|
import { createConsola } from 'consola'
|
||||||
|
import type { Plugin, UserConfig } from 'vite'
|
||||||
|
import { Layers as loadLayer, detectMode } from 'vite-layers'
|
||||||
|
|
||||||
|
const logger = createConsola().withTag('layers')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* vite 配置层
|
||||||
|
* @description 通过 mode 区分 vite 配置文件 (experimental)
|
||||||
|
*/
|
||||||
|
export function Layers(): Plugin {
|
||||||
|
const mode = detectMode()
|
||||||
|
const modeFiles = [mode.slice(0, 3), mode].map((mode) =>
|
||||||
|
r(`vite.config.${mode}.ts`),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...Restart(modeFiles.map((modeFile) => basename(modeFile))),
|
||||||
|
name: 'vite-plugin-layers',
|
||||||
|
enforce: 'post',
|
||||||
|
async config(config) {
|
||||||
|
const modeFile = modeFiles.find((modeFile) => existsSync(modeFile))
|
||||||
|
if (modeFile) {
|
||||||
|
logger
|
||||||
|
.withTag(mode)
|
||||||
|
.success(
|
||||||
|
`vite.config.ts → ${basename(modeFile)} ${gray(`(experimental)`)}`,
|
||||||
|
)
|
||||||
|
return loadLayer({
|
||||||
|
logger: false,
|
||||||
|
extends: [config, modeFile],
|
||||||
|
}) as UserConfig
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
62
presets/plugins/lightningcss.ts
Normal file
62
presets/plugins/lightningcss.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { gray } from 'kolorist'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import { createConsola } from 'consola'
|
||||||
|
import { isPackageExists } from 'local-pkg'
|
||||||
|
import { browserslistToTargets } from 'lightningcss'
|
||||||
|
import { defaultBuildTargets } from '../shared/detect'
|
||||||
|
|
||||||
|
const name = 'vite-plugin-fire-lightningcss'
|
||||||
|
|
||||||
|
const logger = createConsola().withTag('css')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能开启 lightningcss (如果不使用预处理器,或者 postcss)
|
||||||
|
*/
|
||||||
|
export function Lightningcss(): Plugin {
|
||||||
|
const packages = ['less', 'sass', 'stylus']
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
config(config) {
|
||||||
|
config.css ??= {}
|
||||||
|
config.build ??= {}
|
||||||
|
const hasPreprocessor = packages.some((p) => isPackageExists(p))
|
||||||
|
|
||||||
|
const { postcss, modules, transformer } = config.css
|
||||||
|
const conflictConfiguration = [postcss, modules, transformer].some(
|
||||||
|
(c) => !isUndefined(c),
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasPostcssConfigFile = [
|
||||||
|
'postcss.config.js',
|
||||||
|
'postcss.config.cts',
|
||||||
|
'postcss.config.ts',
|
||||||
|
].some((c) => existsSync(c))
|
||||||
|
|
||||||
|
// 如果有预处理器,冲突配置或者 postcss 配置文件则禁用
|
||||||
|
const disabled =
|
||||||
|
hasPreprocessor || conflictConfiguration || hasPostcssConfigFile
|
||||||
|
if (!disabled) {
|
||||||
|
const transformer = 'lightningcss'
|
||||||
|
config.css.transformer = transformer
|
||||||
|
let tip = `${transformer} ${gray(transformer)}`
|
||||||
|
|
||||||
|
if (isUndefined(config.build.cssMinify)) {
|
||||||
|
config.build.cssMinify = 'lightningcss'
|
||||||
|
tip = `${transformer} ${gray('(transformer + cssMinify)')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUndefined(config.css.lightningcss?.targets)) {
|
||||||
|
config.css.lightningcss ??= {}
|
||||||
|
config.css.lightningcss.targets =
|
||||||
|
browserslistToTargets(defaultBuildTargets)
|
||||||
|
}
|
||||||
|
logger.success(tip)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUndefined(v: unknown): v is undefined {
|
||||||
|
return typeof v === 'undefined'
|
||||||
|
}
|
||||||
|
}
|
19
presets/plugins/optimize.ts
Normal file
19
presets/plugins/optimize.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { createConsola } from 'consola'
|
||||||
|
import { gray } from 'kolorist'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
const logger = createConsola().withTag('optimize')
|
||||||
|
export function Optimize(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite-optimize',
|
||||||
|
config(config) {
|
||||||
|
config.css ??= {}
|
||||||
|
config.optimizeDeps ??= {}
|
||||||
|
config.css.preprocessorMaxWorkers = true
|
||||||
|
config.optimizeDeps.holdUntilCrawlEnd = false
|
||||||
|
logger.success(
|
||||||
|
`optimize ${gray('(preprocessorMaxWorkers + closeHoldUntilCrawlEnd)')}`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
30
presets/plugins/restart.ts
Normal file
30
presets/plugins/restart.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import { utimes } from 'fs/promises'
|
||||||
|
import { r } from '../shared/path'
|
||||||
|
import { debounce } from 'perfect-debounce'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
import { slash } from 'vite-layers'
|
||||||
|
|
||||||
|
const defaultPaths = ['package.json', 'pnpm-lock.yaml']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制重启
|
||||||
|
* @description 如果监听更新的话,强制重启项目
|
||||||
|
* @param paths 监听重启路径,默认为 ['package.json', 'pnpm-lock.yaml']
|
||||||
|
*/
|
||||||
|
export function Restart(paths = defaultPaths): Plugin {
|
||||||
|
paths = paths.map((path) => slash(resolve(path)))
|
||||||
|
const restart = debounce(async function touch() {
|
||||||
|
const time = new Date()
|
||||||
|
await utimes(r('vite.config.ts'), time, time)
|
||||||
|
}, 1000)
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-force-restart',
|
||||||
|
apply: 'serve',
|
||||||
|
async watchChange(id) {
|
||||||
|
if (paths.includes(id)) {
|
||||||
|
await restart()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
21
presets/plugins/warmup.ts
Normal file
21
presets/plugins/warmup.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预热
|
||||||
|
* @description 内置的预热,可以加快冷启动
|
||||||
|
*/
|
||||||
|
export function Warmup(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-warmup',
|
||||||
|
apply: 'serve',
|
||||||
|
config(config) {
|
||||||
|
const src = './src/**/*'
|
||||||
|
config.server ??= {}
|
||||||
|
config.server.warmup ??= {}
|
||||||
|
config.server.warmup.clientFiles ??= []
|
||||||
|
if (!config.server.warmup.clientFiles.includes(src)) {
|
||||||
|
config.server.warmup.clientFiles.push(src)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
76
presets/shared/detect.ts
Normal file
76
presets/shared/detect.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 侦察模块
|
||||||
|
* @description 自动检测环境并智能生成
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { r } from './path'
|
||||||
|
import { loadEnv } from 'vite'
|
||||||
|
import browserslist from 'browserslist'
|
||||||
|
import { detectMode } from 'vite-layers'
|
||||||
|
import { isPackageExists } from 'local-pkg'
|
||||||
|
import type { ComponentResolver } from 'unplugin-vue-components'
|
||||||
|
|
||||||
|
const { loadConfig: browserslistLoadConfig } = browserslist
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认打包目标 (浏览器兼容程度)
|
||||||
|
*/
|
||||||
|
export const defaultBuildTargets = browserslistLoadConfig({
|
||||||
|
path: r('./'),
|
||||||
|
}) || ['last 2 versions and not dead, > 0.3%, Firefox ESR']
|
||||||
|
|
||||||
|
type Arrayable<T> = T | Array<T>
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
onlyExist?: [Arrayable<ComponentResolver>, string][]
|
||||||
|
include?: ComponentResolver[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发现 resolvers
|
||||||
|
*/
|
||||||
|
export function detectResolvers(options: Options = {}) {
|
||||||
|
const { onlyExist = [], include = [] } = options
|
||||||
|
|
||||||
|
const existedResolvers = []
|
||||||
|
for (let i = 0; i < onlyExist.length; i++) {
|
||||||
|
const [resolver, packageName] = onlyExist[i]
|
||||||
|
if (
|
||||||
|
isPackageExists(packageName, {
|
||||||
|
paths: [r('./')],
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
existedResolvers.push(resolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existedResolvers.push(...include)
|
||||||
|
|
||||||
|
return existedResolvers
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取环境变量
|
||||||
|
export function useEnv() {
|
||||||
|
function stringToBoolean(v: string) {
|
||||||
|
return Boolean(v === 'true' || false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
VITE_APP_TITLE,
|
||||||
|
VITE_APP_DEV_TOOLS,
|
||||||
|
VITE_APP_MARKDOWN,
|
||||||
|
VITE_APP_API_AUTO_IMPORT,
|
||||||
|
VITE_APP_MOCK_IN_PRODUCTION,
|
||||||
|
VITE_APP_DIR_API_AUTO_IMPORT,
|
||||||
|
VITE_APP_COMPRESSINON_ALGORITHM,
|
||||||
|
} = loadEnv(detectMode(), '.')
|
||||||
|
|
||||||
|
return {
|
||||||
|
VITE_APP_TITLE,
|
||||||
|
VITE_APP_COMPRESSINON_ALGORITHM,
|
||||||
|
VITE_APP_DEV_TOOLS: stringToBoolean(VITE_APP_DEV_TOOLS),
|
||||||
|
VITE_APP_MARKDOWN: stringToBoolean(VITE_APP_MARKDOWN),
|
||||||
|
VITE_APP_API_AUTO_IMPORT: stringToBoolean(VITE_APP_API_AUTO_IMPORT),
|
||||||
|
VITE_APP_MOCK_IN_PRODUCTION: stringToBoolean(VITE_APP_MOCK_IN_PRODUCTION),
|
||||||
|
VITE_APP_DIR_API_AUTO_IMPORT: stringToBoolean(VITE_APP_DIR_API_AUTO_IMPORT),
|
||||||
|
}
|
||||||
|
}
|
89
presets/shared/mock.ts
Normal file
89
presets/shared/mock.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* issue: https://github.com/vbenjs/vite-plugin-mock/issues/47
|
||||||
|
* fix: https://github.com/vbenjs/vite-plugin-mock/issues/47#issuecomment-982724613
|
||||||
|
*/
|
||||||
|
import Mock from 'mockjs'
|
||||||
|
|
||||||
|
export function createFetchSever(mockList: any[]) {
|
||||||
|
if (!window['originFetch']) {
|
||||||
|
window['originFetch'] = window.fetch
|
||||||
|
window.fetch = function (fetchUrl: string, init: any) {
|
||||||
|
const currentMock = mockList.find((mi) => fetchUrl.includes(mi.url))
|
||||||
|
if (currentMock) {
|
||||||
|
const result = createFetchReturn(currentMock, init)
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
return window['originFetch'](fetchUrl, init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __param2Obj__(url: string) {
|
||||||
|
const search = url.split('?')[1]
|
||||||
|
if (!search) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return JSON.parse(
|
||||||
|
'{"' +
|
||||||
|
decodeURIComponent(search)
|
||||||
|
.replace(/"/g, '\\"')
|
||||||
|
.replace(/&/g, '","')
|
||||||
|
.replace(/=/g, '":"')
|
||||||
|
.replace(/\+/g, ' ') +
|
||||||
|
'"}',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function __Fetch2ExpressReqWrapper__(handle: () => any) {
|
||||||
|
return function (options: any) {
|
||||||
|
let result = null
|
||||||
|
if (typeof handle === 'function') {
|
||||||
|
const { body, method, url, headers } = options
|
||||||
|
|
||||||
|
let b = body
|
||||||
|
b = JSON.parse(body)
|
||||||
|
result = handle({
|
||||||
|
method,
|
||||||
|
body: b,
|
||||||
|
query: __param2Obj__(url),
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
result = handle
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mock.mock(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (delay = 0) => {
|
||||||
|
if (delay) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, delay)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFetchReturn(mock: any, init) {
|
||||||
|
const { timeout, response } = mock
|
||||||
|
const mockFn = __Fetch2ExpressReqWrapper__(response)
|
||||||
|
const data = mockFn(init)
|
||||||
|
await sleep(timeout)
|
||||||
|
const result = {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
clone() {
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
text() {
|
||||||
|
return Promise.resolve(data)
|
||||||
|
},
|
||||||
|
json() {
|
||||||
|
return Promise.resolve(data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
15
presets/shared/path.ts
Normal file
15
presets/shared/path.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { dirname, resolve } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const _dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
const root = resolve(_dirname, '../../')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径重写
|
||||||
|
* @param path 相对路径
|
||||||
|
* @returns 基于根目录的相对路径
|
||||||
|
*/
|
||||||
|
export function r(path: string) {
|
||||||
|
return resolve(root, path).replaceAll('\\', '/')
|
||||||
|
}
|
18
presets/types/vite.d.ts
vendored
Normal file
18
presets/types/vite.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-pages/client" />
|
||||||
|
/// <reference types="unplugin-vue-router/client" />
|
||||||
|
/// <reference types="vite-plugin-use-modules/client" />
|
||||||
|
/// <reference types="vite-plugin-vue-meta-layouts/client" />
|
||||||
|
/// <reference types="@intlify/vite-plugin-vue-i18n/client" />
|
||||||
|
|
||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.md" {
|
||||||
|
import { ComponentOptions } from "vue";
|
||||||
|
const Component: ComponentOptions;
|
||||||
|
export default Component;
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
1
public/notFound/33.svg
Normal file
1
public/notFound/33.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.4 KiB |
BIN
public/uav/bg.png
Normal file
BIN
public/uav/bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 887 KiB |
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="iconify iconify--logos" width="31.88" height="32" viewBox="0 0 256 257"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
public/vue.svg
Normal file
1
public/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="iconify iconify--logos" width="37.07" height="36" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36z"/><path fill="#41B883" d="m0 0 128 220.8L256 0h-51.2L128 132.48 50.56 0H0z"/><path fill="#35495E" d="M50.56 0 128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56z"/></svg>
|
After Width: | Height: | Size: 388 B |
3
renovate.json
Normal file
3
renovate.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["github>unjs/renovate-config"]
|
||||||
|
}
|
76
scripts/create.cjs
Normal file
76
scripts/create.cjs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const { existsSync } = require('fs')
|
||||||
|
|
||||||
|
const { showDir, showExt, moduleTypes } = require('./shared/base.cjs')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动创建
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function create(plop) {
|
||||||
|
let exist = null
|
||||||
|
let modulePath = null
|
||||||
|
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动创建',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
default: 'component',
|
||||||
|
message: '您希望生成哪种类型的模块?',
|
||||||
|
choices: moduleTypes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isMarkdown',
|
||||||
|
type: 'confirm',
|
||||||
|
message: '是否 markdown 类型?',
|
||||||
|
default: false,
|
||||||
|
// 如果是 page 类型需要询问是否为 markdown 类型
|
||||||
|
when({ type }) {
|
||||||
|
return type === 'page'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'input',
|
||||||
|
message({ type }) {
|
||||||
|
return `请输入 ${type} 的命名`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldReset',
|
||||||
|
type: 'confirm',
|
||||||
|
default: false,
|
||||||
|
message({ type }) {
|
||||||
|
return `目标 ${type} 已存在,是否重置?`
|
||||||
|
},
|
||||||
|
// 确认模块是否已存在,是则询问是否重置
|
||||||
|
when({ type, name, isMarkdown }) {
|
||||||
|
const dir = showDir(type)
|
||||||
|
const ext = showExt(type, isMarkdown)
|
||||||
|
modulePath = `src/${dir}/${name}.${ext}`
|
||||||
|
exist = existsSync(modulePath)
|
||||||
|
if (exist) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
const { type, shouldReset } = answer
|
||||||
|
if (exist && !shouldReset) {
|
||||||
|
throw new Error(`${type} 创建失败`)
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'add',
|
||||||
|
force: true,
|
||||||
|
path: `../${modulePath}`,
|
||||||
|
templateFile: `./template/${type}.hbs`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = create
|
36
scripts/deps-fresh.cjs
Normal file
36
scripts/deps-fresh.cjs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动更新依赖
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function depsFresh(plop) {
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动更新依赖',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
default: 'patch',
|
||||||
|
message: '你希望发布一个什么版本?',
|
||||||
|
choices: ['patch', 'minor', 'major'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldWrite',
|
||||||
|
type: 'confirm',
|
||||||
|
default: false,
|
||||||
|
message: '是否直接更新?',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
const { type, shouldWrite } = answer
|
||||||
|
|
||||||
|
execSync(`npx taze ${type} ${shouldWrite ? '-w' : ''}`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = depsFresh
|
65
scripts/release.cjs
Normal file
65
scripts/release.cjs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const { createConsola } = require('consola')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
const { repository } = require('../package.json')
|
||||||
|
const { gray } = require('kolorist')
|
||||||
|
const { simpleGit } = require('simple-git')
|
||||||
|
const logger = createConsola().withTag('release')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动发版
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
async function release(plop) {
|
||||||
|
const git = simpleGit()
|
||||||
|
|
||||||
|
const remotes = await git.getRemotes(true)
|
||||||
|
|
||||||
|
const urls = remotes.map((r) => {
|
||||||
|
return r.refs.push
|
||||||
|
.replace('git@github.com:', 'https://github.com/')
|
||||||
|
.replace('.git', '')
|
||||||
|
})
|
||||||
|
let allowRelease = false
|
||||||
|
if (!urls.includes(repository.url)) {
|
||||||
|
allowRelease = await logger.prompt(`是否发布到 ${gray(repository.url)}`, {
|
||||||
|
type: 'confirm',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
allowRelease = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowRelease) {
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动发版',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
default: 'patch',
|
||||||
|
message: '你希望发布一个什么版本?',
|
||||||
|
choices: [
|
||||||
|
'patch',
|
||||||
|
'minor',
|
||||||
|
'major',
|
||||||
|
'prepatch',
|
||||||
|
'premajor',
|
||||||
|
'preminor',
|
||||||
|
'prerelease',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
const { type } = answer
|
||||||
|
execSync(
|
||||||
|
`npx changelogen --${type} --release && git push --follow-tags`,
|
||||||
|
{
|
||||||
|
stdio: 'inherit',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = release
|
75
scripts/remove.cjs
Normal file
75
scripts/remove.cjs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
const { unlinkSync } = require('fs')
|
||||||
|
const { readdir } = require('fs/promises')
|
||||||
|
const { basename } = require('path')
|
||||||
|
const { showDir, moduleTypes } = require('./shared/base.cjs')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动删除
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function remove(plop) {
|
||||||
|
plop.setActionType('remove', (answers) => {
|
||||||
|
const { name, type, shouldRemove } = answers
|
||||||
|
const dir = showDir(type)
|
||||||
|
const target = `./src/${dir}/${name}`
|
||||||
|
if (shouldRemove) {
|
||||||
|
return unlinkSync(target)
|
||||||
|
}
|
||||||
|
throw new Error(`删除 ${target} 失败`)
|
||||||
|
})
|
||||||
|
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动删除',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
message: '请选择您要删除的类型',
|
||||||
|
async choices() {
|
||||||
|
const entrys = await readdir('./src', {
|
||||||
|
recursive: false,
|
||||||
|
withFileTypes: true,
|
||||||
|
})
|
||||||
|
const dirs = entrys.filter((e) => e.isDirectory())
|
||||||
|
const types = moduleTypes.filter((type) => {
|
||||||
|
const dir = showDir(type)
|
||||||
|
return dirs.includes(`./src/${dir}`)
|
||||||
|
})
|
||||||
|
return types
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'list',
|
||||||
|
message({ type }) {
|
||||||
|
return `请选择您要删除的 ${type} 模块`
|
||||||
|
},
|
||||||
|
async choices({ type }) {
|
||||||
|
const dir = showDir(type)
|
||||||
|
const entrys = await readdir(`src/${dir}`, {
|
||||||
|
recursive: false,
|
||||||
|
withFileTypes: true,
|
||||||
|
})
|
||||||
|
let modules = entrys.filter((e) => e.isFile())
|
||||||
|
modules = modules.map((module) => {
|
||||||
|
return basename(module)
|
||||||
|
})
|
||||||
|
return modules
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldRemove',
|
||||||
|
type: 'confirm',
|
||||||
|
default: false,
|
||||||
|
message: '再次确认是否删除',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'remove',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = remove
|
113
scripts/safe-init.cjs
Normal file
113
scripts/safe-init.cjs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
const { resolve } = require('path')
|
||||||
|
const { gray, green } = require('kolorist')
|
||||||
|
const { createConsola } = require('consola')
|
||||||
|
const { existsSync, lstatSync } = require('fs')
|
||||||
|
const { removeSync, emptyDirSync } = require('fs-extra')
|
||||||
|
|
||||||
|
function slash(path) {
|
||||||
|
return path.replace(/\\/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function r(dir) {
|
||||||
|
return slash(resolve(__dirname, '../', dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
const entrys = [
|
||||||
|
'src/components',
|
||||||
|
'src/api',
|
||||||
|
'mock',
|
||||||
|
'layouts/default.vue',
|
||||||
|
'src/pages/index.vue',
|
||||||
|
'src/pages/about.md',
|
||||||
|
'src/pages/echarts.vue',
|
||||||
|
'src/stores',
|
||||||
|
'locales/简体中文',
|
||||||
|
'locales/English',
|
||||||
|
]
|
||||||
|
|
||||||
|
const resolvedEntrys = entrys.map((entry) => r(entry))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全初始化
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function safeInit(plop) {
|
||||||
|
const logger = createConsola().withTag('safe:init')
|
||||||
|
|
||||||
|
logger.warn('实验性功能')
|
||||||
|
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '安全初始化',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'yes',
|
||||||
|
type: 'confirm',
|
||||||
|
message: '是否安全的初始化?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cleanStyles',
|
||||||
|
type: 'confirm',
|
||||||
|
message: '是否清理 styles?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
if (!answer.yes) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (answer.cleanStyles) {
|
||||||
|
resolvedEntrys.push(r('src/styles'))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
// 这里不用异步是因为 plop action 只支持同步
|
||||||
|
resolvedEntrys.forEach((e) => {
|
||||||
|
if (!existsSync(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const entry = lstatSync(e)
|
||||||
|
if (entry.isFile()) {
|
||||||
|
removeSync(e)
|
||||||
|
logClean(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
emptyDirSync(e)
|
||||||
|
logClean(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'add',
|
||||||
|
force: true,
|
||||||
|
path: '../src/pages/index.vue',
|
||||||
|
templateFile: './template/page.hbs',
|
||||||
|
data: {
|
||||||
|
name: 'index',
|
||||||
|
isMarkdown: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'add',
|
||||||
|
force: true,
|
||||||
|
path: '../src/layouts/default.vue',
|
||||||
|
templateFile: './template/layout.hbs',
|
||||||
|
data: {
|
||||||
|
name: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function logClean(path) {
|
||||||
|
console.log(`${green('√ clean')} ${gray(path)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = safeInit
|
41
scripts/shared/base.cjs
Normal file
41
scripts/shared/base.cjs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 获取扩展名
|
||||||
|
* @param {string} type 模块类型
|
||||||
|
* @param {boolean} isMarkdown 是否是 markdown,默认为 false
|
||||||
|
* @returns {string} 扩展名
|
||||||
|
*/
|
||||||
|
const showExt = (type, isMarkdown = false) => {
|
||||||
|
const isTs = type === 'api' || type === 'store' || type === 'module'
|
||||||
|
const ext = isMarkdown ? 'md' : isTs ? 'ts' : 'vue'
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块类型
|
||||||
|
*/
|
||||||
|
const moduleTypes = [
|
||||||
|
'api',
|
||||||
|
'page',
|
||||||
|
'store',
|
||||||
|
'layout',
|
||||||
|
'module',
|
||||||
|
'component',
|
||||||
|
'composable',
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取目录
|
||||||
|
* @param {string} type 类型
|
||||||
|
*/
|
||||||
|
const showDir = (type) => {
|
||||||
|
if (type === 'api') {
|
||||||
|
return 'api'
|
||||||
|
}
|
||||||
|
return `${type}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
showExt,
|
||||||
|
showDir,
|
||||||
|
moduleTypes,
|
||||||
|
}
|
2
scripts/template/api.hbs
Normal file
2
scripts/template/api.hbs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
11
scripts/template/component.hbs
Normal file
11
scripts/template/component.hbs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{name}}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
5
scripts/template/composable.hbs
Normal file
5
scripts/template/composable.hbs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
export default {{name}} = () => {
|
||||||
|
|
||||||
|
}
|
4
scripts/template/layout.hbs
Normal file
4
scripts/template/layout.hbs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
{{pascalCase name}} layout
|
||||||
|
<router-view />
|
||||||
|
</template>
|
6
scripts/template/module.hbs
Normal file
6
scripts/template/module.hbs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { App } from "vue"
|
||||||
|
|
||||||
|
|
||||||
|
export default (app: App) => {
|
||||||
|
|
||||||
|
}
|
17
scripts/template/page.hbs
Normal file
17
scripts/template/page.hbs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{{#if isMarkdown}}
|
||||||
|
## {{pascalCase name}} Page
|
||||||
|
|
||||||
|
> The page is markdown file
|
||||||
|
{{else}}
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{pascalCase name}} page
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{{/if}}
|
9
scripts/template/store.hbs
Normal file
9
scripts/template/store.hbs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export default defineStore('{{name}}', {
|
||||||
|
state() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
actions: {}
|
||||||
|
})
|
13
src/App.vue
Normal file
13
src/App.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* a {
|
||||||
|
color: rgba(37, 99, 235);
|
||||||
|
} */
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
6
src/api/mock.ts
Normal file
6
src/api/mock.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { useRequest } from 'vue-request'
|
||||||
|
|
||||||
|
export const testRequest = () => {
|
||||||
|
const { data, loading, error } = useRequest(() => http.post('/mock/post'))
|
||||||
|
return { data, loading, error }
|
||||||
|
}
|
27
src/composables/env.ts
Normal file
27
src/composables/env.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 是否在开发环境
|
||||||
|
* @description 在 `if (IN_DEV)` 块中的代码在 `vite build` 时会自动编译掉,不会带到生产环境
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* if (IN_DEV) {
|
||||||
|
* console.log("开发环境")
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const IN_DEV = import.meta.env.DEV
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否在生产环境
|
||||||
|
* @example
|
||||||
|
* if (IN_PROD) {
|
||||||
|
* console.log("生产环境")
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const IN_PROD = import.meta.env.PROD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不带尾部 / 的 BASE_URL
|
||||||
|
*/
|
||||||
|
export const BASE_URL_WITHOUT_TAIL = import.meta.env.BASE_URL.endsWith('/')
|
||||||
|
? import.meta.env.BASE_URL.slice(0, -1)
|
||||||
|
: import.meta.env.BASE_URL
|
60
src/composables/http.ts
Normal file
60
src/composables/http.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const http = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加请求拦截器
|
||||||
|
http.interceptors.request.use(
|
||||||
|
function (config) {
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
toast.warning(error.message ?? '未知请求错误')
|
||||||
|
// 对请求错误做些什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 添加响应拦截器
|
||||||
|
http.interceptors.response.use(
|
||||||
|
function (response) {
|
||||||
|
// 2xx 范围内的状态码都会触发该函数。
|
||||||
|
// 对响应数据进行格式化
|
||||||
|
if (response.data) {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
const status = error.response?.status
|
||||||
|
let { msg, message } = error.response?.data ?? {}
|
||||||
|
|
||||||
|
if (!msg && message) {
|
||||||
|
msg = message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!msg) {
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
msg = '参数错误'
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
msg = '服务端错误'
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
msg = '路由未找到'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
msg = error.message ?? '未知响应错误'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.warning(msg)
|
||||||
|
// 超出 2xx 范围的状态码都会触发该函数。
|
||||||
|
// 对响应错误做点什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
7
src/composables/path.ts
Normal file
7
src/composables/path.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* base 安全的路径解析
|
||||||
|
* @param path 路径
|
||||||
|
*/
|
||||||
|
export function safeResolve(path: string) {
|
||||||
|
return BASE_URL_WITHOUT_TAIL + path
|
||||||
|
}
|
4
src/composables/toast.ts
Normal file
4
src/composables/toast.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import 'vue-toastification/dist/index.css'
|
||||||
|
import { createToastInterface } from 'vue-toastification'
|
||||||
|
|
||||||
|
export default createToastInterface()
|
12
src/composables/useLanguage.ts
Normal file
12
src/composables/useLanguage.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default () => {
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
const toggleLocale = () => {
|
||||||
|
locale.value = locale.value === 'zh-CN' ? 'en' : 'zh-CN'
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = computed(() =>
|
||||||
|
locale.value === 'zh-CN' ? '中文' : 'English',
|
||||||
|
)
|
||||||
|
|
||||||
|
return { t, language, toggleLocale }
|
||||||
|
}
|
14
src/composables/useTyped.ts
Normal file
14
src/composables/useTyped.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { init } from 'ityped'
|
||||||
|
|
||||||
|
export default (strings: string[]) => {
|
||||||
|
const typedRef = ref<Element>()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init(typedRef.value!, {
|
||||||
|
strings,
|
||||||
|
showCursor: false,
|
||||||
|
disableBackTyping: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return typedRef
|
||||||
|
}
|
25
src/composables/useVisits.ts
Normal file
25
src/composables/useVisits.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useRequest } from 'vue-request'
|
||||||
|
|
||||||
|
export function useVisits() {
|
||||||
|
// 开发环境下
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const visits = useStorage('visits-kv', 0)
|
||||||
|
if (typeof visits.value === 'number') {
|
||||||
|
visits.value++
|
||||||
|
}
|
||||||
|
return visits
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: visits } = useRequest(async function () {
|
||||||
|
try {
|
||||||
|
const n = await http.get('https://visits-kv.deno.dev/tov-template', {
|
||||||
|
baseURL: '',
|
||||||
|
})
|
||||||
|
return Number(n) ?? 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return visits ?? 0
|
||||||
|
}
|
118
src/layouts/default.vue
Normal file
118
src/layouts/default.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<el-row :gutter="0">
|
||||||
|
<el-col :span="3" class="menu-left">
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
class="el-menu-vertical"
|
||||||
|
background-color="#304156"
|
||||||
|
text-color="#bfcbd9"
|
||||||
|
active-text-color="#409EFF"
|
||||||
|
:collapse="isCollapse"
|
||||||
|
router
|
||||||
|
>
|
||||||
|
<el-menu-item index="/" class="menu-item">
|
||||||
|
<el-icon>
|
||||||
|
<HomeFilled />
|
||||||
|
</el-icon>
|
||||||
|
<template #title>首页</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/uav" class="menu-item">
|
||||||
|
<el-icon>
|
||||||
|
<Location />
|
||||||
|
</el-icon>
|
||||||
|
<template #title>无人机</template>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/task" class="menu-item">
|
||||||
|
<el-icon>
|
||||||
|
<MapLocation />
|
||||||
|
</el-icon>
|
||||||
|
<template #title>任务</template>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item index="/settings" class="menu-item">
|
||||||
|
<el-icon>
|
||||||
|
<Setting />
|
||||||
|
</el-icon>
|
||||||
|
<template #title>设置</template>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="21" class="menu-right">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import {
|
||||||
|
HomeFilled,
|
||||||
|
Location,
|
||||||
|
MapLocation,
|
||||||
|
Setting
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const activeMenu = ref(route.path)
|
||||||
|
// 默认不折叠 // 默认不折叠
|
||||||
|
const isCollapse = ref(false)
|
||||||
|
|
||||||
|
watch(() => route.path, (newPath) => {
|
||||||
|
activeMenu.value = newPath
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.menu-left {
|
||||||
|
width: 200px;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #304156;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-right {
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-vertical {
|
||||||
|
border-right: none;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu-vertical:not(.el-menu--collapse) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0 20px !important;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .el-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item :deep(.el-menu-tooltip__trigger) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
3
src/layouts/notFound.vue
Normal file
3
src/layouts/notFound.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
17
src/main.ts
Normal file
17
src/main.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// https://unocss.dev/ 原子 css 库
|
||||||
|
import '@unocss/reset/tailwind-compat.css' // unocss reset
|
||||||
|
import 'virtual:uno.css'
|
||||||
|
import 'virtual:unocss-devtools'
|
||||||
|
// 你自定义的 css
|
||||||
|
import './styles/main.css'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn'
|
||||||
|
import App from './App.vue'
|
||||||
|
const app = createApp(App)
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
app.use(ElementPlus, { locale: zhCn })
|
||||||
|
app.mount('#app')
|
58
src/pages/[...notFound].vue
Normal file
58
src/pages/[...notFound].vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const typedRef = useTyped([' is not found!'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-screen flex flex-wrap items-center justify-around text-center">
|
||||||
|
<div class="desc font-blod">
|
||||||
|
<div class="code text-7xl">404</div>
|
||||||
|
<div ref="typedRef" class="content mb-5 text-3xl">The Page</div>
|
||||||
|
<RouterLink :to="safeResolve('/')">
|
||||||
|
<button
|
||||||
|
class="rounded bg-light-800 px-5 py-2 text-lg transition"
|
||||||
|
hover="shadow-md"
|
||||||
|
dark="text-black"
|
||||||
|
>
|
||||||
|
Go Home
|
||||||
|
</button>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
:src="safeResolve('/notFound/32.svg')"
|
||||||
|
class="cover"
|
||||||
|
alt="page not found"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.code {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
height: auto;
|
||||||
|
width: 700px;
|
||||||
|
margin: 0 5px;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
flex: 1;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"title": "404",
|
||||||
|
"layout": "notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
359
src/pages/[id].vue
Normal file
359
src/pages/[id].vue
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="grid-content">
|
||||||
|
<!--顶部-->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="top">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!-- 中间-->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!-- 底部-->
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { Connection, FullScreen, VideoCamera, Message, Back } from '@element-plus/icons-vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 获取系统ID
|
||||||
|
const sysId = computed(() => {
|
||||||
|
const params = route.params as Record<string, any>
|
||||||
|
return 'id' in params ? (Array.isArray(params.id) ? params.id[0] : params.id ?? '') : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态数据
|
||||||
|
const state = reactive({
|
||||||
|
POWER_STATUS: { data: null as any },
|
||||||
|
SYS_STATUS: { data: null as any },
|
||||||
|
GPS_STATUS: { data: null as any },
|
||||||
|
HEARTBEAT: { data: null as any },
|
||||||
|
ATTITUDE: { data: null as any },
|
||||||
|
VFR_HUD: { data: null as any },
|
||||||
|
GLOBAL_POSITION_INT: { data: null as any },
|
||||||
|
MISSION_CURRENT: { data: null as any },
|
||||||
|
BATTERY_STATUS: { data: null as any },
|
||||||
|
EKF_STATUS_REPORT: { data: null as any }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 视频流URL
|
||||||
|
const videoUrl = ref('')
|
||||||
|
// WebSocket连接
|
||||||
|
const ws = ref<WebSocket | null>(null)
|
||||||
|
// 消息日志
|
||||||
|
const messages = ref<string[]>([])
|
||||||
|
// 连接状态
|
||||||
|
const isConnected = ref(false)
|
||||||
|
// 地图实例
|
||||||
|
const mapInstance = ref<any>(null)
|
||||||
|
// 地图容器引用
|
||||||
|
const mapContainer = ref<HTMLElement | null>(null)
|
||||||
|
// 计算属性
|
||||||
|
const systemStatusText = computed(() => {
|
||||||
|
if (!state.HEARTBEAT.data) return '未知'
|
||||||
|
const status = state.HEARTBEAT.data.system_status
|
||||||
|
if (status === 3) return '待机'
|
||||||
|
if (status === 4) return '活动中'
|
||||||
|
if (status === 5) return '关键'
|
||||||
|
return '未知'
|
||||||
|
})
|
||||||
|
|
||||||
|
const systemStatusType = computed(() => {
|
||||||
|
if (!state.HEARTBEAT.data) return 'info'
|
||||||
|
const status = state.HEARTBEAT.data.system_status
|
||||||
|
if (status === 3) return 'warning'
|
||||||
|
if (status === 4) return 'success'
|
||||||
|
if (status === 5) return 'danger'
|
||||||
|
return 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ekfHealth = computed(() => {
|
||||||
|
return state.EKF_STATUS_REPORT.data?.health || 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const ekfStatus = computed(() => {
|
||||||
|
const health = ekfHealth.value
|
||||||
|
if (health > 80) return 'success'
|
||||||
|
if (health > 50) return 'warning'
|
||||||
|
return 'exception'
|
||||||
|
})
|
||||||
|
|
||||||
|
const horizonStyle = computed(() => {
|
||||||
|
const pitch = state.ATTITUDE.data?.pitch || 0
|
||||||
|
const roll = state.ATTITUDE.data?.roll || 0
|
||||||
|
return {
|
||||||
|
transform: `rotate(${roll}deg) translateY(${pitch * 5}px)`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const yawStyle = computed(() => {
|
||||||
|
const yaw = state.ATTITUDE.data?.yaw || 0
|
||||||
|
return {
|
||||||
|
transform: `rotate(${yaw}deg)`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
function getFixType(type: number) {
|
||||||
|
const types = ['无', '无', '2D', '3D']
|
||||||
|
return types[type] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemMode(mode: number) {
|
||||||
|
// 这里需要根据MAVLink协议解析模式
|
||||||
|
return mode ? `模式 ${mode}` : '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFlightStatus(status: number) {
|
||||||
|
const statusMap = {
|
||||||
|
0: '未知',
|
||||||
|
1: '启动',
|
||||||
|
2: '校准',
|
||||||
|
3: '待机',
|
||||||
|
4: '活动中',
|
||||||
|
5: '关键',
|
||||||
|
6: '紧急'
|
||||||
|
}
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
const wsUrl = `ws://localhost:8000/ws/mavlink/heartbeat/1/`
|
||||||
|
ws.value = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
ws.value.onopen = () => {
|
||||||
|
isConnected.value = true
|
||||||
|
addMessage('WebSocket 已连接')
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.value.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
handleMavlinkMessage(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('消息解析失败:', error)
|
||||||
|
addMessage('消息解析失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.value.onclose = () => {
|
||||||
|
isConnected.value = false
|
||||||
|
addMessage('WebSocket 已断开')
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.value.onerror = (err) => {
|
||||||
|
addMessage('WebSocket 错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMavlinkMessage(data: any) {
|
||||||
|
if (!data.msg_type) return
|
||||||
|
|
||||||
|
// 更新对应状态
|
||||||
|
if (state[data.msg_type as keyof typeof state] !== undefined) {
|
||||||
|
state[data.msg_type as keyof typeof state].data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特殊处理某些消息
|
||||||
|
switch (data.msg_type) {
|
||||||
|
case 'HEARTBEAT':
|
||||||
|
addMessage(`心跳: 系统状态 ${getFlightStatus(data.system_status)}`)
|
||||||
|
break
|
||||||
|
case 'GPS_STATUS':
|
||||||
|
if (data.fix_type >= 3) {
|
||||||
|
updateMapPosition()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendHeartbeat() {
|
||||||
|
if (ws.value && isConnected.value) {
|
||||||
|
ws.value.send(JSON.stringify({ type: 'heartbeat' }))
|
||||||
|
addMessage('已发送心跳检测')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(msg: string) {
|
||||||
|
messages.value.push(msg)
|
||||||
|
if (messages.value.length > 100) {
|
||||||
|
messages.value.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(date: Date) {
|
||||||
|
return date.toLocaleTimeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen()
|
||||||
|
} else {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMap() {
|
||||||
|
if (!mapContainer.value) return
|
||||||
|
|
||||||
|
// 初始化地图
|
||||||
|
mapInstance.value = echarts.init(mapContainer.value)
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: '{b}'
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
type: 'graph',
|
||||||
|
layout: 'none',
|
||||||
|
coordinateSystem: 'none',
|
||||||
|
symbolSize: 50,
|
||||||
|
roam: true,
|
||||||
|
label: {
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
edgeSymbol: ['circle', 'arrow'],
|
||||||
|
edgeSymbolSize: [4, 10],
|
||||||
|
edgeLabel: {
|
||||||
|
fontSize: 20
|
||||||
|
},
|
||||||
|
data: [{
|
||||||
|
name: '无人机',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
symbolSize: 30,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#409EFF'
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
links: [],
|
||||||
|
lineStyle: {
|
||||||
|
opacity: 0.9,
|
||||||
|
width: 2,
|
||||||
|
curveness: 0
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
mapInstance.value.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMapPosition() {
|
||||||
|
if (!mapInstance.value || !state.GLOBAL_POSITION_INT.data) return
|
||||||
|
|
||||||
|
const lon = state.GLOBAL_POSITION_INT.data.lon / 1e7
|
||||||
|
const lat = state.GLOBAL_POSITION_INT.data.lat / 1e7
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
series: [{
|
||||||
|
data: [{
|
||||||
|
name: '无人机',
|
||||||
|
x: lon,
|
||||||
|
y: lat,
|
||||||
|
symbolSize: 30,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#409EFF'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
mapInstance.value.setOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无人机控制方法
|
||||||
|
function takeoff() {
|
||||||
|
if (ws.value && isConnected.value) {
|
||||||
|
ws.value.send(JSON.stringify({ command: 'takeoff' }))
|
||||||
|
addMessage('发送起飞指令')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function land() {
|
||||||
|
if (ws.value && isConnected.value) {
|
||||||
|
ws.value.send(JSON.stringify({ command: 'land' }))
|
||||||
|
addMessage('发送降落指令')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnToLaunch() {
|
||||||
|
if (ws.value && isConnected.value) {
|
||||||
|
ws.value.send(JSON.stringify({ command: 'rtl' }))
|
||||||
|
addMessage('发送返航指令')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMode(mode: string) {
|
||||||
|
if (ws.value && isConnected.value) {
|
||||||
|
ws.value.send(JSON.stringify({ command: 'set_mode', mode }))
|
||||||
|
addMessage(`设置模式: ${mode}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
connectWebSocket()
|
||||||
|
initMap()
|
||||||
|
|
||||||
|
// 模拟视频流
|
||||||
|
setTimeout(() => {
|
||||||
|
videoUrl.value = 'https://example.com/uav-video-stream'
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (ws.value) ws.value.close()
|
||||||
|
if (mapInstance.value) mapInstance.value.dispose()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.grid-content {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
background-image: url('uav/bg.png');
|
||||||
|
background-size: cover; /* 确保图片覆盖整个区域 */
|
||||||
|
}
|
||||||
|
.top{
|
||||||
|
height: 50px;
|
||||||
|
width: 69%;
|
||||||
|
background-color: #00dc82;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
518
src/pages/index.vue
Normal file
518
src/pages/index.vue
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<el-header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<!-- <img src="@/assets/logo.png" alt="无人机管理系统" class="logo-img">-->
|
||||||
|
<h1 class="title">无人机管理系统</h1>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<el-tooltip content="消息通知" placement="bottom">
|
||||||
|
<el-badge :value="12" class="notification-badge">
|
||||||
|
<el-icon :size="20">
|
||||||
|
<Bell />
|
||||||
|
</el-icon>
|
||||||
|
</el-badge>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-dropdown class="user-dropdown">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="36" src="@/assets/user-avatar.png" />
|
||||||
|
<span class="username">管理员</span>
|
||||||
|
<el-icon class="dropdown-icon">
|
||||||
|
<ArrowDown />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-icon>
|
||||||
|
<User />
|
||||||
|
</el-icon>
|
||||||
|
个人中心
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-icon>
|
||||||
|
<Setting />
|
||||||
|
</el-icon>
|
||||||
|
系统设置
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item divided>
|
||||||
|
<el-icon>
|
||||||
|
<SwitchButton />
|
||||||
|
</el-icon>
|
||||||
|
退出登录
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<el-main class="main-content">
|
||||||
|
<!-- 状态卡片 -->
|
||||||
|
<div class="status-cards">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
|
||||||
|
<el-card shadow="hover" class="status-card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon bg-blue">
|
||||||
|
<el-icon :size="28">
|
||||||
|
<Collection />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-title">无人机总数</div>
|
||||||
|
<div class="card-value">24</div>
|
||||||
|
<div class="card-trend">
|
||||||
|
<span class="trend-up">↑ 2.5%</span>
|
||||||
|
<span class="trend-text">较上月</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
|
||||||
|
<el-card shadow="hover" class="status-card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon bg-green">
|
||||||
|
<el-icon :size="28">
|
||||||
|
<Check />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-title">可用无人机</div>
|
||||||
|
<div class="card-value">18</div>
|
||||||
|
<div class="card-trend">
|
||||||
|
<span class="trend-up">↑ 5.8%</span>
|
||||||
|
<span class="trend-text">较上周</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
|
||||||
|
<el-card shadow="hover" class="status-card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon bg-orange">
|
||||||
|
<el-icon :size="28">
|
||||||
|
<Warning />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-title">任务中</div>
|
||||||
|
<div class="card-value">5</div>
|
||||||
|
<div class="card-trend">
|
||||||
|
<span class="trend-down">↓ 1.2%</span>
|
||||||
|
<span class="trend-text">较昨日</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
|
||||||
|
<el-card shadow="hover" class="status-card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon bg-purple">
|
||||||
|
<el-icon :size="28">
|
||||||
|
<Clock />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-title">待处理任务</div>
|
||||||
|
<div class="card-value">8</div>
|
||||||
|
<div class="card-trend">
|
||||||
|
<span class="trend-up">↑ 3.1%</span>
|
||||||
|
<span class="trend-text">较昨日</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="chart-area">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-card shadow="hover" class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>无人机任务统计</span>
|
||||||
|
<el-select v-model="chartTimeRange" class="time-select" size="small">
|
||||||
|
<el-option label="本周" value="week" />
|
||||||
|
<el-option label="本月" value="month" />
|
||||||
|
<el-option label="本季度" value="quarter" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="chart-container">
|
||||||
|
<!-- 这里放置echarts图表 -->
|
||||||
|
<div class="mock-chart" style="height: 300px;">
|
||||||
|
<div class="mock-chart-placeholder">
|
||||||
|
<el-icon :size="48">
|
||||||
|
<DataLine />
|
||||||
|
</el-icon>
|
||||||
|
<p>任务统计图表</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover" class="chart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>无人机状态分布</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="chart-container">
|
||||||
|
<!-- 这里放置饼图 -->
|
||||||
|
<div class="mock-chart" style="height: 300px;">
|
||||||
|
<div class="mock-chart-placeholder">
|
||||||
|
<el-icon :size="48">
|
||||||
|
<PieChart />
|
||||||
|
</el-icon>
|
||||||
|
<p>状态分布图表</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近任务列表 -->
|
||||||
|
<div class="task-list">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>最近任务</span>
|
||||||
|
<el-button type="primary" size="small" :icon="Refresh" @click="refreshTasks">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="recentTasks" style="width: 100%" stripe>
|
||||||
|
<el-table-column prop="id" label="任务ID" width="100" />
|
||||||
|
<el-table-column prop="name" label="任务名称" />
|
||||||
|
<el-table-column prop="drone" label="无人机" width="120" />
|
||||||
|
<el-table-column prop="startTime" label="开始时间" width="180" />
|
||||||
|
<el-table-column prop="endTime" label="结束时间" width="180" />
|
||||||
|
<el-table-column prop="status" label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusTagType(row.status)" size="small">
|
||||||
|
{{ row.status }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120">
|
||||||
|
<template #default>
|
||||||
|
<el-button type="text" size="small">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
small
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="50"
|
||||||
|
:page-size="5"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
ArrowDown,
|
||||||
|
User,
|
||||||
|
Setting,
|
||||||
|
SwitchButton,
|
||||||
|
Collection,
|
||||||
|
Check,
|
||||||
|
Warning,
|
||||||
|
Clock,
|
||||||
|
DataLine,
|
||||||
|
PieChart,
|
||||||
|
Refresh
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 图表时间范围选择
|
||||||
|
const chartTimeRange = ref('week')
|
||||||
|
|
||||||
|
// 最近任务数据
|
||||||
|
const recentTasks = ref([
|
||||||
|
{
|
||||||
|
id: 'T20230701',
|
||||||
|
name: '农田巡查-东部区域',
|
||||||
|
drone: 'DJI-001',
|
||||||
|
startTime: '2023-07-01 08:30',
|
||||||
|
endTime: '2023-07-01 11:45',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'T20230702',
|
||||||
|
name: '电力线路检查',
|
||||||
|
drone: 'DJI-003',
|
||||||
|
startTime: '2023-07-02 09:00',
|
||||||
|
endTime: '2023-07-02 12:15',
|
||||||
|
status: '已完成'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'T20230703',
|
||||||
|
name: '建筑工地监测',
|
||||||
|
drone: 'DJI-002',
|
||||||
|
startTime: '2023-07-03 10:30',
|
||||||
|
endTime: '2023-07-03 14:20',
|
||||||
|
status: '进行中'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'T20230704',
|
||||||
|
name: '森林防火巡查',
|
||||||
|
drone: 'DJI-005',
|
||||||
|
startTime: '2023-07-04 13:00',
|
||||||
|
endTime: '2023-07-04 16:30',
|
||||||
|
status: '已计划'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'T20230705',
|
||||||
|
name: '交通路况监测',
|
||||||
|
drone: 'DJI-004',
|
||||||
|
startTime: '2023-07-05 07:45',
|
||||||
|
endTime: '2023-07-05 10:15',
|
||||||
|
status: '已取消'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 获取状态标签类型
|
||||||
|
const getStatusTagType = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case '已完成':
|
||||||
|
return 'success'
|
||||||
|
case '进行中':
|
||||||
|
return 'primary'
|
||||||
|
case '已计划':
|
||||||
|
return 'info'
|
||||||
|
case '已取消':
|
||||||
|
return 'danger'
|
||||||
|
default:
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新任务
|
||||||
|
const refreshTasks = () => {
|
||||||
|
console.log('刷新任务数据')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
const handlePageChange = (currentPage: number) => {
|
||||||
|
console.log('当前页:', currentPage)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-container {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部样式 */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-badge {
|
||||||
|
margin-right: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin: 0 8px 0 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区样式 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态卡片样式 */
|
||||||
|
.status-cards {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blue {
|
||||||
|
background-color: #f0f7ff;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-green {
|
||||||
|
background-color: #f0f9eb;
|
||||||
|
color: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-orange {
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
color: #E6A23C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-purple {
|
||||||
|
background-color: #f9f0ff;
|
||||||
|
color: #8E44AD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-trend {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-up {
|
||||||
|
color: #67C23A;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-down {
|
||||||
|
color: #F56C6C;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-text {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表区域样式 */
|
||||||
|
.chart-area {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-select {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mock-chart {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mock-chart-placeholder {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 任务列表样式 */
|
||||||
|
.task-list {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
188
src/pages/login.vue
Normal file
188
src/pages/login.vue
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<el-card class="login-card">
|
||||||
|
<div class="logo">
|
||||||
|
<el-icon :size="60" color="#409EFF">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 16s-5.8-4-10-7l-2 2-2-2c-4.2 3-10 7-10 7l10 7 10-7z"/>
|
||||||
|
<path d="M12 9v6"/>
|
||||||
|
<path d="M15 12h-6"/>
|
||||||
|
</svg>
|
||||||
|
</el-icon>
|
||||||
|
<h1>无人机管理控制平台</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
ref="loginForm"
|
||||||
|
@submit.prevent="handleLogin"
|
||||||
|
class="login-form"
|
||||||
|
>
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="form.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
prefix-icon="User"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
prefix-icon="Lock"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="form.remember">记住我</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
native-type="submit"
|
||||||
|
size="large"
|
||||||
|
class="login-btn"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
登 录
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="links">
|
||||||
|
<!-- <el-link type="info" :underline="false">忘记密码?</el-link>-->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<div class="copyright">
|
||||||
|
© 2025 无人机管理控制平台 版权所有
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loginForm = ref<FormInstance>()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
remember: false
|
||||||
|
})
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 12, message: '长度在 3 到 12 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 18, message: '长度在 6 到 18 个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await loginForm.value?.validate()
|
||||||
|
|
||||||
|
// 这里添加实际的登录逻辑
|
||||||
|
console.log('登录信息:', form)
|
||||||
|
// 模拟登录过程
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
// 登录成功后跳转
|
||||||
|
router.push('/')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #0f2c59 0%, #1a4b8c 100%);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card :deep(.el-card__body) {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #303133;
|
||||||
|
margin: 15px 0 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
margin-top: 30px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整Element Plus组件在暗背景下的显示 */
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__label) {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
721
src/pages/task.vue
Normal file
721
src/pages/task.vue
Normal file
@ -0,0 +1,721 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mission-management">
|
||||||
|
<div class="header">
|
||||||
|
<h2>任务管理</h2>
|
||||||
|
<div class="operation-buttons">
|
||||||
|
<el-button type="primary" @click="handleCreate">
|
||||||
|
<el-icon>
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
|
创建任务
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:disabled="!selectedIds.length"
|
||||||
|
@click="handleBatchCancel"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<Close />
|
||||||
|
</el-icon>
|
||||||
|
批量取消
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-container">
|
||||||
|
<el-input
|
||||||
|
v-model="listQuery.keyword"
|
||||||
|
placeholder="搜索任务名称/描述"
|
||||||
|
style="width: 300px"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleFilter"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button :icon="Search" @click="handleFilter" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-select
|
||||||
|
v-model="listQuery.status"
|
||||||
|
placeholder="状态筛选"
|
||||||
|
clearable
|
||||||
|
style="width: 120px; margin-left: 10px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in statusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="listQuery.dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
style="margin-left: 10px; width: 280px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="missionList"
|
||||||
|
border
|
||||||
|
fit
|
||||||
|
highlight-current-row
|
||||||
|
style="width: 100%; margin-top: 20px"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
|
<el-table-column prop="name" label="任务名称" min-width="150" />
|
||||||
|
<el-table-column label="关联无人机" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-for="drone in row.drones" :key="drone.id" style="margin-right: 5px">
|
||||||
|
{{ drone.name }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="type" label="任务类型" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="typeTagMap[row.type]">
|
||||||
|
{{ typeMap[row.type] }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusTagMap[row.status]">
|
||||||
|
{{ statusMap[row.status] }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="startTime" label="开始时间" width="180" />
|
||||||
|
<el-table-column prop="endTime" label="结束时间" width="180" />
|
||||||
|
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="handleDetail(row)">
|
||||||
|
<el-icon>
|
||||||
|
<View />
|
||||||
|
</el-icon>
|
||||||
|
详情
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:disabled="row.status !== 'pending'"
|
||||||
|
@click="handleExecute(row)"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<VideoPlay />
|
||||||
|
</el-icon>
|
||||||
|
执行
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
:disabled="!['pending', 'progress'].includes(row.status)"
|
||||||
|
@click="handleCancel(row)"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<Close />
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="listQuery.page"
|
||||||
|
v-model:page-size="listQuery.limit"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 30, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务创建/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'edit' ? '编辑任务' : '创建任务'"
|
||||||
|
width="700px"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="missionForm"
|
||||||
|
:model="missionForm"
|
||||||
|
:rules="missionRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="任务名称" prop="name">
|
||||||
|
<el-input v-model="missionForm.name" placeholder="请输入任务名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任务类型" prop="type">
|
||||||
|
<el-select v-model="missionForm.type" placeholder="请选择任务类型">
|
||||||
|
<el-option
|
||||||
|
v-for="item in typeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="关联无人机" prop="drones">
|
||||||
|
<el-select
|
||||||
|
v-model="missionForm.drones"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
placeholder="请选择无人机"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="drone in droneOptions"
|
||||||
|
:key="drone.id"
|
||||||
|
:label="drone.name"
|
||||||
|
:value="drone.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任务时间" prop="timeRange">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="missionForm.timeRange"
|
||||||
|
type="datetimerange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始时间"
|
||||||
|
end-placeholder="结束时间"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任务描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="missionForm.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入任务描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="航线规划" prop="route">
|
||||||
|
<el-button type="primary" @click="showRouteMap = true">
|
||||||
|
<el-icon>
|
||||||
|
<MapLocation />
|
||||||
|
</el-icon>
|
||||||
|
规划航线
|
||||||
|
</el-button>
|
||||||
|
<div v-if="missionForm.route" class="route-preview">
|
||||||
|
<span>已设置 {{ missionForm.route.points.length }} 个航点</span>
|
||||||
|
<el-button type="text" @click="showRouteMap = true">查看</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmMission">确认</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 航线规划地图 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showRouteMap"
|
||||||
|
title="航线规划"
|
||||||
|
width="80%"
|
||||||
|
top="5vh"
|
||||||
|
fullscreen
|
||||||
|
>
|
||||||
|
<div class="map-container">
|
||||||
|
<!-- 这里放置地图组件 -->
|
||||||
|
<div style="height: 80vh; background: #f5f5f5; display: flex; align-items: center; justify-content: center">
|
||||||
|
<h3>地图组件区域 (可集成高德/Google/百度地图)</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showRouteMap = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveRoute">保存航线</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 任务详情抽屉 -->
|
||||||
|
<el-drawer
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="任务详情"
|
||||||
|
direction="rtl"
|
||||||
|
size="50%"
|
||||||
|
>
|
||||||
|
<div v-if="currentMission" class="mission-detail">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="任务ID">{{ currentMission.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="任务名称">{{ currentMission.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="任务类型">
|
||||||
|
<el-tag :type="typeTagMap[currentMission.type]">
|
||||||
|
{{ typeMap[currentMission.type] }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="任务状态">
|
||||||
|
<el-tag :type="statusTagMap[currentMission.status]">
|
||||||
|
{{ statusMap[currentMission.status] }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="关联无人机">
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 5px">
|
||||||
|
<el-tag
|
||||||
|
v-for="drone in currentMission.drones"
|
||||||
|
:key="drone.id"
|
||||||
|
type="info"
|
||||||
|
>
|
||||||
|
{{ drone.name }} ({{ drone.model }})
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="开始时间">{{ currentMission.startTime }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结束时间">{{ currentMission.endTime }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="任务描述">
|
||||||
|
<div style="white-space: pre-line">{{ currentMission.description }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="航线规划">
|
||||||
|
<div
|
||||||
|
style="height: 300px; background: #f5f5f5; display: flex; align-items: center; justify-content: center">
|
||||||
|
<h3>航线地图预览</h3>
|
||||||
|
</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div v-if="currentMission.status === 'progress'" class="real-time-data">
|
||||||
|
<h3 style="margin: 20px 0 10px">实时数据</h3>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<span>无人机状态</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div v-for="drone in currentMission.drones" :key="drone.id" class="drone-status">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="label">{{ drone.name }}:</span>
|
||||||
|
<el-tag :type="drone.status === 'online' ? 'success' : 'danger'" size="small">
|
||||||
|
{{ drone.status === 'online' ? '在线' : '离线' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="label">电量:</span>
|
||||||
|
<el-progress
|
||||||
|
:percentage="drone.battery"
|
||||||
|
:color="batteryColor(drone.battery)"
|
||||||
|
:show-text="false"
|
||||||
|
:stroke-width="12"
|
||||||
|
style="width: 80px"
|
||||||
|
/>
|
||||||
|
<span class="value">{{ drone.battery }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<span>任务进度</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<el-progress
|
||||||
|
:percentage="missionProgress"
|
||||||
|
:stroke-width="16"
|
||||||
|
:text-inside="true"
|
||||||
|
/>
|
||||||
|
<div class="progress-detail">
|
||||||
|
<div>已完成航点: 12/24</div>
|
||||||
|
<div>预计剩余时间: 32分钟</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<span>实时画面</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content" style="text-align: center">
|
||||||
|
<div
|
||||||
|
style="height: 160px; background: #000; display: flex; align-items: center; justify-content: center">
|
||||||
|
<span style="color: #fff">视频直播画面</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Close,
|
||||||
|
Search,
|
||||||
|
View,
|
||||||
|
VideoPlay,
|
||||||
|
MapLocation
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 状态映射
|
||||||
|
const statusMap = {
|
||||||
|
pending: '待执行',
|
||||||
|
progress: '进行中',
|
||||||
|
completed: '已完成',
|
||||||
|
cancelled: '已取消',
|
||||||
|
failed: '失败'
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusTagMap = {
|
||||||
|
pending: 'warning',
|
||||||
|
progress: 'primary',
|
||||||
|
completed: 'success',
|
||||||
|
cancelled: 'info',
|
||||||
|
failed: 'danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeMap = {
|
||||||
|
patrol: '巡检',
|
||||||
|
mapping: '测绘',
|
||||||
|
emergency: '应急',
|
||||||
|
transport: '运输'
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeTagMap = {
|
||||||
|
patrol: '',
|
||||||
|
mapping: 'success',
|
||||||
|
emergency: 'danger',
|
||||||
|
transport: 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: 'pending', label: '待执行' },
|
||||||
|
{ value: 'progress', label: '进行中' },
|
||||||
|
{ value: 'completed', label: '已完成' },
|
||||||
|
{ value: 'cancelled', label: '已取消' },
|
||||||
|
{ value: 'failed', label: '失败' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ value: 'patrol', label: '巡检' },
|
||||||
|
{ value: 'mapping', label: '测绘' },
|
||||||
|
{ value: 'emergency', label: '应急' },
|
||||||
|
{ value: 'transport', label: '运输' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 电池颜色
|
||||||
|
const batteryColor = (percentage) => {
|
||||||
|
if (percentage > 70) return '#67C23A'
|
||||||
|
if (percentage > 30) return '#E6A23C'
|
||||||
|
return '#F56C6C'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表相关
|
||||||
|
const listLoading = ref(false)
|
||||||
|
const missionList = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const selectedIds = ref([])
|
||||||
|
|
||||||
|
const listQuery = reactive({
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
keyword: '',
|
||||||
|
status: '',
|
||||||
|
dateRange: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 无人机选项(模拟数据)
|
||||||
|
const droneOptions = ref([
|
||||||
|
{ id: 1, name: '巡检无人机1号', model: 'DJI Mavic 3' },
|
||||||
|
{ id: 2, name: '测绘无人机A', model: 'DJI Phantom 4 RTK' },
|
||||||
|
{ id: 3, name: '应急无人机', model: 'DJI Matrice 300' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 表单相关
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const showRouteMap = ref(false)
|
||||||
|
const dialogType = ref('add')
|
||||||
|
const currentMission = ref(null)
|
||||||
|
const missionProgress = ref(45) // 模拟进度
|
||||||
|
|
||||||
|
const missionForm = reactive({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
type: 'patrol',
|
||||||
|
drones: [],
|
||||||
|
timeRange: [],
|
||||||
|
description: '',
|
||||||
|
route: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const missionRules = reactive({
|
||||||
|
name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
||||||
|
type: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
|
||||||
|
drones: [{ required: true, message: '请选择至少一架无人机', trigger: 'change' }],
|
||||||
|
timeRange: [{ required: true, message: '请选择任务时间', trigger: 'change' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取任务列表
|
||||||
|
const fetchMissions = async () => {
|
||||||
|
listLoading.value = true
|
||||||
|
try {
|
||||||
|
// 这里替换为实际API调用
|
||||||
|
// const res = await getMissionList(listQuery)
|
||||||
|
// 模拟数据
|
||||||
|
setTimeout(() => {
|
||||||
|
missionList.value = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '园区安全巡检',
|
||||||
|
type: 'patrol',
|
||||||
|
status: 'pending',
|
||||||
|
drones: [
|
||||||
|
{ id: 1, name: '巡检无人机1号', model: 'DJI Mavic 3', status: 'online', battery: 85 },
|
||||||
|
{ id: 3, name: '应急无人机', model: 'DJI Matrice 300', status: 'online', battery: 90 }
|
||||||
|
],
|
||||||
|
startTime: '2023-05-20 09:00:00',
|
||||||
|
endTime: '2023-05-20 11:00:00',
|
||||||
|
description: '每日例行园区安全巡检,重点检查围墙和屋顶区域'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '建筑工地测绘',
|
||||||
|
type: 'mapping',
|
||||||
|
status: 'progress',
|
||||||
|
drones: [
|
||||||
|
{ id: 2, name: '测绘无人机A', model: 'DJI Phantom 4 RTK', status: 'online', battery: 65 }
|
||||||
|
],
|
||||||
|
startTime: '2023-05-19 14:00:00',
|
||||||
|
endTime: '2023-05-19 16:30:00',
|
||||||
|
description: '新建筑工地三维建模测绘'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '紧急物资运输',
|
||||||
|
type: 'emergency',
|
||||||
|
status: 'completed',
|
||||||
|
drones: [
|
||||||
|
{ id: 3, name: '应急无人机', model: 'DJI Matrice 300', status: 'offline', battery: 25 }
|
||||||
|
],
|
||||||
|
startTime: '2023-05-18 10:15:00',
|
||||||
|
endTime: '2023-05-18 11:30:00',
|
||||||
|
description: '向山区运送紧急医疗物资'
|
||||||
|
}
|
||||||
|
].slice(
|
||||||
|
(listQuery.page - 1) * listQuery.limit,
|
||||||
|
listQuery.page * listQuery.limit
|
||||||
|
)
|
||||||
|
total.value = 3
|
||||||
|
listLoading.value = false
|
||||||
|
}, 500)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
listLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格选择
|
||||||
|
const handleSelectionChange = (selection) => {
|
||||||
|
selectedIds.value = selection.map((item) => item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选
|
||||||
|
const handleFilter = () => {
|
||||||
|
listQuery.page = 1
|
||||||
|
fetchMissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
listQuery.limit = val
|
||||||
|
fetchMissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
listQuery.page = val
|
||||||
|
fetchMissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建任务
|
||||||
|
const handleCreate = () => {
|
||||||
|
dialogType.value = 'add'
|
||||||
|
Object.assign(missionForm, {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
type: 'patrol',
|
||||||
|
drones: [],
|
||||||
|
timeRange: [],
|
||||||
|
description: '',
|
||||||
|
route: null
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleDetail = (row) => {
|
||||||
|
currentMission.value = row
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行任务
|
||||||
|
const handleExecute = (row) => {
|
||||||
|
ElMessageBox.confirm(`确认开始执行任务 "${row.name}"?`, '提示', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}).then(() => {
|
||||||
|
// 这里替换为实际API调用
|
||||||
|
// await executeMission(row.id)
|
||||||
|
ElMessage.success('任务已开始执行')
|
||||||
|
fetchMissions()
|
||||||
|
}).catch(() => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消任务
|
||||||
|
const handleCancel = (row) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确认${row.status === 'progress' ? '终止' : '取消'}任务 "${row.name}"?`,
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
// 这里替换为实际API调用
|
||||||
|
// await cancelMission(row.id)
|
||||||
|
ElMessage.success(`任务已${row.status === 'progress' ? '终止' : '取消'}`)
|
||||||
|
fetchMissions()
|
||||||
|
}).catch(() => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量取消
|
||||||
|
const handleBatchCancel = () => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确认取消选中的 ${selectedIds.value.length} 个任务?`,
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
// 这里替换为实际API调用
|
||||||
|
// await batchCancelMissions(selectedIds.value)
|
||||||
|
ElMessage.success('取消成功')
|
||||||
|
selectedIds.value = []
|
||||||
|
fetchMissions()
|
||||||
|
}).catch(() => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存航线
|
||||||
|
const saveRoute = () => {
|
||||||
|
// 这里应该是从地图组件获取航线数据
|
||||||
|
missionForm.route = {
|
||||||
|
points: [
|
||||||
|
{ lat: 39.9042, lng: 116.4074 },
|
||||||
|
{ lat: 39.9082, lng: 116.4074 },
|
||||||
|
{ lat: 39.9082, lng: 116.4174 },
|
||||||
|
{ lat: 39.9042, lng: 116.4174 }
|
||||||
|
],
|
||||||
|
distance: '2.5km',
|
||||||
|
estimatedTime: '15分钟'
|
||||||
|
}
|
||||||
|
showRouteMap.value = false
|
||||||
|
ElMessage.success('航线已保存')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认表单
|
||||||
|
const confirmMission = () => {
|
||||||
|
// 这里替换为实际表单验证和API调用
|
||||||
|
ElMessage.success(
|
||||||
|
dialogType.value === 'add' ? '创建成功' : '更新成功'
|
||||||
|
)
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchMissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchMissions()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mission-management {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-preview {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-detail {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.drone-status {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 80px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-detail {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
411
src/pages/uav.vue
Normal file
411
src/pages/uav.vue
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
<template>
|
||||||
|
<div class="drone-management">
|
||||||
|
<div class="header">
|
||||||
|
<h2>无人机管理</h2>
|
||||||
|
<div class="operation-buttons">
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon>
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
|
添加无人机
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" :disabled="!selectedIds.length" @click="handleBatchDelete">
|
||||||
|
<el-icon>
|
||||||
|
<Delete />
|
||||||
|
</el-icon>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-container">
|
||||||
|
<el-input
|
||||||
|
v-model="listQuery.keyword"
|
||||||
|
placeholder="搜索无人机名称/型号"
|
||||||
|
style="width: 300px"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleFilter"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button :icon="Search" @click="handleFilter" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-select
|
||||||
|
v-model="listQuery.status"
|
||||||
|
placeholder="状态筛选"
|
||||||
|
clearable
|
||||||
|
style="width: 120px; margin-left: 10px"
|
||||||
|
>
|
||||||
|
<el-option label="在线" value="online" />
|
||||||
|
<el-option label="离线" value="offline" />
|
||||||
|
<el-option label="维修中" value="maintenance" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="listLoading"
|
||||||
|
:data="droneList"
|
||||||
|
border
|
||||||
|
fit
|
||||||
|
highlight-current-row
|
||||||
|
style="width: 100%"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
|
<el-table-column prop="name" label="无人机名称" min-width="120" />
|
||||||
|
<el-table-column prop="model" label="型号" width="150" />
|
||||||
|
<el-table-column prop="sn" label="序列号" width="180" />
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusType[row.status]">
|
||||||
|
{{ statusMap[row.status] }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="battery" label="电量" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-progress
|
||||||
|
:percentage="row.battery"
|
||||||
|
:color="batteryColor(row.battery)"
|
||||||
|
:show-text="false"
|
||||||
|
:stroke-width="18"
|
||||||
|
/>
|
||||||
|
{{ row.battery }}%
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="lastConnectTime" label="最后连接时间" width="180" />
|
||||||
|
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" @click="handleEdit(row)">
|
||||||
|
<el-icon>
|
||||||
|
<Edit />
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="handleDelete(row)">
|
||||||
|
<el-icon>
|
||||||
|
<Delete />
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="warning" @click="handleControl(row)">
|
||||||
|
<el-icon>
|
||||||
|
<Connection />
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="listQuery.page"
|
||||||
|
v-model:page-size="listQuery.limit"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 30, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogType === 'edit' ? '编辑无人机' : '添加无人机'"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="droneForm"
|
||||||
|
:model="droneForm"
|
||||||
|
:rules="droneRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="无人机名称" prop="name">
|
||||||
|
<el-input v-model="droneForm.name" placeholder="请输入无人机名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="无人机型号" prop="model">
|
||||||
|
<el-input v-model="droneForm.model" placeholder="请输入无人机型号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="序列号" prop="sn">
|
||||||
|
<el-input v-model="droneForm.sn" placeholder="请输入序列号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="droneForm.status" placeholder="请选择状态">
|
||||||
|
<el-option
|
||||||
|
v-for="item in statusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmDrone">确认</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Delete,
|
||||||
|
Edit,
|
||||||
|
Search,
|
||||||
|
Connection
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
|
||||||
|
// 状态映射
|
||||||
|
const statusMap = {
|
||||||
|
online: '在线',
|
||||||
|
offline: '离线',
|
||||||
|
maintenance: '维修中'
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusType = {
|
||||||
|
online: 'success',
|
||||||
|
offline: 'danger',
|
||||||
|
maintenance: 'warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: 'online', label: '在线' },
|
||||||
|
{ value: 'offline', label: '离线' },
|
||||||
|
{ value: 'maintenance', label: '维修中' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 电池颜色
|
||||||
|
const batteryColor = (percentage) => {
|
||||||
|
if (percentage > 70) return '#67C23A'
|
||||||
|
if (percentage > 30) return '#E6A23C'
|
||||||
|
return '#F56C6C'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表相关
|
||||||
|
const listLoading = ref(false)
|
||||||
|
const droneList = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const selectedIds = ref([])
|
||||||
|
|
||||||
|
const listQuery = reactive({
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
keyword: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单相关
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogType = ref('add')
|
||||||
|
const droneForm = reactive({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
model: '',
|
||||||
|
sn: '',
|
||||||
|
status: 'online',
|
||||||
|
battery: 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const droneRules = reactive({
|
||||||
|
name: [{ required: true, message: '请输入无人机名称', trigger: 'blur' }],
|
||||||
|
model: [{ required: true, message: '请输入无人机型号', trigger: 'blur' }],
|
||||||
|
sn: [{ required: true, message: '请输入序列号', trigger: 'blur' }],
|
||||||
|
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取无人机列表
|
||||||
|
const fetchDrones = async () => {
|
||||||
|
listLoading.value = true
|
||||||
|
try {
|
||||||
|
// 这里替换为实际API调用
|
||||||
|
// const res = await getDroneList(listQuery)
|
||||||
|
// 模拟数据
|
||||||
|
setTimeout(() => {
|
||||||
|
droneList.value = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '巡检无人机1号',
|
||||||
|
model: 'DJI Mavic 3',
|
||||||
|
sn: 'DJI123456789',
|
||||||
|
status: 'online',
|
||||||
|
battery: 85,
|
||||||
|
lastConnectTime: '2023-05-15 14:30:22'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '测绘无人机A',
|
||||||
|
model: 'DJI Phantom 4 RTK',
|
||||||
|
sn: 'DJI987654321',
|
||||||
|
status: 'offline',
|
||||||
|
battery: 25,
|
||||||
|
lastConnectTime: '2023-05-14 09:15:33'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '应急无人机',
|
||||||
|
model: 'DJI Matrice 300',
|
||||||
|
sn: 'DJI456789123',
|
||||||
|
status: 'maintenance',
|
||||||
|
battery: 0,
|
||||||
|
lastConnectTime: '2023-05-10 16:45:12'
|
||||||
|
}
|
||||||
|
].slice(
|
||||||
|
(listQuery.page - 1) * listQuery.limit,
|
||||||
|
listQuery.page * listQuery.limit
|
||||||
|
)
|
||||||
|
total.value = 3
|
||||||
|
listLoading.value = false
|
||||||
|
}, 500)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
listLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格选择
|
||||||
|
const handleSelectionChange = (selection) => {
|
||||||
|
selectedIds.value = selection.map((item) => item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选
|
||||||
|
const handleFilter = () => {
|
||||||
|
listQuery.page = 1
|
||||||
|
fetchDrones()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
listQuery.limit = val
|
||||||
|
fetchDrones()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
listQuery.page = val
|
||||||
|
fetchDrones()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加无人机
|
||||||
|
const handleAdd = () => {
|
||||||
|
dialogType.value = 'add'
|
||||||
|
Object.assign(droneForm, {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
model: '',
|
||||||
|
sn: '',
|
||||||
|
status: 'online',
|
||||||
|
battery: 100
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑无人机
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
dialogType.value = 'edit'
|
||||||
|
Object.assign(droneForm, row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除无人机
|
||||||
|
const handleDelete = (row) => {
|
||||||
|
ElMessageBox.confirm(`确认删除无人机 "${row.name}"?`, '警告', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
// 这里替换为实际API调用
|
||||||
|
// await deleteDrone(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchDrones()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
const handleBatchDelete = () => {
|
||||||
|
ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 台无人机?`, '警告', {
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
// 这里替换为实际API调用
|
||||||
|
// await batchDeleteDrones(selectedIds.value)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
selectedIds.value = []
|
||||||
|
fetchDrones()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制无人机
|
||||||
|
const handleControl = (row) => {
|
||||||
|
ElMessage.info(`正在连接无人机 ${row.name}...`)
|
||||||
|
// 这里可以跳转到控制页面或打开控制对话框
|
||||||
|
router.push(`${row.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认表单
|
||||||
|
const confirmDrone = () => {
|
||||||
|
// 这里替换为实际表单验证和API调用
|
||||||
|
ElMessage.success(
|
||||||
|
dialogType.value === 'add' ? '添加成功' : '更新成功'
|
||||||
|
)
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchDrones()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDrones()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.drone-management {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tag {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-progress {
|
||||||
|
display: inline-block;
|
||||||
|
width: 80px;
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
23
src/plugins/mock.ts
Normal file
23
src/plugins/mock.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 该模块主要给生产时的 mock 用,一般情况下你并不需要关注
|
||||||
|
*/
|
||||||
|
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
||||||
|
import { createFetchSever } from '../../presets/shared/mock'
|
||||||
|
|
||||||
|
const shouldCreateServer =
|
||||||
|
!import.meta.env.DEV && import.meta.env.VITE_APP_MOCK_IN_PRODUCTION === 'true'
|
||||||
|
|
||||||
|
// 生产环境时才创建服务
|
||||||
|
if (shouldCreateServer) {
|
||||||
|
const mockModules: any[] = []
|
||||||
|
const modules = import.meta.glob('../../mock/*.ts', {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
Object.values(modules).forEach((v: any) => {
|
||||||
|
if (Array.isArray(v.default)) {
|
||||||
|
mockModules.push(...v.default)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
createProdMockServer(mockModules)
|
||||||
|
createFetchSever(mockModules)
|
||||||
|
}
|
9
src/plugins/nprogress.ts
Normal file
9
src/plugins/nprogress.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { router } from './router'
|
||||||
|
import { useNProgress } from '@vueuse/integrations/useNProgress'
|
||||||
|
|
||||||
|
// https://vueuse.org/integrations/useNProgress/
|
||||||
|
const { start, done } = useNProgress()
|
||||||
|
|
||||||
|
router.beforeEach(() => start())
|
||||||
|
|
||||||
|
router.afterEach(() => done(true))
|
7
src/plugins/pinia.ts
Normal file
7
src/plugins/pinia.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import persistedstate from 'pinia-plugin-persistedstate'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
pinia.use(persistedstate)
|
||||||
|
|
||||||
|
export default pinia
|
26
src/plugins/router.ts
Normal file
26
src/plugins/router.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { createGetRoutes, setupLayouts } from 'virtual:meta-layouts'
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { routes as fileRoutes } from 'vue-router/auto-routes'
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
// 在这里定义你的 meta 类型
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
interface RouteMeta {
|
||||||
|
title?: string
|
||||||
|
layout?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重定向 BASE_URL
|
||||||
|
fileRoutes.flat(Infinity).forEach((route) => {
|
||||||
|
route.path = safeResolve(route.path)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: setupLayouts(fileRoutes),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getRoutes = createGetRoutes(router)
|
||||||
|
|
||||||
|
export default router
|
17
src/plugins/title.ts
Normal file
17
src/plugins/title.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { router } from './router'
|
||||||
|
|
||||||
|
useTitle(
|
||||||
|
() => {
|
||||||
|
const { path, meta } = router.currentRoute.value
|
||||||
|
if (meta.title) {
|
||||||
|
return `· ${meta.title}`
|
||||||
|
}
|
||||||
|
if (path === '/') {
|
||||||
|
return '· home'
|
||||||
|
}
|
||||||
|
return path.replaceAll('/', ' · ')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleTemplate: `${import.meta.env.VITE_APP_TITLE} %s`,
|
||||||
|
},
|
||||||
|
)
|
15
src/stores/createCounter.ts
Normal file
15
src/stores/createCounter.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export default defineStore('counter', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
count: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
inc() {
|
||||||
|
this.count++
|
||||||
|
},
|
||||||
|
},
|
||||||
|
persist: true,
|
||||||
|
})
|
55
src/styles/main.css
Normal file
55
src/styles/main.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
@import './md.css';
|
||||||
|
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
background: #100c2a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .bar {
|
||||||
|
@apply bg-blue-700 bg-opacity-75;
|
||||||
|
|
||||||
|
background: repeating-linear-gradient(90deg, #00dc82 0, #34cdfe 50%, #0047e1);
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1031;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 全局滚动条
|
||||||
|
*/
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 11px;
|
||||||
|
background-color: rgb(246, 247, 248);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgb(233, 236, 239);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: rgb(246, 247, 248);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark ::-webkit-scrollbar,
|
||||||
|
html.dark ::-webkit-scrollbar-track {
|
||||||
|
background-color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #343a40;
|
||||||
|
}
|
83
src/styles/md.css
Normal file
83
src/styles/md.css
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
@import 'prism-theme-vars/base.css';
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
--prism-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica,
|
||||||
|
Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not(.dark) {
|
||||||
|
--prism-foreground: #393a34;
|
||||||
|
--prism-background: #f8f8f8;
|
||||||
|
|
||||||
|
--prism-comment: #868e96;
|
||||||
|
--prism-namespace: #444444;
|
||||||
|
--prism-string: #bc8671;
|
||||||
|
--prism-punctuation: #80817d;
|
||||||
|
--prism-literal: #36acaa;
|
||||||
|
--prism-keyword: #d73a49;
|
||||||
|
--prism-function: #0c4c7d;
|
||||||
|
--prism-deleted: #9a050f;
|
||||||
|
--prism-class: #2b91af;
|
||||||
|
--prism-builtin: #800000;
|
||||||
|
--prism-property: #ce9178;
|
||||||
|
--prism-regex: #ad502b;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--prism-foreground: #d4d4d4;
|
||||||
|
--prism-background: #1e1e1e;
|
||||||
|
|
||||||
|
--prism-namespace: #aaaaaa;
|
||||||
|
--prism-comment: #868e96;
|
||||||
|
--prism-namespace: #444444;
|
||||||
|
--prism-string: #ce9178;
|
||||||
|
--prism-punctuation: #d4d4d4;
|
||||||
|
--prism-literal: #36acaa;
|
||||||
|
--prism-keyword: #0ca678;
|
||||||
|
--prism-function: #dcdcaa;
|
||||||
|
--prism-deleted: #9a050f;
|
||||||
|
--prism-class: #4ec9b0;
|
||||||
|
--prism-builtin: #d16969;
|
||||||
|
--prism-property: #ce9178;
|
||||||
|
--prism-regex: #ad502b;
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose blockquote p:first-of-type::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre {
|
||||||
|
color: #495057;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose-sm p {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose blockquote {
|
||||||
|
margin: 0;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .prose blockquote {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .prose pre {
|
||||||
|
color: #f8f9fa;
|
||||||
|
background: #2a2f33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
}
|
7
src/test/__snapshots__/index.test.ts.snap
Normal file
7
src/test/__snapshots__/index.test.ts.snap
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Vitest Snapshot v1
|
||||||
|
|
||||||
|
exports[`suite name > snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
`;
|
16
src/test/index.test.ts
Normal file
16
src/test/index.test.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { assert, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
describe('suite name', () => {
|
||||||
|
it('foo', () => {
|
||||||
|
expect(1 + 1).toEqual(2)
|
||||||
|
expect(true).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bar', () => {
|
||||||
|
assert.equal(Math.sqrt(4), 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('snapshot', () => {
|
||||||
|
expect({ foo: 'bar' }).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
32
tsconfig.json
Normal file
32
tsconfig.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"sourceMap": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsxImportSource": "vue",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["esnext", "dom"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["src/*"],
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"srcipts",
|
||||||
|
"presets",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"./vite.config.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
24
uno.config.ts
Normal file
24
uno.config.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
defineConfig,
|
||||||
|
presetAttributify,
|
||||||
|
presetIcons,
|
||||||
|
presetTypography,
|
||||||
|
presetUno,
|
||||||
|
transformerVariantGroup,
|
||||||
|
transformerDirectives,
|
||||||
|
} from 'unocss'
|
||||||
|
|
||||||
|
import presetAutoprefixer from './presets/autoprefixer'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
transformers: [transformerDirectives(), transformerVariantGroup()],
|
||||||
|
presets: [
|
||||||
|
presetAttributify(),
|
||||||
|
presetIcons({
|
||||||
|
autoInstall: true,
|
||||||
|
}),
|
||||||
|
presetUno(),
|
||||||
|
presetTypography(),
|
||||||
|
presetAutoprefixer(),
|
||||||
|
],
|
||||||
|
})
|
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Tov from './presets'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [Tov()],
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user