第一次

This commit is contained in:
TimSpan 2025-04-25 09:17:25 +08:00
commit 89c09e5a6b
76 changed files with 9564 additions and 0 deletions

12
.env.development Normal file
View File

@ -0,0 +1,12 @@
VITE_APP_ENV=development
VITE_APP_PORT=9527
VITE_DROP_CONSOLE=false
# axios
VITE_APP_BASE_API=/api
VITE_APP_PROXY_URL=http://172.10.10.93:1233
#crypto js 前后端需保持一致
# VITE_APP_CRYPTO_JS_SECRET_KEY=f0234d57c311beb2
# VITE_APP_CRYPTO_JS_SECRET_IV=eb7905b31669ad1e
VITE_APP_CRYPTO_JS_SECRET_KEY=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCo58dwFmtTkiA4bj/FiBqlYsiKOugT/WCQRYFPY49A7y6VEswzMihgxSz0B/UDdekM9PDsKy06Gjy8BpWu+ikR1ms+/PbMJOmw5jGUeiswf8uqJDDquHG5oJJk0o7J8/1JvzmpbN/Ctjcm2yUscTfG2WvvY0ViwnptYU7+ZkRcuQIDAQAB

12
.env.production Normal file
View File

@ -0,0 +1,12 @@
VITE_APP_ENV=production
VITE_APP_PORT=9528
VITE_DROP_CONSOLE=true
# axios
VITE_APP_BASE_API=/api
VITE_APP_PROXY_URL=https://localhost:8083
#crypto js 前后端需保持一致
# VITE_APP_CRYPTO_JS_SECRET_KEY=f0234d57c311beb2
# VITE_APP_CRYPTO_JS_SECRET_IV=eb7905b31669ad1e
VITE_APP_CRYPTO_JS_SECRET_KEY=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCo58dwFmtTkiA4bj/FiBqlYsiKOugT/WCQRYFPY49A7y6VEswzMihgxSz0B/UDdekM9PDsKy06Gjy8BpWu+ikR1ms+/PbMJOmw5jGUeiswf8uqJDDquHG5oJJk0o7J8/1JvzmpbN/Ctjcm2yUscTfG2WvvY0ViwnptYU7+ZkRcuQIDAQAB

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?

18
README.md Normal file
View File

@ -0,0 +1,18 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support For `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

13
index.html Normal file
View File

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

2815
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --mode development",
"pro": "vite --mode production",
"build": "vite build --mode production",
"preview": "vite preview"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@types/node": "^20.5.0",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"ant-design-vue": "4.x",
"axios": "^1.4.0",
"crypto-js": "^4.1.1",
"echarts": "^5.4.3",
"jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",
"pinia-plugin-persistedstate": "^3.2.0",
"terser": "^5.19.2",
"vue": "^3.3.4",
"vue-router": "4"
},
"devDependencies": {
"@types/crypto-js": "^4.1.1",
"@types/lodash-es": "^4.17.12",
"@types/nprogress": "^0.2.3",
"@vitejs/plugin-vue": "^4.2.3",
"less": "^4.2.0",
"sass": "^1.65.1",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vue-tsc": "^1.8.5"
}
}

2099
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

13
src/App.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<!-- <a-config-provider> -->
<router-view></router-view>
<!-- </a-config-provider> -->
</template>
<script setup lang="ts"></script>
<style lang="scss">
body {
font-family: AliBaBaPuHuTi, serif;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

4
src/assets/css/font.css Normal file
View File

@ -0,0 +1,4 @@
@font-face {
font-family: AliBaBaPuHuTi;
src: url('../font/AlibabaPuHuiTi-2-65-Medium.ttf');
}

53
src/assets/css/reset.css Normal file
View File

@ -0,0 +1,53 @@
/* http://meyerweb.com/eric/tools/css/reset/ */
/* v1.0 | 20080212 */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
/* remember to define focus styles! */
:focus {
outline: 0;
}
/* remember to highlight inserts somehow! */
ins {
text-decoration: none;
}
del {
text-decoration: line-through;
}
/* tables still need 'cellspacing="0"' in the markup */
table {
border-collapse: collapse;
border-spacing: 0;
}

Binary file not shown.

File diff suppressed because one or more lines are too long

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

431
src/assets/scss/common.scss Normal file
View File

@ -0,0 +1,431 @@
/* flex */
.flx-center {
display: flex;
align-items: center;
justify-content: center;
}
.flx-justify-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flx-align-center {
display: flex;
align-items: center;
}
/* clearfix */
.clearfix::after {
display: block;
height: 0;
overflow: hidden;
clear: both;
content: "";
}
/* 文字单行省略号 */
.sle {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 文字多行省略号 */
.mle {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
/* 文字多了自动換行 */
.break-word {
word-break: break-all;
word-wrap: break-word;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.2s;
}
.fade-transform-enter-from {
opacity: 0;
transition: all 0.2s;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transition: all 0.2s;
transform: translateX(30px);
}
/* breadcrumb-transform */
.breadcrumb-enter-active {
transition: all 0.2s;
}
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(10px);
}
/* scroll bar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background-color: var(--el-border-color-darker);
border-radius: 20px;
}
/* nprogress */
#nprogress .bar {
background: var(--el-color-primary) !important;
}
#nprogress .spinner-icon {
border-top-color: var(--el-color-primary) !important;
border-left-color: var(--el-color-primary) !important;
}
#nprogress .peg {
box-shadow: 0 0 10px var(--el-color-primary), 0 0 5px var(--el-color-primary) !important;
}
/* 外边距、内边距全局样式 */
@for $i from 0 through 40 {
.mt#{$i} {
margin-top: #{$i}px !important;
}
.mr#{$i} {
margin-right: #{$i}px !important;
}
.mb#{$i} {
margin-bottom: #{$i}px !important;
}
.ml#{$i} {
margin-left: #{$i}px !important;
}
.pt#{$i} {
padding-top: #{$i}px !important;
}
.pr#{$i} {
padding-right: #{$i}px !important;
}
.pb#{$i} {
padding-bottom: #{$i}px !important;
}
.pl#{$i} {
padding-left: #{$i}px !important;
}
}
/* -- 内外边距 -- */
.margin-0 {
margin: 0;
}
.margin-xs {
margin: 5px;
}
.margin-sm {
margin: 10px;
}
.margin {
margin: 15px;
}
.margin-lg {
margin: 20px;
}
.margin-xl {
margin: 25px;
}
.margin-top-xs {
margin-top: 5px;
}
.margin-top-sm {
margin-top: 10px;
}
.margin-top {
margin-top: 15px;
}
.margin-top-lg {
margin-top: 20px;
}
.margin-top-xl {
margin-top: 25px;
}
.margin-right-xs {
margin-right: 5px;
}
.margin-right-sm {
margin-right: 10px;
}
.margin-right {
margin-right: 15px;
}
.margin-right-lg {
margin-right: 20px;
}
.margin-right-xl {
margin-right: 25px;
}
.margin-bottom-xs {
margin-bottom: 5px;
}
.margin-bottom-sm {
margin-bottom: 10px;
}
.margin-bottom {
margin-bottom: 15px;
}
.margin-bottom-lg {
margin-bottom: 20px;
}
.margin-bottom-xl {
margin-bottom: 25px;
}
.margin-left-xs {
margin-left: 5px;
}
.margin-left-sm {
margin-left: 10px;
}
.margin-left {
margin-left: 15px;
}
.margin-left-lg {
margin-left: 20px;
}
.margin-left-xl {
margin-left: 25px;
}
.margin-lr-xs {
margin-left: 5px;
margin-right: 5px;
}
.margin-lr-sm {
margin-left: 10px;
margin-right: 10px;
}
.margin-lr {
margin-left: 15px;
margin-right: 15px;
}
.margin-lr-lg {
margin-left: 20px;
margin-right: 20px;
}
.margin-lr-xl {
margin-left: 25px;
margin-right: 25px;
}
.margin-tb-xs {
margin-top: 5px;
margin-bottom: 5px;
}
.margin-tb-sm {
margin-top: 10px;
margin-bottom: 10px;
}
.margin-tb {
margin-top: 15px;
margin-bottom: 15px;
}
.margin-tb-lg {
margin-top: 20px;
margin-bottom: 20px;
}
.margin-tb-xl {
margin-top: 25px;
margin-bottom: 25px;
}
.padding-0 {
padding: 0;
}
.padding-xs {
padding: 5px;
}
.padding-sm {
padding: 10px;
}
.padding {
padding: 15px;
}
.padding-lg {
padding: 20px;
}
.padding-xl {
padding: 25px;
}
.padding-top-xs {
padding-top: 5px;
}
.padding-top-sm {
padding-top: 10px;
}
.padding-top {
padding-top: 15px;
}
.padding-top-lg {
padding-top: 20px;
}
.padding-top-xl {
padding-top: 25px;
}
.padding-right-xs {
padding-right: 5px;
}
.padding-right-sm {
padding-right: 10px;
}
.padding-right {
padding-right: 15px;
}
.padding-right-lg {
padding-right: 20px;
}
.padding-right-xl {
padding-right: 25px;
}
.padding-bottom-xs {
padding-bottom: 5px;
}
.padding-bottom-sm {
padding-bottom: 10px;
}
.padding-bottom {
padding-bottom: 15px;
}
.padding-bottom-lg {
padding-bottom: 20px;
}
.padding-bottom-xl {
padding-bottom: 25px;
}
.padding-left-xs {
padding-left: 5px;
}
.padding-left-sm {
padding-left: 10px;
}
.padding-left {
padding-left: 15px;
}
.padding-left-lg {
padding-left: 20px;
}
.padding-left-xl {
padding-left: 25px;
}
.padding-lr-xs {
padding-left: 5px;
padding-right: 5px;
}
.padding-lr-sm {
padding-left: 10px;
padding-right: 10px;
}
.padding-lr {
padding-left: 15px;
padding-right: 15px;
}
.padding-lr-lg {
padding-left: 20px;
padding-right: 20px;
}
.padding-lr-xl {
padding-left: 25px;
padding-right: 25px;
}
.padding-tb-xs {
padding-top: 5px;
padding-bottom: 5px;
}
.padding-tb-sm {
padding-top: 10px;
padding-bottom: 10px;
}
.padding-tb {
padding-top: 15px;
padding-bottom: 15px;
}
.padding-tb-lg {
padding-top: 20px;
padding-bottom: 20px;
}
.padding-tb-xl {
padding-top: 25px;
padding-bottom: 25px;
}

View File

@ -0,0 +1,4 @@
@font-face {
font-family: AliBaBaPuHuTi;
src: url('../font/AlibabaPuHuiTi-2-65-Medium.ttf');
}

View File

@ -0,0 +1,4 @@
@mixin wh($w, $h) {
width: $w;
height: $h;
}

View File

@ -0,0 +1,53 @@
/* http://meyerweb.com/eric/tools/css/reset/ */
/* v1.0 | 20080212 */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
/* remember to define focus styles! */
:focus {
outline: 0;
}
/* remember to highlight inserts somehow! */
ins {
text-decoration: none;
}
del {
text-decoration: line-through;
}
/* tables still need 'cellspacing="0"' in the markup */
table {
border-collapse: collapse;
border-spacing: 0;
}

2
src/assets/scss/var.scss Normal file
View File

@ -0,0 +1,2 @@
/* global css variable */
$primary-color: var(--el-color-primary);

View File

@ -0,0 +1,2 @@
/* 存放css变量 */
$primary-color: red;

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

104
src/axios/index.ts Normal file
View File

@ -0,0 +1,104 @@
import axios, {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from "axios";
import { message } from "ant-design-vue";
import { useUserStore } from "@/stores/modules/userStore";
import { CLIENT_TYPE } from "@/config/constant";
export interface JsonResult<T> {
code: number;
message: string;
data?: T;
}
const axiosConfig: AxiosRequestConfig = {
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: import.meta.env.VITE_APP_ENV === "production" ? 10000 : 60000,
timeoutErrorMessage: "请求超时......",
};
class RequestHttp {
service: AxiosInstance;
public constructor(config: AxiosRequestConfig) {
this.service = axios.create(config);
//1.添加请求拦截
this.service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
//默认带上用户token
config.headers.set("token", useUserStore().userInfo?.tokenValue);
config.headers.set("Client-Type", CLIENT_TYPE);
return config;
},
(error: AxiosError): Promise<string> => {
message.error(error.message).then((r) => {});
return Promise.reject(error);
}
);
//2.添加响应拦截
this.service.interceptors.response.use(
(response: AxiosResponse): Promise<any> => {
const jsonResult: JsonResult<unknown> = response.data;
if (jsonResult.code !== 200) {
//todo 一些特定的错误需要重新登录 这里暂时没处理
message.error(jsonResult.message).then((r) => {});
return Promise.reject(jsonResult);
}
return Promise.resolve(jsonResult);
},
(error: AxiosError): Promise<string> => {
message.error(error.message).then((r) => {});
return Promise.reject(error);
}
);
}
/**
*
*/
get<T>(
url: string,
params?: object,
_object: AxiosRequestConfig = {}
): Promise<JsonResult<T>> {
return this.service.get(url, { params, ..._object });
}
post<T>(
url: string,
params?: object | object[],
_object: AxiosRequestConfig = {}
): Promise<JsonResult<T>> {
return this.service.post(url, params, _object);
}
put<T>(
url: string,
params?: object | object[],
_object: AxiosRequestConfig = {}
): Promise<JsonResult<T>> {
return this.service.put(url, params, _object);
}
delete<T>(
url: string,
params?: object,
_object: AxiosRequestConfig = {}
): Promise<JsonResult<T>> {
return this.service.delete(url, { params, ..._object });
}
download(
url: string,
params?: object,
_object: AxiosRequestConfig = {}
): Promise<BlobPart> {
return this.service.post(url, params, { ..._object, responseType: "blob" });
}
}
const api = new RequestHttp(axiosConfig);
export default api;

View File

@ -0,0 +1,42 @@
<template>
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconClassName" :fill="color" />
</svg>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps({
iconName: {
type: String,
required: true,
},
className: {
type: String,
default: "",
},
color: {
type: String,
default: "#ffffff",
},
});
// iconfont
const iconClassName = computed(() => {
return `#${props.iconName}`;
});
//
const svgClass = computed(() => {
if (props.className) {
return `svg-icon ${props.className}`;
}
return "svg-icon";
});
</script>
<style scoped lang="scss">
.svg-icon {
width: 2em;
height: 1.5em;
position: relative;
fill: currentColor;
vertical-align: -2px;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="not-container">
<img src="@/assets/images/404.png" class="not-img" alt="404" />
<div class="not-detail">
<h2>404</h2>
<h4>抱歉您访问的页面不存在~🤷🤷</h4>
<!-- <a-button type="primary" @click="router.push(INDEX_URL)"
>返回首页</a-button -->
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
</script>
<style scoped lang="scss">
.not-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.not-img {
margin-right: 120px;
}
.not-detail {
display: flex;
flex-direction: column;
h2,
h4 {
padding: 0;
margin: 0;
}
h2 {
font-size: 60px;
color: red;
}
h4 {
margin: 30px 0 20px;
font-size: 19px;
font-weight: normal;
color: red;
}
}
}
</style>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div>500</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,153 @@
<template>
<div class="layout-container">
<div
class="layout_left"
:class="{ collapsed: useSetting.collapsed ? true : false }"
>
<Logo :collapsed="useSetting.collapsed"></Logo>
<!-- 展示菜单 -->
<div class="layout_left_item">
<a-menu
style="padding-right: 14px; width: 100%"
theme="light"
:inlineCollapsed="useSetting.collapsed ? true : false"
mode="inline"
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
>
<!-- :selectedKeys="selectedKeys" -->
<Menu-item :menu-item="userStore.menuRoutes"></Menu-item>
</a-menu>
</div>
</div>
<div
class="layout_tabbar"
:class="{ collapsed: useSetting.collapsed ? true : false }"
>
<tabbar v-model:collapsed="useSetting.collapsed"></tabbar>
</div>
<div
class="layout_content"
:class="{ collapsed: useSetting.collapsed ? true : false }"
>
<div
style="
height: 40px;
width: 100%;
background-color: #fff;
border-top: #ccc 1px solid;
"
>
<tabsPane></tabsPane>
</div>
<div class="layout_content_item">
<router-view v-slot="{ Component, route }">
<transition appear name="fade" mode="out-in">
<keep-alive :include="keepAliveNames">
<component v-if="flag" :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, nextTick } from "vue";
import Logo from "./logo/index.vue";
import MenuItem from "./menuItem/MenuItem.vue";
import tabbar from "./tabbar/index.vue";
import tabsPane from "@/components/tabsPane/index.vue";
import { useUserStore } from "@/stores/modules/userStore";
import { useUserSetting } from "@/stores/modules/setting";
import { useRoute } from "vue-router";
import api from "@/axios";
const userStore = useUserStore();
const useSetting = useUserSetting();
const route = useRoute();
const keepAliveNames = ref<string[]>([]);
const openKeys = ref([useSetting.selectedKeys]);
const flag = ref(true);
// const resp = api.get("/management/auth/myAuthRouter");
// console.log(resp, "000");
//
watch(
() => useSetting.refresh,
() => {
flag.value = false;
nextTick(() => {
flag.value = true;
});
}
);
const selectedKeys = computed(() => [route.path]);
</script>
<style lang="scss" scoped>
.layout-container {
width: 100%;
height: 100vh;
// display: flex;
.layout_left {
width: 260px;
height: 100vh;
// background: #001529;
color: #fff;
transition: all 0.3s;
.layout_left_item {
height: calc(100vh - 50px);
overflow-y: auto;
}
&.collapsed {
width: 65px;
}
}
.layout_tabbar {
position: fixed;
top: 0;
left: 260px;
width: calc(100% - 260px);
height: 50px;
transition: all 0.3s;
background: #f5f5f5;
&.collapsed {
width: calc(100% - 65px);
left: 65px;
}
}
.layout_content {
position: absolute;
width: calc(100% - 260px);
height: calc(100vh - 50px);
top: 50px;
left: 260px;
// padding: 20px;
overflow: auto;
transition: all 0.3s;
background: #f5f5f5;
&.collapsed {
width: calc(100% - 65px);
left: 65px;
}
.layout_content_item {
padding: 20px;
}
}
}
.fade-enter-from {
opacity: 0;
transition: all 0.2s;
transform: translateX(-30px);
}
// .fade-enter-active {
// opacity: 0;
// }
.fade-leave-to {
opacity: 0;
transition: all 0.2s;
transform: translateX(30px);
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div class="logo">
<img src="@/assets/vue.svg" alt="xxxx后台系统" />
<p
style="margin-left: 20px"
:class="{ logoItem: props.collapsed ? true : false }"
>
智慧食堂系统
</p>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
collapsed: {
type: Boolean,
default: false,
},
});
</script>
<style lang="scss" scoped>
.logo {
height: 50px;
width: 100%;
color: black;
padding: 20px;
display: flex;
align-items: center;
background: #fff;
border-bottom: 1px solid #ccc;
transition: all 0.3s;
img {
width: 30px;
height: 30px;
}
p {
font-size: 18px;
transition: all 1.2s;
}
.logoItem {
color: #fff;
transition: none;
}
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<template v-for="(item, index) in menuItem" :key="index">
<a-sub-menu v-if="item.children" :key="item.path">
<template #icon>
<SvgIcon :icon-name="item.icon"></SvgIcon>
</template>
<template #title>
<span style="margin-left: 5px">{{ item.name }}</span>
</template>
<menu-item :menu-item="item.children" />
</a-sub-menu>
<a-menu-item
v-else
:key="item.path + ''"
@click="routerMenuItem(item.path)"
>
<template #icon>
<SvgIcon :icon-name="item.icon"></SvgIcon>
</template>
<span style="margin-left: 5px">{{ item.name }}</span>
</a-menu-item>
</template>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import { SystemMenu } from "@/types/config/index";
import SvgIcon from "@/components/SvgIcon/index.vue";
import { useUserSetting } from "@/stores/modules/setting";
const router = useRouter();
const useSetting = useUserSetting();
withDefaults(
defineProps<{
menuItem?: SystemMenu[];
}>(),
{
menuItem: (): SystemMenu[] => {
return [];
},
}
);
const routerMenuItem = (path: string) => {
router.push(path);
useSetting.setSelectedKeys(path);
};
</script>
<style lang="scss" scoped>
.MenuItem {
padding-inline: 16px;
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<div class="tabbar">
<div class="tabbar-left">
<menu-unfold-outlined
class="trigger"
@click="changeIcon"
v-if="useSetting.collapsed"
/>
<menu-fold-outlined class="trigger" v-else @click="changeIcon" />
<!-- 左侧面包屑 -->
<a-breadcrumb>
<template #separator><span>></span></template>
<a-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="item.path"
v-show="item.name"
>
{{ item.name }}</a-breadcrumb-item
>
</a-breadcrumb>
</div>
<div class="tabbar-right">
<a-button style="margin-right: 8px" shape="circle" @click="refreshButton"
><sync-outlined
/></a-button>
<a-button style="margin-right: 8px" shape="circle" @click="fullScreen"
><ExpandOutlined
/></a-button>
<a-button style="margin-right: 8px" shape="circle"
><SettingOutlined
/></a-button>
<a-avatar src="https://www.antdv.com/assets/logo.1ef800a8.svg" />
<a-dropdown v-model:open="visible">
<a class="ant-dropdown-link" @click.prevent>
admin
<DownOutlined />
</a>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu-item key="1">退出</a-menu-item>
<a-menu-item key="2">个人中心</a-menu-item>
<a-menu-item key="3">修改密码</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
SyncOutlined,
ExpandOutlined,
SettingOutlined,
DownOutlined,
} from "@ant-design/icons-vue";
import type { MenuProps } from "ant-design-vue";
import { useUserSetting } from "@/stores/modules/setting";
import { useRoute, useRouter } from "vue-router";
import { useUserStore } from "@/stores/modules/userStore";
import router from "@/router";
const userStore = useUserStore();
const useSetting = useUserSetting();
const $Route = useRoute();
const $Router = useRouter();
const visible = ref(false);
const handleMenuClick: MenuProps["onClick"] = (e) => {
if (e.key === "1") {
// 退
userStore.deleteToken();
$Router.push({ path: "/login", query: { redirect: $Route.path } });
} else if (e.key === "3") {
visible.value = false;
}
};
const changeIcon = () => {
// pinia
useSetting.toggleCollapsed();
};
//
const refreshButton = () => {
useSetting.refreshPage();
};
// ;
const fullScreen = () => {
let full = document.fullscreenElement; //
if (!full) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
};
const breadcrumbList: any = computed(() => {
return $Route.matched.map((item) => ({
path: item.path,
name: item.name,
children: item.children || [],
}));
});
</script>
<style lang="scss" scoped>
.tabbar {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
background-color: #fff;
.tabbar-left {
height: 100%;
display: flex;
align-items: center;
.trigger {
font-size: 18px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
border-left: 1px solid #ccc;
}
.breadcrumbItem {
color: rgba(0, 0, 0, 0.45);
transition: color 0.2s;
padding: 0 4px;
border-radius: 4px;
height: 22px;
display: inline-block;
margin-inline: -4px;
}
}
.tabbar-right {
padding: 0 24px;
cursor: pointer;
display: flex;
align-items: center;
.ant-dropdown-link {
color: rgba(0, 0, 0, 0.45);
margin-left: 8px;
}
}
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<a-tabs
type="editable-card"
v-model:activeKey="activeKey"
@edit="handleTabEdit"
@tabClick="handleTabClick"
hide-add
>
<a-tab-pane
v-for="tab in tabsList"
:key="tab.key"
:tab="tab.title"
:closable="tab.closable"
>
</a-tab-pane>
</a-tabs>
</template>
<script setup>
import { ref, watch, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
const router = useRouter();
const route = useRoute();
//
const tabsList = ref([
{
key: "/home",
title: "首页",
closable: false,
},
]);
//
const activeKey = ref(route.path);
//
onMounted(() => {
addTab(route);
});
//
watch(
() => route.path,
(newPath) => {
activeKey.value = newPath;
addTab(route);
}
);
//
const addTab = (route) => {
const exists = tabsList.value.some((tab) => tab.key === route.path);
if (!exists) {
tabsList.value.push({
key: route.path,
title: route.name || "新tab",
closable: route.path !== "/home", //
});
}
};
//
const handleTabClick = (key) => {
router.push(key);
};
//
const handleTabEdit = (targetKey, action) => {
if (action === "remove") {
removeTab(targetKey);
}
};
//
const removeTab = (targetKey) => {
if (targetKey === "/home") return; //
const tabs = tabsList.value;
let newActiveKey = activeKey.value;
if (targetKey === activeKey.value) {
tabs.forEach((tab, index) => {
if (tab.key === targetKey) {
const nextTab = tabs[index + 1] || tabs[index - 1];
newActiveKey = nextTab?.key || "/home";
}
});
}
tabsList.value = tabs.filter((tab) => tab.key !== targetKey);
activeKey.value = newActiveKey;
router.push(newActiveKey);
};
</script>

33
src/config/constant.ts Normal file
View File

@ -0,0 +1,33 @@
/**
*
*/
export const CLIENT_TYPE = "management";
export const INDEX_ROUTER = {
path: "/index",
name: "1905141507795456000",
meta: {
title: "首页",
icon: "LineMdHomeMd",
isKeepAlive: false,
btnList: [],
isFixed: true,
isFull: false,
remark: "",
} as any,
};
export const LOGIN_ROUTER = {
path: "/login",
name: "1905141542192943104",
meta: {
title: "登录",
btnList: [],
icon: "",
isFixed: false,
isKeepAlive: false,
isFull: false,
remark: "",
} as any,
};
export const ROUTER_WHITE_LIST: string[] = [LOGIN_ROUTER.path];

70
src/config/dict.ts Normal file
View File

@ -0,0 +1,70 @@
import api from "@/axios";
import { isEmpty, isNull } from "lodash-es";
type EnumType =
| "EnableStatus"
| "AnnouncementType"
| "FileType"
| "IndustryType"
| "IsEnable"
| "IsOrNot"
| "MenuType"
| "ProjectStatus"
| "Sex";
export const initEnums = () => {
api
.get<Record<EnumType, SelectNodeVo<any>[]>>("/common/enums")
.then((resp) => {
sessionStorage.setItem("enumsMap", JSON.stringify(resp.data));
});
};
export const enumSelectNodes = <T, E = any>(
enumType: EnumType
): SelectNodeVo<T, E>[] =>
JSON.parse(sessionStorage.getItem("enumsMap") as string)?.[enumType];
export const defaultAllEnumSelectNodes = <T, E = any>(
enumType: EnumType
): SelectNodeVo<T, E>[] => {
return [
{
value: null as T,
label: "全部",
},
...enumSelectNodes<T, E>(enumType),
];
};
export const getEnumByValue = <T, E = any>(
enumType: EnumType,
value: T
): null | SelectNodeVo<T, E> => {
const enumsMap: Record<EnumType, SelectNodeVo<T, E>[]> = JSON.parse(
sessionStorage.getItem("enumsMap") as string
);
if (isNull(enumsMap)) {
return null;
}
const enumList = enumsMap[enumType];
if (isEmpty(enumList)) {
return null;
}
for (let i = 0; i < enumList.length; i++) {
if (value === enumList[i].value) {
return enumList[i];
}
}
return null;
};
export const getEnumLabelByValue = <T>(
enumType: EnumType,
value: T
): string => {
const enums = getEnumByValue(enumType, value);
if (isNull(enums)) {
return "-";
}
return enums.label;
};

1
src/config/index.ts Normal file
View File

@ -0,0 +1 @@

48
src/global.d.ts vendored Normal file
View File

@ -0,0 +1,48 @@
declare const __APP_ENV__: ImportMetaEnv;
interface Window {
_AMapSecurityConfig: {
securityJsCode: string;
};
}
interface tokenInfo {
isLogin: boolean;
loginDevice: string;
loginId: string;
loginType: string;
sessionTimeout: string;
tag: string;
tokenActiveTimeout: string;
tokenName: string;
tokenSessionTimeout: string;
tokenTimeout: string;
tokenValue: string;
}
class SelectNodeVo<T, E = Record<string, any>> {
value: T;
label: string;
options?: SelectNodeVo<T>[];
orderIndex?: number;
disabled?: boolean;
extData?: E;
}
interface RouterVo {
/** id **/
snowFlakeId: string;
/** 父级id **/
parentId: string;
/** 子项 **/
children: unsupported;
/** 名称 **/
name: string;
/** 菜单类型 **/
type: SelectNodeVo<"dir" | "menu" | "btn">;
/** 路径 **/
path: string;
/** 重定向地址 **/
redirect: string;
/** 路由元数据 **/
meta: RouterMetaVo;
}

27
src/main.ts Normal file
View File

@ -0,0 +1,27 @@
import { createApp } from "vue";
import App from "@/App.vue";
// vue Router
import router from "@/router";
// AntDesignVue
import Antd from "ant-design-vue";
// pinia store
import pinia from "@/stores";
// AntDesignVue样式
import "ant-design-vue/dist/reset.css";
//浏览器重置样式
import "@/assets/scss/reset.scss";
//公共样式
import "@/assets/scss/common.scss";
// 阿里巴巴普惠体
import "@/assets/scss/font.scss";
// icon图标
import SvgIcon from "@/components/SvgIcon/index.vue";
import "@/assets/iconfont/iconfont.js";
import "@/styles/index.scss";
import { initEnums } from "@/config/dict";
// initEnums();
const vueApp = createApp(App);
vueApp.component("SvgIcon", SvgIcon);
vueApp.use(router).use(Antd).use(pinia).mount("#app");

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

@ -0,0 +1,69 @@
import {
createRouter,
createWebHistory,
createWebHashHistory,
} from "vue-router";
import { constantRoute, errorRoute } from "./routes";
import { useUserStore } from "@/stores/modules/userStore";
const router = createRouter({
history: createWebHashHistory(),
routes: [...constantRoute, ...errorRoute],
strict: false,
scrollBehavior: () => ({ left: 0, top: 0 }), //滚动行为
});
/**
*
*/
router.beforeEach((to: any, from: any, next: any) => {
//动态设置标题
const title: string = "后台管理系统";
document.title = to.meta.name ? `${to.meta.name} - ${title}` : title;
//todo 鉴权操作...
const userStore = useUserStore();
if (userStore.token) {
if (to.path === "/login") {
next({ path: "/home" });
} else {
next();
}
} else {
if (to.path === "/login") {
next();
} else {
next({ path: "/login", query: { redirect: to.path } });
}
}
// if (userStore.userInfo?.tokenValue) {
// if (to.path === "/login") {
// next({ path: "/home" });
// } else {
// next();
// }
// } else {
// if (to.path === "/login") {
// next();
// } else {
// next({ path: "/login", query: { redirect: to.path } });
// }
// }
//放行
return next();
});
/**
*
*/
router.onError((error: any) => {
console.warn("路由错误", error.message);
});
/**
*
* */
router.afterEach((to: any, from: any) => {
//...
});
export default router;

199
src/router/routes.ts Normal file
View File

@ -0,0 +1,199 @@
export const constantRoute: any = [
{
path: "/home",
name: "首页",
component: () => import("@/views/home/index.vue"),
title: "home",
icon: "icon-shouye1",
},
{
path: "/screen",
name: "数据大屏",
component: () => import("@/views/screen/index.vue"),
title: "screen",
icon: "icon-shujudaping2",
},
{
title: "userManagement",
name: "系统管理",
path: "/userManagement",
icon: "icon-quanxianguanli",
children: [
{
title: "bgManagement",
name: "用户管理",
path: "/userManagement/bgManagement",
icon: "icon-yonghuguanli",
component: () => import("@/views/userManagement/bgManagement/index.vue"),
},
{
title: "role",
name: "角色管理",
path: "/userManagement/role",
icon: "icon-jiaoseguanli2",
component: () => import("@/views/userManagement/role/index.vue"),
},
{
title: "permission",
name: "菜单管理",
path: "/userManagement/permission",
icon: "icon-caidanguanli2",
component: () => import("@/views/userManagement/permission/index.vue"),
},
{
title: "log",
name: "日志管理",
path: "/userManagement/log",
icon: "icon-caidanguanli2",
component: () => import("@/views/userManagement/log/index.vue"),
}
],
},
{
title: "product",
name: "菜谱管理",
path: "/product",
icon: "icon-shangpinguanli3",
children: [
{
title: "food",
name: "食材管理",
path: "/product/food",
icon: "icon-shuxingguanli2",
component: () => import("@/views/product/food/index.vue"),
},
{
title: "dishes",
name: "菜品管理",
path: "/product/dishes",
icon: "icon-shuxingguanli2",
component: () => import("@/views/product/dishes/index.vue"),
},
{
title: "trademark",
name: "菜单",
path: "/product/trademark",
icon: "icon-pinpaiguanli2",
component: () => import("@/views/product/trademark/index.vue"),
},
],
},
{
title: "foodprocurement",
name: "食品采购管理",
path: "/foodprocurement",
icon: "icon-shangpinguanli3",
children: [
{
title: "directory",
name: "采购名录",
path: "/foodprocurement/directory",
icon: "icon-shuxingguanli2",
component: () => import("@/views/foodprocurement/directory/index.vue"),
},
{
title: "plan",
name: "采购计划",
path: "/foodprocurement/plan",
icon: "icon-shuxingguanli2",
component: () => import("@/views/foodprocurement/plan/index.vue"),
},
{
title: "orders",
name: "采购订单",
path: "/foodprocurement/orders",
icon: "icon-shuxingguanli2",
component: () => import("@/views/foodprocurement/orders/index.vue"),
},
{
title: "inventory",
name: "库存管理",
path: "/foodprocurement/inventory",
icon: "icon-shuxingguanli2",
component: () => import("@/views/foodprocurement/inventory/index.vue"),
},
{
title: "dataStatistics",
name: "采购数据统计",
path: "/foodprocurement/dataStatistics",
icon: "icon-shuxingguanli2",
component: () => import("@/views/foodprocurement/dataStatistics/index.vue"),
}
],
},
{
path: "/foodSampleManagement",
name: "食品留样管理",
component: () => import("@/views/foodSampleManagement/index.vue"),
title: "foodSampleManagement",
icon: "icon-shujudaping2",
},
{
path: "/workAccounts",
name: "工作台账管理",
component: () => import("@/views/workAccounts/index.vue"),
title: "workAccounts",
icon: "icon-shujudaping2",
},
{
path: "/smart",
name: "智能分析统计",
component: () => import("@/views/smart/index.vue"),
title: "smart",
icon: "icon-shujudaping2",
},
{
path: "/foodAafetyEducation",
name: "食安教育",
component: () => import("@/views/foodAafetyEducation/index.vue"),
title: "foodAafetyEducation",
icon: "icon-shujudaping2",
},
{
path: "/hardwareData",
name: "硬件数据集成",
component: () => import("@/views/hardwareData/index.vue"),
title: "hardwareData",
icon: "icon-shujudaping2",
},
{
path: "/earlyWarning",
name: "智能预警中心",
component: () => import("@/views/earlyWarning/index.vue"),
title: "earlyWarning",
icon: "icon-shujudaping2",
}
];
export const errorRoute: any = [
{
path: "/login",
name: "登录",
component: () => import("@/views/login/login.vue"),
title: "login",
},
{
path: "/404",
name: "404",
component: () => import("@/components/errorMessage/404.vue"),
title: "404",
},
{
path: "/",
name: "",
title: "",
component: () => import("@/components/layout/index.vue"),
children: constantRoute,
},
{
path: "/500",
name: "500",
title: "500",
component: () => import("@/components/errorMessage/500.vue"),
},
{
path: "/:pathMatch(.*)*",
redirect: "/404",
component: () => import("@/components/errorMessage/404.vue"),
},
];

8
src/stores/index.ts Normal file
View File

@ -0,0 +1,8 @@
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
//创建store实例 持久化插件
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
export default pinia;

View File

@ -0,0 +1,10 @@
// 小仓库:动态路由
import { defineStore } from "pinia";
import { ref } from "vue";
import api from "@/axios";
export const routingStore = defineStore("routing", () => {
// const resp = api.get("/management/auth/myAuthRouter");
// console.log(resp);
return {};
});

View File

@ -0,0 +1,25 @@
// 小仓库layout组件的配置
import { defineStore } from "pinia";
export const useUserSetting = defineStore({
id: "user-setting",
state: () => {
return {
collapsed: false, //侧边栏折叠
selectedKeys: "",
refresh: false, //全局刷新
};
},
actions: {
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
refreshPage() {
this.refresh = !this.refresh;
},
setSelectedKeys(key: string) {
this.selectedKeys = key;
},
},
getters: {},
persist: {},
});

View File

@ -0,0 +1,41 @@
import { defineStore } from "pinia";
import { constantRoute, errorRoute } from "@/router/routes";
import { SystemMenu } from "@/types/config/index";
export interface UserStore {
userInfo: tokenInfo | undefined;
menuRoutes: SystemMenu[];
token:string
}
export const useUserStore = defineStore({
id: "user-stores",
state: (): UserStore => {
return {
userInfo: undefined,
menuRoutes: constantRoute,
token:''
};
},
actions: {
tokenList(token:string){
this.token = token
},
setTokenInfo(user: tokenInfo) {
// 保存userinfo
this.userInfo = user;
},
// 清空用户信息(退出登录)
deleteToken() {
this.token = undefined
this.userInfo = undefined;
},
},
persist: {
key: "user-stores",
storage: window.localStorage,
paths:["token"]
// paths: ["userInfo"],
},
});

1
src/styles/index.scss Normal file
View File

@ -0,0 +1 @@
@import "./variable.scss";

10
src/styles/variable.scss Normal file
View File

@ -0,0 +1,10 @@
// 项目全局样式
$base-color: red;
// 左侧菜单颜色
$base-menu-width: 260px;
// 左侧菜单的背景颜色
$base-menu-background: #001529;
// 顶部导航的高度
$base-tabbar-height: 60px;

11
src/types/config/index.ts Normal file
View File

@ -0,0 +1,11 @@
import { RouteComponent } from "vue-router";
export interface SystemMenu {
hidden?: boolean;
title?: string;
path?: string;
name?: string;
icon?: string;
component?: RouteComponent;
children?: SystemMenu[];
}

17
src/utils/aesUtil.ts Normal file
View File

@ -0,0 +1,17 @@
import { JSEncrypt } from "jsencrypt";
const rsa = new JSEncrypt();
// rsa.setPublicKey(__APP_ENV.VITE_APP_JS_ENCRYPT_PUBLIC_KEY)
rsa.setPublicKey(import.meta.env.VITE_APP_CRYPTO_JS_SECRET_KEY);
export const encryptStr = (text: string): string => {
const r = rsa.encrypt(text);
if (!r) {
throw "加密失败";
}
return r;
};
export default {
encryptStr,
};

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>智能预警中心</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>食安教育</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>食品留样管理</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>采购数据统计</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>采购名录</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>库存管理</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>采购订单</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>采购计划</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>硬件数据集成</div>
</template>
<style scoped lang="scss">
</style>

160
src/views/home/index.vue Normal file
View File

@ -0,0 +1,160 @@
<template>
<div class="home">
<a-card :bordered="false" style="width: 100%">
<div class="homeIndex">
<div>
<a-form name="horizontal_login" layout="inline" autocomplete="off">
<a-form-item label="名字" name="username">
<a-input placeholder="请输入名字"> </a-input>
</a-form-item>
<a-form-item label="年龄" name="username">
<a-input placeholder="请输入名字"> </a-input>
</a-form-item>
<a-form-item label="地址" name="username">
<a-input placeholder="请输入名字"> </a-input>
</a-form-item>
<a-form-item label="时间" name="range-picker">
<a-range-picker value-format="YYYY-MM-DD" />
</a-form-item>
<a-form-item label="名字" name="username">
<a-input placeholder="请输入名字"> </a-input>
</a-form-item>
</a-form>
</div>
<div>
<a-button type="primary">
<template #icon>
<ZoomInOutlined />
</template>
搜索</a-button
>
</div>
</div>
</a-card>
<a-card :bordered="false" class="homeItem">
<div>
<a-button type="primary" style="margin-right: 10px">
<template #icon>
<PlusOutlined />
</template>
新增</a-button
>
<a-button
type="primary"
:disabled="!hasSelected"
:loading="state.loading"
@click="start"
>
Reload {{ state.selectedRowKeys.length }}
</a-button>
</div>
<div style="margin-bottom: 16px"></div>
<a-table
:row-selection="{
selectedRowKeys: state.selectedRowKeys,
onChange: onSelectChange,
}"
:columns="columns"
:data-source="data"
:scroll="{ x: 1500, y: 430 }"
/>
</a-card>
</div>
</template>
<script lang="tsx" setup>
import { PlusOutlined, ZoomInOutlined } from "@ant-design/icons-vue";
import { computed, reactive } from "vue";
type Key = string | number;
interface DataType {
key: Key;
name: string;
age: number;
address: string;
}
const columns = [
{
title: "名字",
dataIndex: "name",
customRender: (text: any) => {
return text.value;
},
},
{
title: "年龄",
dataIndex: "age",
},
{
title: "地址",
dataIndex: "address",
},
{
title: " 操作",
dataIndex: "operation",
customRender: () => {
return (
<div>
<a-button type="primary" danger class="margin-right-sm">
删除
</a-button>
<a-button class="margin-right-sm">查看</a-button>
<a-button type="primary">编辑</a-button>
</div>
);
},
},
];
const data: DataType[] = [];
for (let i = 0; i < 100; i++) {
data.push({
key: i,
name: `小明 ${i}`,
age: 32 + i,
address: `湖南省长沙市岳麓区. ${i}`,
});
}
const state = reactive<{
selectedRowKeys: Key[];
loading: boolean;
}>({
selectedRowKeys: [], //
loading: false,
});
const hasSelected = computed(() => state.selectedRowKeys.length > 0);
const start = () => {
state.loading = true;
// ajax request after empty completing
setTimeout(() => {
state.loading = false;
state.selectedRowKeys = [];
}, 1000);
};
const onSelectChange = (selectedRowKeys: Key[]) => {
console.log("selectedRowKeys changed: ", selectedRowKeys);
state.selectedRowKeys = selectedRowKeys;
};
</script>
<style lang="scss" scoped>
.home {
height: calc(100vh - 130px);
// background: #fff;
overflow: hidden;
.homeIndex {
display: flex;
justify-content: space-between;
align-items: center;
}
.homeItem {
width: 100%;
height: calc(100vh - 100px);
overflow: hidden;
margin-top: 20px;
}
}
</style>

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

@ -0,0 +1,398 @@
<template>
<div id="container">
<div class="checkbox" style="z-index: 999999">
<div class="checkboxItem">
<a-checkbox
style="margin-right: 10px"
v-model:checked="state.checkAll"
:indeterminate="state.indeterminate"
@change="onCheckAllChange"
>
全部
</a-checkbox>
<a-checkbox-group
v-model:value="state.checkedList"
:options="plainOptions"
/>
</div>
</div>
</div>
</template>
<script setup lang="tsx">
import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
import AMapLoader from "@amap/amap-jsapi-loader";
import IconAssets from "@/assets/images/point.png";
let maps: any = null;
let loadedAMap: any;
let cluster: any = null;
let deviceInfoWindow = ref<any>();
const wristbandIcon = ref<any>();
const plainOptions = [
"保安",
"警察",
"移动设备",
"手持设备",
"AI设备",
"固定设备",
];
const state = reactive({
indeterminate: false,
checkAll: true,
checkedList: plainOptions,
});
var points = [
{
weight: 8,
lnglat: ["116.506647", "39.795337"],
name: "北京",
id: 0,
age: 15,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.843352", "40.377362"],
name: "上海",
id: 1,
age: 18,
phone: "13575452134",
address: "长沙市开福区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.637122", "40.324272"],
name: "成都",
id: 2,
age: 25,
phone: "13575452134",
address: "长沙市雨花区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.105381", "39.937183"],
name: "天津",
id: 3,
age: 45,
phone: "13575452134",
address: "衡阳市石鼓区",
units: "衡阳市石鼓区牛逼有限公司",
},
{
weight: 1,
lnglat: ["116.653525", "40.128936"],
name: "南京",
id: 4,
age: 45,
phone: "13575452134",
address: "岳阳市镜湖区",
},
{
weight: 1,
lnglat: ["116.486409", "39.921489"],
name: "长沙",
id: 5,
age: 65,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.658603", "39.902486"],
name: "衡阳",
id: 6,
age: 55,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.338033", "39.728908"],
name: "北京1",
id: 7,
age: 35,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.235906", "40.218085"],
name: "北京2",
id: 8,
age: 65,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.366794", "39.915309"],
name: "北京3",
id: 9,
age: 12,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.418757", "39.917544"],
name: "北京4",
id: 10,
age: 12,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.139157", "39.735535"],
name: "北京5",
id: 11,
age: 12,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.195445", "39.914601"],
name: "北京6",
id: 12,
age: 12,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.310316", "39.956074"],
name: "北京7",
id: 13,
age: 12,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
{
weight: 1,
lnglat: ["116.286968", "39.863642"],
name: "北京8",
id: 14,
age: 12,
phone: "13575452134",
address: "长沙市岳麓区麓谷街道芜湖",
},
];
const count = points.length;
window._AMapSecurityConfig = {
securityJsCode: "35be6bbf6cd521a1f17a9e455e52ffe9",
};
const initMap = () => {
AMapLoader.load({
key: "b886c3f67152803e081f0b0f5594a55e",
version: "2.0",
plugins: ["AMap.MarkerCluster"],
}).then((AMap) => {
loadedAMap = AMap;
maps = new AMap.Map("container", {
// id
resizeEnable: true,
viewMode: "3D", // 3D
zoom: 10, //
center: [116.407387, 39.904179], //
});
wristbandIcon.value = new AMap.Icon({
image: IconAssets, //Icon
imageOffset: new AMap.Pixel(-9, -3), //
imageSize: new AMap.Size(40, 40), //
});
createCluster();
mapZoom();
console.log(1111);
maps.on("zoomend", function () {
clearMarkers();
let zoom = maps.getZoom();
if (zoom > 10) {
addNewMarkers();
cluster?.setMap(null); //
} else {
createCluster(); //
mapZoom();
}
});
});
};
const addNewMarkers = () => {
points.forEach((item: any) => {
const marker = new loadedAMap.Marker({
position: item.lnglat,
icon: new loadedAMap.Icon({
image: IconAssets,
size: new loadedAMap.Size(40, 46.5),
imageSize: new loadedAMap.Size(30, 30),
}),
});
marker.on("click", function () {
deviceInfoWindow.value = new loadedAMap.InfoWindow({
isCustom: true,
content: content.value,
offset: new loadedAMap.Pixel(5, -13),
});
deviceInfoWindow.value.open(maps, item.lnglat);
});
contents(item);
maps.add(marker);
});
};
const content = ref();
const contents = (item: any) => {
content.value = `<div class="comments" style="width: 300px;">
<ul class="commentUl" style="list-style: none">
<li><h2 style="color: skyblue">${item.name}</h2></li>
<li>年龄<span>${item.age}</span></li>
<li>电话<span>${item.phone}</span></li>
<li>地址<span>${item.address}</span></li>
</ul>
</div>`;
};
const mapZoom = () => {
cluster.on("click", (e: any) => {
const { clusterData, lnglat } = e;
let currentZoom = maps.getZoom();
// <= 12
if (currentZoom < 10) {
maps.setZoomAndCenter(currentZoom + 1, lnglat); // 2
}
clearMarkers(); // Marker
let markers: any[] = [];
clusterData.forEach((item: any) => {
const marker = new loadedAMap.Marker({
position: item.lnglat,
offset: new loadedAMap.Pixel(-15, -15),
icon: new loadedAMap.Icon({
image: IconAssets,
size: new loadedAMap.Size(40, 46.5),
imageSize: new loadedAMap.Size(30, 30),
}),
});
marker.on("click", function () {
deviceInfoWindow.value = new loadedAMap.InfoWindow({
isCustom: true,
content: content.value,
offset: new loadedAMap.Pixel(5, -13),
});
deviceInfoWindow.value.open(maps, item.lnglat);
});
markers.push(marker);
maps.add(marker);
});
// Marker
maps.setFitView(markers);
});
};
const createCluster = () => {
if (cluster) cluster.setMap(null); //
// @ts-ignore
cluster = new loadedAMap.MarkerCluster(maps, points, {
gridSize: 80,
maxZoom: 12,
renderClusterMarker: _renderClusterMarker,
});
};
// Marker
function clearMarkers() {
const overlays = maps.getAllOverlays("marker");
overlays.forEach((marker: any) => {
if (!marker.isCluster) {
maps.remove(marker); // Marker
}
});
}
const _renderClusterMarker = function (context: any) {
var factor = Math.pow(context.count / count, 1 / 18);
var div = document.createElement("div");
var Hue = 180 - factor * 180;
var bgColor = "hsla(" + Hue + ",100%,40%,0.7)";
var fontColor = "hsla(" + Hue + ",100%,90%,1)";
var borderColor = "hsla(" + Hue + ",100%,40%,1)";
var shadowColor = "hsla(" + Hue + ",100%,90%,1)";
div.style.backgroundColor = bgColor;
var size = Math.round(30 + Math.pow(context.count / count, 1 / 5) * 20);
div.style.width = div.style.height = size + "px";
div.style.border = "solid 1px " + borderColor;
div.style.borderRadius = size / 2 + "px";
div.style.boxShadow = "0 0 5px " + shadowColor;
div.innerHTML = context.count;
div.style.lineHeight = size + "px";
div.style.color = fontColor;
div.style.fontSize = "14px";
div.style.textAlign = "center";
context.marker.setOffset(new loadedAMap.Pixel(-size / 2, -size / 2));
context.marker.setContent(div);
};
// const _renderMarker = (context: any) => {
// const content =
// '<div style="background-color: hsla(180, 100%, 50%, 0.3); height: 18px; width: 18px; border: 1px solid hsl(180, 100%, 40%); border-radius: 12px; box-shadow: hsl(180, 100%, 50%) 0px 0px 3px;"></div>';
// const offset = new loadedAMap.Pixel(-9, -9);
// context.marker.setContent(content);
// context.marker.setOffset(offset);
// context.on("click", () => {
// console.log(121);
// });
// };
const onCheckAllChange = (e: any) => {
console.log(e);
Object.assign(state, {
checkedList: e.target.checked ? plainOptions : [],
indeterminate: true,
});
};
watch(
() => state.checkedList,
(val) => {
console.log(val);
if (val[0] !== plainOptions[0]) {
clearMarkers();
} else {
addNewMarkers();
}
state.indeterminate = !!val.length && val.length < plainOptions.length;
state.checkAll = val.length === plainOptions.length;
}
);
onMounted(() => {
initMap();
});
onUnmounted(() => {
maps?.destroy();
});
</script>
<style scoped lang="scss">
#container {
width: 100vw;
height: 100vh;
position: relative;
.comments {
width: 300px !important;
box-shadow: 0 4px 8px #0003;
}
.checkbox {
width: 100vw;
height: 50px;
background-color: #ffffffb3;
box-sizing: border-box;
position: absolute;
top: 0;
//left: 40%;
.checkboxItem {
text-align: center;
line-height: 50px;
}
}
}
</style>

View File

@ -0,0 +1,83 @@
body {
padding: 0;
margin: 0;
height: 100%;
}
*:focus {
outline: none;
}
ul {
padding: 0;
margin: 0;
}
li {
list-style-type: none;
}
.page {
width: 1024px;
margin: auto;
}
/* input{
background-color:#DA70D6 !important;
} */
input:-webkit-autofill {
/*自动填入文本颜色*/
-webkit-text-fill-color: #ffffff !important;
/*自动填入光标颜色*/
caret-color: white;
/*背景透明;原理:动画由透明变有背景色;参数:背景色属性、动画时间、动画速度曲线、延迟时间*/
transition: background-color 0s linear 3600s;
}
input::-webkit-input-placeholder {
color: #305d9b;
}
input::-moz-input-placeholder {
color: #305d9b;
}
input::-ms-input-placeholder {
color: #305d9b;
}
input[type="button"] {
border: none;
}
// body {
// background: -webkit-linear-gradient(#030f2b, #152578);
// background: -o-linear-gradient(#030f2b, #152578);
// background: -moz-linear-gradient(#030f2b, #152578);
// background: -mos-linear-gradient(#030f2b, #152578);
// background: linear-gradient(#030f2b, #152578);
// }
.login {
height: 100vh;
width: 100%;
background: url("@/assets/background-34eb3d2b.jpg") no-repeat;
background-size: cover;
.loginItem {
position: relative;
top: 30vh;
width: 50%;
border: 1px solid #ccc;
padding: 40px;
border-radius: 10px;
h1 {
color: white;
font-size: 40px;
}
h2 {
margin-bottom: 20px;
color: white;
font-size: 20px;
}
}
}

117
src/views/login/login.vue Normal file
View File

@ -0,0 +1,117 @@
<template>
<div class="login">
<a-row>
<a-col :span="12"></a-col>
<a-col :span="12">
<a-form
ref="formRef"
class="loginItem"
:model="formState"
name="login"
autocomplete="off"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<h1>Hello</h1>
<h2>您好欢迎来到我的世界</h2>
<a-form-item
name="phoneNumber"
:rules="[{ required: true, message: '请输入您的电话号码!' }]"
>
<a-input
placeholder="请输入您的电话号码"
v-model:value="formState.phoneNumber"
>
<template #prefix>
<UserOutlined class="site-form-item-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[{ required: true, message: '请输入您的密码!' }]"
>
<a-input-password
placeholder="请输入您的密码"
v-model:value="formState.password"
>
<template #prefix>
<LockOutlined class="site-form-item-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item name="remember">
<a-checkbox v-model:checked="formState.remember">记住我</a-checkbox>
</a-form-item>
<a-form-item>
<a-button
style="width: 100%"
type="primary"
:loading="loading"
html-type="submit"
>登录</a-button
>
</a-form-item>
</a-form>
</a-col>
</a-row>
</div>
</template>
<!-- -->
<script setup lang="ts">
// import api from "@/axios";
import { ref, computed, reactive } from "vue";
import aesUtil from "@/utils/aesUtil";
import { UserOutlined, LockOutlined } from "@ant-design/icons-vue";
import { useUserStore } from "@/stores/modules/userStore";
import { encryptStr } from "@/utils/aesUtil";
import { useRoute, useRouter } from "vue-router";
import { message } from "ant-design-vue";
import api from "@/axios";
const userStore = useUserStore();
const formRef = ref();
const loading = ref(false);
interface FormState {
phoneNumber: string;
password: string;
remember: boolean;
}
const formState = reactive<FormState>({
phoneNumber: "15576404472",
password: "123456",
remember: true,
});
//
const $route = useRoute();
//
const $router = useRouter();
const onFinish = () => {
userStore.tokenList('Ghdfef13156513256413565')
let redirect: any = $route.query.redirect
$router.push({ path: redirect || "/login" })
message.success("成功登录")
// loading.value = true;
// const encryptedText = encryptStr(formState.password);
// api
// .post("/login/management", {
// phoneNumber: formState.phoneNumber,
// password: encryptedText,
// })
// .then((res) => {
// if (res.code === 200) {
// userStore.setTokenInfo(res.data as tokenInfo);
// let redirect: any = $route.query.redirect;
// $router.push({ path: redirect || "/login" });
// message.success("");
// }
// loading.value = false;
// });
};
const onFinishFailed = (errorInfo: any) => {
console.log("Failed:", errorInfo);
};
</script>
<style lang="scss">
@import "./login.scss";
</style>

View File

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

View File

@ -0,0 +1,26 @@
<template>
<div class="attr">
<a-card :bordered="false" style="width: 100%">
<p>属性管理</p>
</a-card>
<a-card :bordered="false" class="attrItem">
<p>我是表格页面</p>
</a-card>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.attr {
height: calc(100vh - 130px);
// background: #fff;
overflow: hidden;
.attrItem {
width: 100%;
height: calc(100vh - 100px);
overflow: hidden;
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="trademark">
<a-card :bordered="false" style="width: 100%">
<p>品牌管理</p>
</a-card>
<a-card :bordered="false" class="trademarkItem">
<p>我是表格页面</p>
</a-card>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.trademark {
height: calc(100vh - 130px);
overflow: hidden;
.trademarkItem {
width: 100%;
height: calc(100vh - 100px);
overflow: hidden;
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,5 @@
<template>
<div>数据大屏</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped></style>

11
src/views/smart/index.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>智能分析统计</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,26 @@
<template>
<div class="bgManagement">
<a-card :bordered="false" style="width: 100%">
<p>用户管理</p>
</a-card>
<a-card :bordered="false" class="bgManagementItem">
<p>我是表格页面</p>
</a-card>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.bgManagement {
height: calc(100vh - 130px);
// background: #fff;
overflow: hidden;
.bgManagementItem {
width: 100%;
height: calc(100vh - 100px);
overflow: hidden;
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>日志管理</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,26 @@
<template>
<div class="permission">
<a-card :bordered="false" style="width: 100%">
<p>菜单管理</p>
</a-card>
<a-card :bordered="false" class="permissionItem">
<p>我是表格页面</p>
</a-card>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.permission {
height: calc(100vh - 130px);
// background: #fff;
overflow: hidden;
.permissionItem {
width: 100%;
height: calc(100vh - 100px);
overflow: hidden;
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<div class="role">
<a-card :bordered="false" style="width: 100%">
<p>角色管理</p>
</a-card>
<a-card :bordered="false" class="roleItem">
<p>我是表格页面</p>
</a-card>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.role {
height: calc(100vh - 130px);
// background: #fff;
overflow: hidden;
.roleItem {
width: 100%;
height: calc(100vh - 100px);
overflow: hidden;
margin-top: 20px;
}
}
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>工作台账管理</div>
</template>
<style scoped lang="scss">
</style>

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

@ -0,0 +1,21 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
//当前环境
readonly VITE_APP_ENV: "development" | "production";
//启动端口
readonly VITE_APP_PORT: number;
// axios
readonly VITE_APP_BASE_API: string;
readonly VITE_APP_PROXY_URL: string;
// crypto-js
readonly VITE_APP_CRYPTO_JS_SECRET_KEY: string;
// readonly VITE_APP_CRYPTO_JS_SECRET_IV: string
}
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

42
tsconfig.json Normal file
View File

@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"allowJs": true,
"module": "ESNext",
"lib": ["ESNext", "DOM", "ScriptHost"],
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "preserve",
/* Linting */
"strict": true,
//使
//"noUnusedLocals": true,
//switch使break
"noFallthroughCasesInSwitch": true,
// const str:string = undefined | null;
"strictNullChecks": false,
//JavaScript.map
"sourceMap": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": [
"node_modules",
"dist",
"src/assets/iconfont/iconfont.js",
"**/*.js"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

9
tsconfig.node.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

85
vite.config.ts Normal file
View File

@ -0,0 +1,85 @@
import { defineConfig, loadEnv, UserConfigExport } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import vue from "@vitejs/plugin-vue";
import * as path from "path";
const pathSrc = path.resolve(__dirname, "src");
// https://vitejs.dev/config/
// loadEnv对应加装环境中的变量
export default ({ mode }): UserConfigExport => {
// 我要加载哪一个环境的文件process.cwd()
const env: Record<string, string> = loadEnv(mode, process.cwd(), "");
return defineConfig({
define: {
__APP_ENV__: JSON.stringify(env),
},
base: "/",
plugins: [vue(), vueJsx()],
server: {
host: "0.0.0.0",
port: env["VITE_APP_PORT"] as unknown as number,
open: false,
proxy: {
[env["VITE_APP_BASE_API"]]: {
target: env["VITE_APP_PROXY_URL"],
changeOrigin: true,
secure: false,
rewrite: (path) =>
path.replace(RegExp(`^${env["VITE_APP_BASE_API"]}`), ""),
},
},
},
build: {
outDir: "dist",
target: "modules",
chunkSizeWarningLimit: 1500,
minify: "terser",
terserOptions: {
compress: {
//生产环境时移除console
drop_console: env["VITE_DROP_CONSOLE"] as unknown as boolean,
drop_debugger: env["VITE_DROP_CONSOLE"] as unknown as boolean,
},
},
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("node_modules")) {
return id
.toString()
.split("node_modules/")[1]
.split("/")[0]
.toString();
}
},
chunkFileNames(chunkInfo) {
const facadeModuleId = chunkInfo.facadeModuleId
? chunkInfo.facadeModuleId.split("/")
: [];
const fileName =
facadeModuleId[facadeModuleId.length - 2] || "[name]";
return `js/${fileName}/[name].[hash].js`;
},
},
},
},
resolve: {
alias: {
"@": pathSrc,
},
},
css: {
preprocessorOptions: {
scss: {
charset: false,
// 全局css变量
additionalData: `
@import "@/assets/scss/variable.scss";
@import "@/assets/scss/mixin.scss";
`,
},
},
},
});
};

1433
yarn.lock Normal file

File diff suppressed because it is too large Load Diff