Compare commits

..

5 Commits

Author SHA1 Message Date
Bunny 630e54ee74 feat: 整体布局完成 2025-02-25 23:19:32 +08:00
Bunny 26540ca419 feat: 整体布局完成 2025-02-25 23:14:50 +08:00
Bunny d5acd952d6 feat: 添加路由和仓库 2025-02-25 18:29:26 +08:00
Bunny e3c5d7333a feat: 添加环境变量枚举 2025-02-24 22:45:14 +08:00
Bunny 5f5f1e3c6f feat: init 2025-02-24 18:23:33 +08:00
63 changed files with 6770 additions and 2 deletions

21
.dockerignore Normal file
View File

@ -0,0 +1,21 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

20
.env Normal file
View File

@ -0,0 +1,20 @@
# 应用名称
VITE_APP_TITLE="车辆监控中心"
# 平台本地运行端口号
VITE_PORT=7000
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH=/
# 跨域代理地址
VITE_APP_URL=http://localhost:8801
# 如果端口被占用会直接退出,而不是尝试下一个端口
VITE_STRICT_PORT=false
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN=false
# 是否启用gzip压缩
VITE_COMPRESSION=gzip

17
.env.development Normal file
View File

@ -0,0 +1,17 @@
# 平台本地运行端口号
VITE_PORT=7000
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH=/
# 跨域代理地址
VITE_APP_URL=http://localhost:8801
# 如果端口被占用会直接退出,而不是尝试下一个端口
VITE_STRICT_PORT=false
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN=false
# 是否启用gzip压缩
VITE_COMPRESSION=gzip

17
.env.production Normal file
View File

@ -0,0 +1,17 @@
# 平台本地运行端口号
VITE_PORT=8800
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH=/
# 跨域代理地址
VITE_APP_URL=http://localhost:8000
# 如果端口被占用会直接退出,而不是尝试下一个端口
VITE_STRICT_PORT=false
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN=false
# 是否启用gzip压缩
VITE_COMPRESSION=gzip

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

11
.prettierignore Normal file
View File

@ -0,0 +1,11 @@
dist
node_modules
public
.husky
.vscode
.idea
*.sh
*.md
src/assets
stats.html

46
.prettierrc.js Normal file
View File

@ -0,0 +1,46 @@
export default {
// (x)=>{},单个参数箭头函数是否显示小括号。(always:始终显示;avoid:省略括号。默认:always)
arrowParens: "always",
// 开始标签的右尖括号是否跟随在最后一行属性末尾默认false
bracketSameLine: false,
// 对象字面量的括号之间打印空格 (true - Example: { foo: bar } ; false - Example: {foo:bar})
bracketSpacing: true,
// 是否格式化一些文件中被嵌入的代码片段的风格(auto|off;默认auto)
embeddedLanguageFormatting: "auto",
// 指定 HTML 文件的空格敏感度 (css|strict|ignore;默认css)
htmlWhitespaceSensitivity: "ignore",
// 当文件已经被 Prettier 格式化之后,是否会在文件顶部插入一个特殊的 @format 标记默认false
insertPragma: false,
// 在 JSX 中使用单引号替代双引号默认false
jsxSingleQuote: false,
// 每行最多字符数量,超出换行(默认100)
printWidth: 100,
// 超出打印宽度 (always | never | preserve )
proseWrap: "preserve",
// 对象属性是否使用引号(as-needed | consistent | preserve;默认as-needed:对象的属性需要加引号才添加;)
quoteProps: "as-needed",
// 是否只格式化在文件顶部包含特定注释(@prettier| @format)的文件默认false
requirePragma: false,
// 结尾添加分号
semi: true,
// 使用单引号 (true:单引号;false:双引号)
singleQuote: false,
// 缩进空格数默认2个空格
tabWidth: 2,
// 元素末尾是否加逗号默认es5: ES5中的 objects, arrays 等会添加逗号TypeScript 中的 type 后不加逗号
trailingComma: "es5",
// 指定缩进方式空格或tab默认false即使用空格
useTabs: false,
// vue 文件中是否缩进 <style> 和 <script> 标签,默认 false
vueIndentScriptAndStyle: false,
endOfLine: "auto",
overrides: [
{
files: "*.html",
options: {
parser: "html",
},
},
],
};

11
.stylelintignore Normal file
View File

@ -0,0 +1,11 @@
dist
node_modules
public
.husky
.vscode
.idea
*.sh
*.md
src/assets
stats.html

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

16
LICENSE
View File

@ -1,9 +1,25 @@
MIT License
Copyright (c) 2024 Bunny
Copyright (c) 2025 Large-Visual-Screen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,4 +1,6 @@
# vehicle-monitor
![大数据可视化蓝色科技智慧车辆大屏后台统计2](public/大数据可视化蓝色科技智慧车辆大屏后台统计2.png)
智慧智能监管中心
车辆监控中心

51
build/buildEnv.ts Normal file
View File

@ -0,0 +1,51 @@
import type { BuildOptions } from "vite";
import { pathResolve } from "./utils";
export const buildEnv = (): BuildOptions => {
return {
target: "es2015",
assetsInlineLimit: 20000,
// 构建输出的目录,默认值为"dist"
outDir: "docker/dist",
// 用于指定使用的代码压缩工具。在这里minify 被设置为 'terser',表示使用 Terser 进行代码压缩。默认值terser
// esbuild 打包更快,但是不能去除 console.logterser打包慢但能去除 console.log
minify: "terser", // "esbuild"
// 用于配置 Terser 的选项
terserOptions: {
// 用于配置压缩选项
compress: {
drop_console: true, // 是否删除代码中的 console 语句, 默认值false
drop_debugger: true, // 是否删除代码中的 debugger 语句, 默认值false
},
},
// 禁用 gzip 压缩大小报告,可略微减少打包时间
reportCompressedSize: false,
// 用于指定是否生成源映射文件。源映射文件可以帮助调试和定位源代码中的错误。当设置为false时构建过程不会生成源映射文件
sourcemap: false,
// 用于配置 CommonJS 模块的选项
commonjsOptions: {
// 用于指定是否忽略 CommonJS 模块中的 try-catch 语句。当设置为false时构建过程会保留 CommonJS 模块中的 try-catch 语句
ignoreTryCatch: false,
},
// 规定触发警告的 chunk 大小, 当某个代码分块的大小超过该限制时Vite 会发出警告
chunkSizeWarningLimit: 2000,
rollupOptions: {
external: ["md-editor-v3", "echarts"],
input: {
index: pathResolve("../index.html", import.meta.url),
},
// 静态资源分类打包
output: {
chunkFileNames: "static/js/[name]-[hash].js",
entryFileNames: "static/js/[name]-[hash].js",
assetFileNames: "static/[ext]/[name]-[hash].[ext]",
manualChunks: (id) => {
// 如果是包含在包中则打包成 vendor
if (id.includes("node_modules")) {
return `vendor`;
}
},
},
},
};
};

67
build/cdn.ts Normal file
View File

@ -0,0 +1,67 @@
import { Plugin as importToCDN } from "vite-plugin-cdn-import";
import { wrapperEnv } from "./utils";
/**
* @description `cdn`使cdn模式 .env.production VITE_CDN true
* cdnhttps://www.bootcdn.cn当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com
* 使jscss文件cdn
*/
export const cdn = importToCDN({
//prodUrl解释 name: 对应下面modules的nameversion: 自动读取本地package.json中dependencies依赖中对应包的版本号path: 对应下面modules的path当然也可写完整路径会替换prodUrl
// prodUrl: 'https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}',
prodUrl: "https://unpkg.com/{name}@{version}/{path}",
modules: [
{
name: "vue",
var: "Vue",
path: "dist/vue.global.prod.js",
},
{
name: "vue-router",
var: "VueRouter",
path: "dist/vue-router.global.js",
},
// {
// name: 'vue-i18n',
// var: 'VueI18n',
// path: 'dist/vue-i18n.global.prod.js',
// },
// {
// name: 'vue-demi',
// var: 'VueDemi',
// path: 'lib/index.iife.js',
// },
{
name: "pinia",
var: "Pinia",
path: "dist/pinia.iife.js",
},
// {
// name: 'element-plus',
// var: 'ElementPlus',
// path: 'dist/index.full.js',
// css: 'dist/index.css',
// },
{
name: "axios",
var: "axios",
path: "dist/axios.min.js",
},
{
name: "dayjs",
var: "dayjs",
path: "dayjs.min.js",
},
{
name: "echarts",
var: "echarts",
path: "dist/echarts.min.js",
},
],
});
/* 是否使用CDN加速 */
export const useCDN = (mode) => {
const env = wrapperEnv(mode, "VITE");
return env.VITE_CDN ? cdn : null;
};

13
build/define.ts Normal file
View File

@ -0,0 +1,13 @@
import dayjs from "dayjs";
import { dependencies, devDependencies, engines, name, version } from "../package.json";
const __APP_INFO__ = {
pkg: { name, version, engines, dependencies, devDependencies },
lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss"),
};
export const define = () => {
return {
__APP_INFO__: JSON.stringify(__APP_INFO__),
};
};

55
build/info.ts Normal file
View File

@ -0,0 +1,55 @@
import { wrapperEnv } from "./utils";
import dayjs, { type Dayjs } from "dayjs";
import gradientString from "gradient-string";
import duration from "dayjs/plugin/duration";
import boxen, { type Options as BoxenOptions } from "boxen";
dayjs.extend(duration);
const boxenOptions: BoxenOptions = {
padding: 0.94,
borderColor: "cyan",
borderStyle: "round",
textAlignment: "left",
};
/* 输出日志信息 */
const printLogMessage = (VITE_PORT: number) => {
return gradientString("cyan", "magenta").multiline(
`保存成功!服务器重新启动
访
http://localhost:${VITE_PORT}`
);
};
export const viteConsoleLog = (mode) => {
const { VITE_PORT } = wrapperEnv(mode);
let config: { command: string };
let startTime: Dayjs;
let endTime: Dayjs;
return {
name: "vite:buildInfo",
configResolved(resolvedConfig) {
config = resolvedConfig;
},
buildStart() {
console.log(boxen(printLogMessage(VITE_PORT), boxenOptions));
if (config.command === "build") {
startTime = dayjs(new Date());
}
},
closeBundle() {
if (config.command === "build") {
endTime = dayjs(new Date());
const format = dayjs.duration(endTime.diff(startTime)).format("mm分ss秒");
console.log(
boxen(
gradientString("cyan", "magenta").multiline(`🎉 恭喜打包完成(总用时${format}`),
boxenOptions
)
);
}
},
};
};

14
build/optimize.ts Normal file
View File

@ -0,0 +1,14 @@
/**
* `vite.config.ts` `optimizeDeps.include`
* `vite` include esm node_modules/.vite
* include里vite 使 node_modules/.vite
* 使 src/main.ts include vite node_modules/.vite
*/
const include = ["vue", "vue-router", "dayjs", "axios", "pinia", "vue-types", "js-cookie"];
/**
*
*/
const exclude = [];
export { include, exclude };

44
build/plugins.ts Normal file
View File

@ -0,0 +1,44 @@
import vue from "@vitejs/plugin-vue";
import type { PluginOption } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import Inspector from "vite-plugin-vue-inspector";
import { compressPack, report } from "./utils";
import removeConsole from "vite-plugin-remove-console";
import { useCDN } from "./cdn";
import { viteConsoleLog } from "./info";
import UnoCSS from "unocss/vite";
import { presetIcons, presetUno } from "unocss";
import UnoCssIcons from "@unocss/preset-icons";
export const plugins = (mode): PluginOption[] => {
return [
vue(),
vueJsx(),
Inspector(),
report(),
removeConsole(),
useCDN(mode),
viteConsoleLog(mode),
UnoCSS({
hmrTopLevelAwait: false,
inspector: true, // 控制台是否打印 UnoCSS inspector
presets: [
presetIcons({
extraProperties: {
display: "inline-block",
"vertical-align": "middle",
},
}),
UnoCssIcons({
// 其他选项
prefix: "i-",
extraProperties: {
display: "inline-block",
},
}),
presetUno(),
],
}),
compressPack(mode),
];
};

10
build/resolve.ts Normal file
View File

@ -0,0 +1,10 @@
import { pathResolve } from "./utils";
export const resolve = () => {
return {
alias: {
"@": pathResolve("../src"),
"@build": pathResolve(),
},
};
};

33
build/server.ts Normal file
View File

@ -0,0 +1,33 @@
import type { ServerOptions } from "vite";
import { wrapperEnv } from "./utils";
/* 开发服务配置 */
export const server = (mode) => {
const { VITE_PORT, VITE_APP_URL, VITE_STRICT_PORT } = wrapperEnv(mode);
const options: ServerOptions = {
strictPort: VITE_STRICT_PORT,
port: VITE_PORT,
host: "0.0.0.0",
open: true,
cors: true,
proxy: {
"/api": {
target: VITE_APP_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/admin/, "/api"),
},
"/mock": {
target: VITE_APP_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/mock/, "/mock"),
},
},
// 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布
warmup: {
clientFiles: ["./index.html", "./src/{views,components}/*"],
},
};
return options;
};

66
build/utils.ts Normal file
View File

@ -0,0 +1,66 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { loadEnv } from "vite";
import { visualizer } from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
export const root: string = process.cwd();
/**
* @description
* @param dir `build`
* @param metaUrl `url``build``import.meta.url`
*/
export const pathResolve = (dir = ".", metaUrl = import.meta.url) => {
// 当前文件目录的绝对路径
const currentFileDir = dirname(fileURLToPath(metaUrl));
// build 目录的绝对路径
const buildDir = resolve(currentFileDir, "build");
// 解析的绝对路径
const resolvedPath = resolve(currentFileDir, dir);
// 检查解析的绝对路径是否在 build 目录内
if (resolvedPath.startsWith(buildDir)) {
// 在 build 目录内,返回当前文件路径
return fileURLToPath(metaUrl);
}
// 不在 build 目录内,返回解析后的绝对路径
return resolvedPath;
};
/**
*
* @param mode
* @param prefix
* @link https://cn.vite.dev/config/#using-environment-variables-in-config
*/
export const wrapperEnv = (mode, prefix = ""): ViteEnv => {
const env = loadEnv(mode, root, prefix);
// 将变量转换指定类型
for (const envName of Object.keys(env)) {
let realName = env[envName].replace(/\\n/g, "\n");
realName = realName === "true" ? true : realName === "false" ? false : realName;
if (envName === "VITE_PORT") {
realName = Number(realName);
}
env[envName] = realName;
process.env[envName] = realName;
}
return env;
};
/* 打包分析 */
export const report = () => {
const lifecycle = process.env.npm_lifecycle_event;
return lifecycle === "report"
? visualizer({ open: true, brotliSize: true, filename: "report.html" })
: (null as any);
};
/* 启用gzip压缩 */
export const compressPack = (mode) => {
const { VITE_COMPRESSION } = wrapperEnv(mode);
return VITE_COMPRESSION == "gzip" ? viteCompression({ threshold: 1024000 }) : null;
};

31
docker/Dockerfile Normal file
View File

@ -0,0 +1,31 @@
# 使用官方的 Nginx 镜像作为基础镜像
FROM nginx:1.27.3
# 删除默认的 Nginx 配置文件
RUN rm /etc/nginx/conf.d/default.conf
# 将自定义的 Nginx 配置文件复制到容器中
COPY nginx.conf /etc/nginx/conf.d/default.conf
#COPY bunny-web.site.csr /etc/nginx/bunny-web.site.csr
#COPY bunny-web.site.key /etc/nginx/bunny-web.site.key
#COPY bunny-web.site_bundle.crt /etc/nginx/bunny-web.site_bundle.crt
#COPY bunny-web.site_bundle.pem /etc/nginx/bunny-web.site_bundle.pem
# 设置时区,构建镜像时执行的命令
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" > /etc/timezone
# 创建一个目录来存放前端项目文件
WORKDIR /usr/share/nginx/html
# 将前端项目打包文件复制到 Nginx 的默认静态文件目录
COPY dist/ /usr/share/nginx/html
# 复制到nginx目录下
COPY dist/ /etc/nginx/html
# 暴露 Nginx 的默认端口
EXPOSE 80
#EXPOSE 443
# 自动启动 Nginx
CMD ["nginx", "-g", "daemon off;"]

32
docker/nginx.conf Normal file
View File

@ -0,0 +1,32 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80 ;
listen [::]:80;
server_name localhost;
client_max_body_size 5M; # 最大文件上传设置
location / {
root /etc/nginx/html;
index index.html index.htm;
try_files $uri /index.html;
}
# 后端跨域请求
location ~/api/ {
proxy_pass http://172.17.0.1:8000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 404 404.html;
location = /50x.html {
root html;
}
}

174
eslint.config.js Normal file
View File

@ -0,0 +1,174 @@
import js from "@eslint/js";
import pluginVue from "eslint-plugin-vue";
import * as parserVue from "vue-eslint-parser";
import configPrettier from "eslint-config-prettier";
import pluginPrettier from "eslint-plugin-prettier";
import { defineFlatConfig } from "eslint-define-config";
import * as parserTypeScript from "@typescript-eslint/parser";
import pluginTypeScript from "@typescript-eslint/eslint-plugin";
export default defineFlatConfig([
{
...js.configs.recommended,
ignores: ["**/.*", "dist/*", "*.d.ts", "public/*", "src/assets/**", "src/**/iconfont/**"],
languageOptions: {
globals: {
// index.d.ts
RefType: "readonly",
EmitType: "readonly",
TargetContext: "readonly",
ComponentRef: "readonly",
ElRef: "readonly",
ForDataType: "readonly",
AnyFunction: "readonly",
PropType: "readonly",
Writable: "readonly",
Nullable: "readonly",
NonNullable: "readonly",
Recordable: "readonly",
ReadonlyRecordable: "readonly",
Indexable: "readonly",
DeepPartial: "readonly",
Without: "readonly",
Exclusive: "readonly",
TimeoutHandle: "readonly",
IntervalHandle: "readonly",
Effect: "readonly",
ChangeEvent: "readonly",
WheelEvent: "readonly",
ImportMetaEnv: "readonly",
Fn: "readonly",
PromiseFn: "readonly",
ComponentElRef: "readonly",
parseInt: "readonly",
parseFloat: "readonly"
}
},
plugins: {
prettier: pluginPrettier
},
rules: {
...configPrettier.rules,
...pluginPrettier.configs.recommended.rules,
"no-debugger": "off",
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
"prettier/prettier": [
"error",
{
endOfLine: "auto"
}
]
}
},
{
files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"],
languageOptions: {
parser: parserTypeScript,
parserOptions: {
sourceType: "module"
}
},
plugins: {
"@typescript-eslint": pluginTypeScript
},
rules: {
...pluginTypeScript.configs.strict.rules,
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-redeclare": "error",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/prefer-as-const": "warn",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/prefer-literal-enum-member": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/consistent-type-imports": [
"error",
{
disallowTypeAnnotations: false,
fixStyle: "inline-type-imports"
}
],
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
]
}
},
{
files: ["**/*.d.ts"],
rules: {
"eslint-comments/no-unlimited-disable": "off",
"import/no-duplicates": "off",
"unused-imports/no-unused-vars": "off"
}
},
{
files: ["**/*.?([cm])js"],
rules: {
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-var-requires": "off"
}
},
{
files: ["**/*.vue"],
languageOptions: {
globals: {
$: "readonly",
$$: "readonly",
$computed: "readonly",
$customRef: "readonly",
$ref: "readonly",
$shallowRef: "readonly",
$toRef: "readonly"
},
parser: parserVue,
parserOptions: {
ecmaFeatures: {
jsx: true
},
extraFileExtensions: [".vue"],
parser: "@typescript-eslint/parser",
sourceType: "module"
}
},
plugins: {
vue: pluginVue
},
processor: pluginVue.processors[".vue"],
rules: {
...pluginVue.configs.base.rules,
...pluginVue.configs["vue3-essential"].rules,
...pluginVue.configs["vue3-recommended"].rules,
"no-undef": "off",
"no-unused-vars": "off",
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/multi-word-component-names": "off",
"vue/no-setup-props-reactivity-loss": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always"
},
svg: "always",
math: "always"
}
]
}
}
]);

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link href="/vite.svg" rel="icon" type="image/svg+xml"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>%VITE_APP_TITLE%</title>
</head>
<body>
<div id="app"></div>
<script src="/src/main.ts" type="module"></script>
</body>
</html>

86
package.json Normal file
View File

@ -0,0 +1,86 @@
{
"name": "vehicle-monitor",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"report": "rimraf dist && vite build",
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint"
},
"dependencies": {
"@eslint/js": "^9.21.0",
"@typescript-eslint/eslint-plugin": "^8.24.1",
"@typescript-eslint/parser": "^8.24.1",
"@unocss/preset-icons": "^66.0.0",
"@unocss/reset": "^66.0.0",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"animate.css": "^4.1.1",
"axios": "^1.7.9",
"boxen": "^8.0.1",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"eslint": "^9.21.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.27.0",
"gradient-string": "^3.0.0",
"js-cookie": "^3.0.5",
"list": "^2.0.19",
"nprogress": "^0.2.0",
"pinia": "^2.3.1",
"pinia-plugin-persistedstate": "^3.2.3",
"postcss": "^8.5.3",
"postcss-loader": "^8.1.1",
"postcss-px-to-viewport-8-plugin": "^1.2.5",
"prettier": "^3.3.3",
"rimraf": "^5.0.10",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.77.8",
"stylelint": "^16.14.1",
"stylelint-config-recess-order": "^6.0.0",
"stylelint-config-recommended-vue": "^1.6.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-prettier": "^5.0.3",
"terser": "^5.39.0",
"unocss": "^66.0.0",
"vite-plugin-cdn-import": "^1.0.1",
"vite-plugin-remove-console": "^2.2.0",
"vite-plugin-vue-inspector": "^5.3.1",
"vue": "^3.5.13",
"vue-eslint-parser": "^9.4.3",
"vue-router": "^4.4.3",
"vue-types": "^6.0.0"
},
"devDependencies": {
"@iconify/json": "^2.2.310",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"typescript": "~5.7.2",
"vite": "^6.1.0",
"vite-plugin-compression": "^0.5.1",
"vue-tsc": "^2.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
"pnpm": ">=8.6.10"
},
"pnpm": {
"allowedDeprecatedVersions": {
"sourcemap-codec": "*",
"domexception": "*",
"w3c-hr-time": "*",
"stable": "*",
"abab": "*"
},
"peerDependencyRules": {
"allowedVersions": {
"eslint": "9"
}
}
}
}

5046
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.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><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.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.028l72.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"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

10
src/App.vue Normal file
View File

@ -0,0 +1,10 @@
<template>
<router-view />
</template>
<style>
#app {
width: 100%;
height: 1080px;
}
</style>

BIN
src/assets/images/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
src/assets/images/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

6
src/directive/index.ts Normal file
View File

@ -0,0 +1,6 @@
import type { App } from 'vue';
// 全局注册 directive
export function setupDirective(app: App<Element>) {
// 使 v-hasPerm 在所有组件中都可用
}

View File

@ -0,0 +1,38 @@
<script lang="ts" setup></script>
<template>
<main class="mt-[78px] mx-auto flex-center w-[1620px] h-[650px]">
<div class="left mr-[28px]">左边</div>
<div class="middle">中间</div>
<div class="right ml-[28px]">右边</div>
</main>
</template>
<style lang="scss" scoped>
.layout-main {
margin: 0 auto;
}
.left,
.right {
width: 380px;
height: 650px;
background: url("@/assets/images/bg-side.png") no-repeat center;
background-size: cover;
transition: transform 0.8s;
transform-style: preserve-3d;
}
.left {
transform: rotateY(180deg);
}
.middle {
width: 824px;
height: 650px;
background: url("@/assets/images/bg-middle.png") no-repeat center;
background-size: cover;
}
</style>

View File

@ -0,0 +1,25 @@
<script lang="ts" setup></script>
<template>
<footer class="mt-[66px] mx-auto w-[772px] h-[125px]">
<ul class="flex-x-around">
<li
v-for="index in new Array(5)"
:key="index"
class="rectangle w-[138px] h-[125px] flex-y-center bg-[#0E094D55]"
>
<img alt="车辆管理" src="@/assets/images/footer-39.png" />
<span class="c-white font-100">车辆管理</span>
</li>
</ul>
</footer>
</template>
<style lang="scss" scoped>
.rectangle {
img {
width: 67px;
height: 67px;
}
}
</style>

View File

@ -0,0 +1,54 @@
<script lang="ts" setup></script>
<template>
<header class="h-[105px]">
<div class="time ml-[41px] c-[#027AFF]">2025年2月25日22:45:14</div>
<div class="title">
<h1 class="c-white text-align-center">智慧智能监管中心</h1>
<h2 class="text-align-center">车辆监控中心</h2>
</div>
<div class="bar-op flex items-center h-[36px]">
<ul class="float-left flex-x-around w-[148px]">
<li><img alt="icon-1" src="@/assets/images/icon-1.png" /></li>
<li><img alt="icon-2" src="@/assets/images/icon-2.png" /></li>
<li><img alt="icon-3" src="@/assets/images/icon-3.png" /></li>
</ul>
<span class="float-left c-[#027AFF]">王菠萝</span>
</div>
</header>
</template>
<style lang="scss" scoped>
header {
position: relative;
background: url("@/assets/images/bg-header.png") no-repeat center;
background-size: cover;
}
.time {
position: absolute;
line-height: 60px;
font-size: 14px;
}
.title {
h1 {
font-size: 42px;
line-height: 60px;
}
h2 {
font-size: 24px;
line-height: 45px;
color: #00ffff;
}
}
.bar-op {
position: absolute;
top: 11px;
right: 20px;
}
</style>

22
src/layout/index.vue Normal file
View File

@ -0,0 +1,22 @@
<script lang="ts" setup>
import AppMain from "@/layout/components/AppMain/index.vue";
import NavBar from "@/layout/components/NavBar/index.vue";
import Footer from "@/layout/components/Footer/index.vue";
</script>
<template>
<div class="layout w-full h-full">
<NavBar />
<AppMain />
<Footer />
</div>
</template>
<style scoped>
.layout {
width: 100%;
height: 100%;
background: url("@/assets/images/bg.png") no-repeat center;
background-size: cover;
}
</style>

9
src/main.ts Normal file
View File

@ -0,0 +1,9 @@
import { createApp } from 'vue';
import 'animate.css';
import App from './App.vue';
import plugins from '@/plugins';
import '@unocss/reset/tailwind-compat.css';
import 'uno.css';
import 'virtual:unocss-devtools';
createApp(App).use(plugins).mount('#app');

15
src/plugins/index.ts Normal file
View File

@ -0,0 +1,15 @@
import type { App } from 'vue';
import { setupDirective } from '@/directive';
import { setUpRouter } from '@/router';
import { setupStore } from '@/store';
export default {
install(app: App<Element>) {
// 设置路由
setUpRouter(app);
// 设置状态管理
setupStore(app);
// 设置指令
setupDirective(app);
},
};

61
src/router/index.ts Normal file
View File

@ -0,0 +1,61 @@
import type { App } from 'vue';
import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router';
export const Layout = () => import('@/layout/index.vue');
// 静态路由
const routes: RouteRecordRaw[] = [
{
path: '/redirect',
component: Layout,
meta: { hidden: true },
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index.vue'),
},
],
},
{
path: '/',
name: '/',
component: Layout,
// redirect: '/dashboard',
children: [
// {
// path: 'dashboard',
// component: () => import('@/views/index.vue'),
// // 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致
// // 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude
// name: 'Dashboard',
// meta: {
// title: 'dashboard',
// icon: 'homepage',
// affix: true,
// keepAlive: true,
// },
// },
],
},
{
path: '/404',
component: () => import('@/views/error-page/404.vue'),
meta: { hidden: true },
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior: () => ({ x: 0, y: 0 }),
});
/** 全局注册 router */
export const setUpRouter = (app: App<Element>) => {
app.use(router);
};
/** 重置路由 */
export const resetRouter = () => {
router.replace({ path: '/' }).then();
};
export default router;

9
src/store/index.ts Normal file
View File

@ -0,0 +1,9 @@
import type { App } from 'vue';
import { createPinia } from 'pinia';
const store = createPinia();
// 全局注册 store
export function setupStore(app: App<Element>) {
app.use(store);
}

12
src/types/global.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
declare global {
/* 环境便配置 */
declare interface ViteEnv {
VITE_APP_TITLE: string;
VITE_PORT: number;
VITE_PUBLIC_PATH: string;
VITE_APP_URL: string;
VITE_STRICT_PORT: boolean;
VITE_CDN: boolean;
VITE_COMPRESSION: string;
}
}

1
src/types/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
type Recordable<T = any> = Record<string, T>;

1
src/types/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

18
src/utils/nprogress.ts Normal file
View File

@ -0,0 +1,18 @@
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
// 进度条
NProgress.configure({
// 动画方式
easing: 'ease',
// 递增进度条的速度
speed: 500,
// 是否显示加载ico
showSpinner: false,
// 自动递增间隔
trickleSpeed: 200,
// 初始化时的最小百分比
minimum: 0.3,
});
export default NProgress;

71
src/utils/request.ts Normal file
View File

@ -0,0 +1,71 @@
import axios, { type AxiosResponse, type InternalAxiosRequestConfig } from 'axios';
import { useUserStoreHook } from '@/store/modules/user';
import { ResultEnum } from '@/enums/ResultEnum';
import { TOKEN_KEY } from '@/enums/CacheEnum';
import qs from 'qs';
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
headers: { 'Content-Type': 'application/json;charset=utf-8' },
paramsSerializer: params => {
return qs.stringify(params);
},
});
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = localStorage.getItem(TOKEN_KEY);
if (accessToken) {
config.headers.Authorization = accessToken;
}
return config;
},
(error: any) => {
return Promise.reject(error);
},
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
// 检查配置的响应类型是否为二进制类型('blob' 或 'arraybuffer', 如果是,直接返回响应对象
if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') {
return response;
}
const { code, data, msg } = response.data;
if (code === ResultEnum.SUCCESS) {
return data;
}
ElMessage.error(msg || '系统出错');
return Promise.reject(new Error(msg || 'Error'));
},
(error: any) => {
// 异常处理
if (error.response.data) {
const { code, msg } = error.response.data;
if (code === ResultEnum.TOKEN_INVALID) {
ElNotification({
title: '提示',
message: '您的会话已过期,请重新登录',
type: 'info',
});
useUserStoreHook()
.resetToken()
.then(() => {
location.reload();
});
} else {
ElMessage.error(msg || '系统出错');
}
}
return Promise.reject(error.message);
},
);
// 导出 axios 实例
export default service;

View File

@ -0,0 +1,237 @@
<script lang="ts" setup>
import { useRouter } from "vue-router";
const router = useRouter();
</script>
<template>
<div class="page-container">
<div class="pic-404">
<img alt="404" class="pic-404__parent" src="@/assets/images/404.png" />
<img alt="404" class="pic-404__child left" src="@/assets/images/404_cloud.png" />
<img alt="404" class="pic-404__child mid" src="@/assets/images/404_cloud.png" />
<img alt="404" class="pic-404__child right" src="@/assets/images/404_cloud.png" />
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__info">
All rights reserved
<a href="https://wallstreetcn.com" style="color: #20a0ff" target="_blank">wallstreetcn</a>
</div>
<div class="bullshit__headline">The webmaster said that you can not enter this page...</div>
<div class="bullshit__info">
Please check that the URL you entered is correct, or click the button below to return to the
homepage.
</div>
<a class="bullshit__return-home" href="/" @click.prevent="router.replace('/')">
Back to home
</a>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-container {
display: flex;
padding: 100px;
.pic-404 {
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
&.left {
top: 17px;
left: 220px;
width: 80px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-delay: 1s;
animation-fill-mode: forwards;
}
&.mid {
top: 10px;
left: 420px;
width: 46px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-delay: 1.2s;
animation-fill-mode: forwards;
}
&.right {
top: 100px;
left: 500px;
width: 62px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-delay: 1s;
animation-fill-mode: forwards;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
margin-bottom: 20px;
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
margin-bottom: 10px;
font-size: 20px;
font-weight: bold;
line-height: 24px;
color: #222;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
margin-bottom: 30px;
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
font-size: 14px;
line-height: 36px;
color: #fff;
text-align: center;
cursor: pointer;
background: #1482f0;
border-radius: 100px;
opacity: 0;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(60px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
}
}
</style>

5
src/views/index.vue Normal file
View File

@ -0,0 +1,5 @@
<script lang="ts" setup></script>
<template>初始化页面</template>
<style scoped></style>

View File

@ -0,0 +1,15 @@
<template>
<div />
</template>
<script lang="ts" setup>
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const { params, query } = route;
const { path } = params;
router.replace({ path: "/" + path, query });
</script>

51
stylelint.config.js Normal file
View File

@ -0,0 +1,51 @@
module.exports = {
// 继承推荐规范配置
extends: [
'stylelint-config-standard',
'stylelint-config-recommended-scss',
'stylelint-config-recommended-vue/scss',
'stylelint-config-html/vue',
'stylelint-config-recess-order',
],
// 指定不同文件对应的解析器
overrides: [
{
files: ['**/*.{vue,html}'],
customSyntax: 'postcss-html',
},
{
files: ['**/*.{css,scss}'],
customSyntax: 'postcss-scss',
},
],
// 自定义规则
rules: {
'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url")
'selector-class-pattern': null, // 选择器类名命名规则
'custom-property-pattern': null, // 自定义属性命名规则
'keyframes-name-pattern': null, // 动画帧节点样式命名规则
'no-descending-specificity': null, // 允许无降序特异性
'no-empty-source': null, // 允许空样式
// 允许 global 、export 、deep伪类
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global', 'export', 'deep'],
},
],
// 允许未知属性
'property-no-unknown': [
true,
{
ignoreProperties: [],
},
],
// 允许未知规则
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['apply', 'use', 'forward'],
},
],
},
};

49
tsconfig.json Normal file
View File

@ -0,0 +1,49 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"noLib": false,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom"
],
"baseUrl": ".",
"allowJs": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"paths": {
"@/*": [
"src/*"
]
},
"types": [
"vite/client",
"unplugin-icons/types/vue",
"element-plus/global"
]
},
"files": [],
"include": [
"mock/*.ts",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/types/*.d.ts",
"vite.config.ts"
],
"exclude": [
"dist",
"**/*.js",
"node_modules"
]
}

36
uno.config.ts Normal file
View File

@ -0,0 +1,36 @@
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetUno,
presetWebFonts,
transformerDirectives,
transformerVariantGroup,
} from "unocss";
export default defineConfig({
shortcuts: {
"flex-center": "flex justify-center items-center",
"flex-x-between": "flex items-center justify-between",
"flex-x-around": "flex items-center justify-around",
"flex-y-center": "flex flex-col flex-wrap justify-center items-center",
},
theme: {
colors: {
// ...
},
},
presets: [
presetUno(),
presetAttributify(),
presetIcons(),
presetTypography(),
presetWebFonts({
fonts: {
// ...
},
}),
],
transformers: [transformerDirectives(), transformerVariantGroup()],
});

57
vite.config.ts Normal file
View File

@ -0,0 +1,57 @@
import { defineConfig } from "vite";
import { plugins } from "./build/plugins";
import { resolve } from "./build/resolve";
import { buildEnv } from "./build/buildEnv";
import { define } from "./build/define";
import { root, wrapperEnv } from "./build/utils";
import { server } from "./build/server";
import { exclude, include } from "./build/optimize";
import postCssPxToViewport8plugin from "postcss-px-to-viewport-8-plugin";
// https://vite.dev/config/
export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => {
const env = wrapperEnv(mode, "VITE");
return {
root,
base: env.VITE_PUBLIC_PATH,
define: define(),
plugins: plugins(mode),
resolve: resolve(),
esbuild: {
jsxFactory: "h",
jsxFragment: "Fragment",
jsxInject: "import { h } from 'vue';",
},
css: {
postcss: {
plugins: [
postCssPxToViewport8plugin({
unitToConvert: "px",
viewportWidth: 1920, // 设计稿的宽度
unitPrecision: 5, // 单位转换后保留的精度
propList: ["*"], // 能转化为vw的属性列表
viewportUnit: "vw", // 希望使用的视口单位
fontViewportUnit: "vw", // 字体使用的视口单位
selectorBlackList: [], // 需要忽略的CSS选择器不会转为视口单位使用原有的px等单位。
minPixelValue: 1, // 设置最小的转换数值如果为1的话只有大于1的值会被转换
mediaQuery: true, // 媒体查询里的单位是否需要转换单位
replace: true, // 是否直接更换属性值,而不添加备用属性
exclude: [/node_modules/], // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
include: [], // 如果设置了include那将只有匹配到的文件才会被转换
landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
landscapeUnit: "vw", // 横屏时使用的单位
landscapeWidth: 1024, // 横屏时使用的视口宽度
}),
],
},
},
logLevel: "info",
// 设为 false 可以避免 Vite 清屏而错过在终端中打印某些关键信息
clearScreen: false,
build: buildEnv(),
server: server(mode),
preview: server(mode),
optimizeDeps: { include, exclude },
};
});