feat:完成硅谷甄选的学习

This commit is contained in:
sundongyu 2024-06-12 14:58:17 +08:00
commit e1dcaacdb7
60 changed files with 4645 additions and 0 deletions

5
.env.development Normal file
View File

@ -0,0 +1,5 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/api'
VITE_SERVE="http://sph-api.atguigu.cn"

4
.env.production Normal file
View File

@ -0,0 +1,4 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'production'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/prod-api'

4
.env.test Normal file
View File

@ -0,0 +1,4 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/test-api'

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
dist
node_modules

62
.eslintrc.cjs Normal file
View File

@ -0,0 +1,62 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true
},
/* 指定如何解析语法 */
parser: 'vue-eslint-parser',
/** 优先级低于 parse 的语法解析配置 */
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: '@typescript-eslint/parser',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true
}
},
/* 继承已有的规则 */
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended'
],
plugins: ['vue', '@typescript-eslint'],
/*
* "off" 0 ==> 关闭规则
* "warn" 1 ==> 打开的规则作为警告不影响代码执行
* "error" 2 ==> 规则作为一个错误代码不能执行界面报错
*/
rules: {
// eslinthttps://eslint.bootcss.com/docs/rules/
'no-var': 'error', // 要求使用 let 或 const 而不是 var
'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-unexpected-multiline': 'error', // 禁止空余的多行
'no-useless-escape': 'off', // 禁止不必要的转义字符
// 添加允许末尾逗号的规则
'comma-dangle': ['error', 'never'], // 对于多行数组、对象字面量和函数参数,要求末尾有逗号
// 如果你也希望单行情况下允许尾随逗号,可以设置为:'comma-dangle': ['error', 'always']
// typeScript (https://typescript-eslint.io/rules)
'@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量
'@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore
'@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
'@typescript-eslint/semi': 'off',
// eslint-plugin-vue (https://eslint.vuejs.org/rules/)
'vue/multi-word-component-names': 'off', // 要求组件名称始终为 “-” 链接的单词
'vue/script-setup-uses-vars': 'error', // 防止<script setup>使用的变量<template>被标记为未使用
'vue/no-mutating-props': 'off', // 不允许组件 prop的改变
'vue/attribute-hyphenation': 'off' // 对模板中的自定义组件强制执行属性命名样式
}
}

30
.gitignore vendored Normal file
View File

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

8
.prettierignore Normal file
View File

@ -0,0 +1,8 @@
/dist/*
/html/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*
vite.config.ts

10
.prettierrc.json Normal file
View File

@ -0,0 +1,10 @@
{
"singleQuote": true,
"semi": false,
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "ignore",
"endOfLine": "auto",
"trailingComma": "none",
"tabWidth": 2,
"printWidth": 120
}

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

@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

39
README.md Normal file
View File

@ -0,0 +1,39 @@
# vite
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## 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 [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
pnpm install
```
### Compile and Hot-Reload for Development
```sh
pnpm dev
```
### Type-Check, Compile and Minify for Production
```sh
pnpm build
```
### Lint with [ESLint](https://eslint.org/)
```sh
pnpm lint
```

1
env.d.ts vendored Normal file
View File

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

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

65
mock/user.ts Normal file
View File

@ -0,0 +1,65 @@
//用户信息数据
function createUserList() {
return [
{
userId: 1,
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'admin',
password: '111111',
desc: '平台管理员',
roles: ['平台管理员'],
buttons: ['cuser.detail'],
routes: ['home'],
token: 'Admin Token'
},
{
userId: 2,
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'system',
password: '111111',
desc: '系统管理员',
roles: ['系统管理员'],
buttons: ['cuser.detail', 'cuser.user'],
routes: ['home'],
token: 'System Token'
}
]
}
export default [
// 用户登录接口
{
url: '/api/user/login', //请求地址
method: 'post', //请求方式
response: ({ body }) => {
//获取请求体携带过来的用户名与密码
const { username, password } = body
//调用获取用户信息函数,用于判断是否有此用户
const checkUser = createUserList().find((item) => item.username === username && item.password === password)
//没有用户返回失败信息
if (!checkUser) {
return { code: 201, data: { message: '账号或者密码不正确' } }
}
//如果有返回成功信息
const { token } = checkUser
return { code: 200, data: { token } }
}
},
// 获取用户信息
{
url: '/api/user/info',
method: 'get',
response: (request) => {
//获取请求头携带token
const token = request.headers.token
//查看用户信息是否包含有次token用户
const checkUser = createUserList().find((item) => item.token === token)
//没有返回失败的信息
if (!checkUser) {
return { code: 201, data: { message: '获取用户信息失败' } }
}
//如果有返回成功信息
return { code: 200, data: { checkUser } }
}
}
]

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "vite",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --open",
"build:test": "vue-tsc && vite build --mode test",
"build:pro": "vue-tsc && vite build --mode production",
"preview": "vite preview",
"lint": "eslint src",
"fix": "eslint src --fix",
"format": "prettier --write \"./**/*.{html,vue,ts,js,json,md}\"",
"lint:eslint": "eslint src/**/*.{ts,vue} --cache --fix",
"lint:style": "stylelint src/**/*.{css,scss,vue} --cache --fix"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.2",
"element-plus": "^2.7.5",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"sass": "^1.77.4",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@tsconfig/node20": "^20.1.4",
"@types/node": "^20.12.5",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"mockjs": "^1.1.0",
"npm-run-all2": "^6.1.2",
"prettier": "^3.2.5",
"typescript": "~5.4.0",
"vite": "^5.2.8",
"vite-plugin-mock": "^3.0.2",
"vue-tsc": "^2.0.11"
}
}

2646
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

5
src/App.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<RouterView />
</template>
<script setup lang="ts"></script>
<style scoped></style>

28
src/api/menu/index.ts Normal file
View File

@ -0,0 +1,28 @@
import request from '@/utils/request'
// 引入类型
import type { PermisstionResponseData, MenuParams } from './type'
const API = {
//获取全部菜单与按钮的标识数据
ALLPERMISSTION_URL: 'http://sph-api.atguigu.cn/admin/acl/permission',
//给某一级菜单新增一个子菜单
ADDMENU_URL: 'http://sph-api.atguigu.cn/admin/acl/permission/save',
//更新某一个已有的菜单
UPDATE_URL: 'http://sph-api.atguigu.cn/admin/acl/permission/update',
//删除已有的菜单
DELETEMENU_URL: 'http://sph-api.atguigu.cn/admin/acl/permission/remove/'
}
//获取菜单数据
export const reqAllPermission = () => {
return request.get<any, PermisstionResponseData>(API.ALLPERMISSTION_URL)
}
//添加与更新菜单的方法
export const reqAddOrUpdateMenu = (data: MenuParams) => {
if (data.id) {
return request.put<any, any>(API.UPDATE_URL, data)
} else {
return request.post<any, any>(API.ADDMENU_URL, data)
}
}
//删除某一个已有的菜单
export const reqRemoveMenu = (id: number) => request.delete<any, any>(API.DELETEMENU_URL + id)

35
src/api/menu/type.ts Normal file
View File

@ -0,0 +1,35 @@
//数据类型定义
export interface ResponseData {
code: number
message: string
ok: boolean
}
//菜单数据与按钮数据的ts类型
export interface Permisstion {
id?: number
createTime: string
updateTime: string
pid: number
name: string
code: null
toCode: null
type: number
status: null
level: number
children?: PermisstionList
select: boolean
}
export type PermisstionList = Permisstion[]
//菜单接口返回的数据类型
export interface PermisstionResponseData extends ResponseData {
data: PermisstionList
}
//添加与修改菜单携带的参数的ts类型
export interface MenuParams {
id?: number //ID
code: string //权限数值
level: number //几级菜单
name: string //菜单的名字
pid: number //菜单的ID
}

24
src/api/user/index.ts Normal file
View File

@ -0,0 +1,24 @@
//统一管理咱们项目用户相关的接口
import request from '@/utils/request'
import type { loginFormData, loginResponseData, userInfoReponseData } from './type'
//项目用户相关的请求地址
enum API {
LOGIN_URL = '/user/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout'
}
//登录接口
export const reqLogin = (data: loginFormData) => request.post<any, loginResponseData>(API.LOGIN_URL, data)
//获取用户信息
export const reqUserInfo = () => request.get<any, userInfoReponseData>(API.USERINFO_URL)
//退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

29
src/api/user/type.ts Normal file
View File

@ -0,0 +1,29 @@
//定义用户相关数据的ts类型
//用户登录接口携带参数的ts类型
export interface loginFormData {
username: string
password: string
}
//定义全部接口返回数据都拥有ts类型
export interface ResponseData {
code: number
message: string
ok: boolean
}
//定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {
data: string
}
//定义获取用户信息返回数据类型
export interface userInfoReponseData extends ResponseData {
data: {
routes: string[]
buttons: string[]
roles: string[]
name: string
avatar: string
}
}

BIN
src/assets/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,88 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
>Cypress Component Testing</a
>.
<br />
More instructions are available in <code>README.md</code>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
Discord server, or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

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

@ -0,0 +1,117 @@
<template>
<div class="layout_container">
<!-- 左侧菜单 -->
<div class="layout_slider" :class="{ fold: LayOutSettingStore.fold ? true : false }">
<Logo></Logo>
<!-- 展示菜单 -->
<!-- 滚动组件 -->
<el-scrollbar class="scrollbar">
<!-- 菜单组件-->
<el-menu
:default-active="$route.path"
:collapse="LayOutSettingStore.fold ? true : false"
background-color="#001529"
text-color="white"
>
<!--根据路由动态生成菜单-->
<MenuIndex :menuList="userStore.menuRoutes"></MenuIndex>
</el-menu>
</el-scrollbar>
</div>
<!-- 内容展示区域 -->
<div class="layout_tabbar" :class="{ fold: LayOutSettingStore.fold ? true : false }">
<!-- layout组件的顶部导航tabbar -->
<Tabbar></Tabbar>
</div>
<div class="layout_main" :class="{ fold: LayOutSettingStore.fold ? true : false }">
<MianIndex></MianIndex>
</div>
</div>
</template>
<script setup lang="ts">
//
import { useRoute } from 'vue-router'
//logo
import Logo from '@/layout/logo/index.vue'
//
import MenuIndex from '@/layout/menu/index.vue'
//
import useUserStore from '@/stores/modules/user'
import MianIndex from '@/layout/main/index.vue'
//tabbar
import Tabbar from '@/layout/tabbar/index.vue'
//
import useLayOutSettingStore from '@/stores/modules/setting'
//layout
let LayOutSettingStore = useLayOutSettingStore()
let userStore = useUserStore()
//
let $route = useRoute()
</script>
<script lang="ts">
export default {
name: 'Layout'
}
</script>
<style scoped lang="scss">
.layout_container {
width: 100%;
height: 100vh;
.layout_slider {
color: white;
width: $base-menu-width;
height: 100vh;
background: $base-menu-background;
transition: all 0.3s;
.scrollbar {
width: 100%;
height: calc(100vh - $base-menu-logo-height);
.el-menu {
border-right: none;
}
}
&.fold {
width: $base-menu-min-width;
}
}
.layout_tabbar {
position: fixed;
width: calc(100% - $base-menu-width);
height: $base-tabbar-height;
top: 0px;
left: $base-menu-width;
transition: all 0.3s;
&.fold {
width: calc(100vw - $base-menu-min-width);
left: $base-menu-min-width;
}
}
.layout_main {
position: absolute;
width: calc(100% - $base-menu-width);
height: calc(100vh - $base-tabbar-height);
left: $base-menu-width;
top: $base-tabbar-height;
padding: 20px;
overflow: auto;
transition: all 0.3s;
&.fold {
width: calc(100vw - $base-menu-min-width);
left: $base-menu-min-width;
}
}
}
:deep(.el-menu-item.is-active) {
color: $--active-color;
}
</style>

31
src/layout/logo/index.vue Normal file
View File

@ -0,0 +1,31 @@
<template>
<div class="logo">
<img src="@/assets/logo.svg" alt="" />
<p>硅谷甄选</p>
</div>
</template>
<script setup lang="ts"></script>
<script lang="ts">
export default {
name: 'Logo'
}
</script>
<style scoped lang="scss">
.logo {
width: 100%;
height: $base-menu-logo-height;
color: white;
display: flex;
align-items: center;
padding: 10px;
img {
width: 40px;
height: 40px;
}
p {
font-size: $base-logo-title-fontSize;
margin-left: 10px;
}
}
</style>

56
src/layout/main/index.vue Normal file
View File

@ -0,0 +1,56 @@
<template>
<!-- 路由组件出口的位置 -->
<!-- 是Vue Router的核心组件用于展示当前路由所对应的组件当路由发生变化时 -->
<router-view v-slot="{ Component }">
<!-- 这是Vue的插槽语法糖用于接收从<router-view>传递过来的数据在这个例子中它接收了一个名为Component的变量该变量就是当前路由对应的组件 -->
<transition name="fade">
<!-- 这是Vue的过渡动画组件用于在组件切换时添加过渡效果这里定义了一个名为fade的过渡效果意味着组件切换时将会有一个淡入淡出的效果 -->
<!-- 渲染layout一级路由组件的子路由 -->
<component :is="Component" v-if="flag" />
<!-- 这意味着它将渲染当前激活路由对应的组件v-if="flag"则是一个条件渲染指令只有当flag为真时这个组件才会被渲染 -->
</transition>
</router-view>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
//
import useLayOutSettingStore from '@/stores/modules/setting'
let layOutSettingStore = useLayOutSettingStore()
//
let flag = ref(true)
//,
watch(
() => layOutSettingStore.refsh,
() => {
//:
flag.value = false
nextTick(() => {
flag.value = true
})
}
)
</script>
<script lang="ts">
export default {
name: 'MainIndex'
}
</script>
<style scoped>
.fade-enter-from {
opacity: 0;
transform: scale(0);
}
.fade-enter-active {
transition: all 0.3s;
}
.fade-enter-to {
opacity: 1;
transform: scale(1);
}
</style>

53
src/layout/menu/index.vue Normal file
View File

@ -0,0 +1,53 @@
<template>
<template v-for="item in menuList" :key="item.path">
<!--没有子路由-->
<template v-if="!item.children">
<el-menu-item :index="item.path" v-if="!item.meta.hidden" @click="goRoute">
<el-icon><Edit /></el-icon>
<template #title>
<span>{{ item.meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由但是只有一个子路由 -->
<template v-if="item.children && item.children.length == 1">
<el-menu-item :index="item.children[0].path" v-if="!item.children[0].meta.hidden" @click="goRoute">
<el-icon><Edit /></el-icon>
<template #title>
<span>{{ item.children[0].meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由且个数大于一个1 -->
<el-sub-menu :index="item.path" v-if="item.children && item.children.length > 1">
<template #title>
<el-icon><Edit /></el-icon>
<span>{{ item.meta.title }}</span>
</template>
<MenuIndex :menuList="item.children"></MenuIndex>
</el-sub-menu>
</template>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
//
import { Edit } from '@element-plus/icons-vue'
//
defineProps(['menuList'])
//
let $router = useRouter()
//
const goRoute = (vc: any) => {
//
$router.push(vc.index)
}
</script>
<script lang="ts">
export default {
name: 'MenuIndex'
}
</script>
<style scoped></style>

View File

@ -0,0 +1,40 @@
<template>
<!-- 顶部左侧静态 -->
<el-icon style="margin-right: 10px" @click="changeIcon">
<Moon />
</el-icon>
<!-- 左侧面包屑 -->
<el-breadcrumb separator-icon="ArrowRight">
<!-- 面包动态展示路由名字与标题 -->
<el-breadcrumb-item v-for="(item, index) in $route.matched" :key="index" v-show="item.meta.title" :to="item.path">
<!-- 图标 -->
<el-icon>
<ArrowRight />
</el-icon>
<!-- 面包屑展示匹配路由的标题 -->
<span style="margin: 0 5px">{{ item.meta.title }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router'
//
import { ArrowRight, Moon } from '@element-plus/icons-vue'
import useLayOutSettingStore from '@/stores/modules/setting'
//layout
let LayOutSettingStore = useLayOutSettingStore()
//
const $route = useRoute()
const changeIcon = () => {
//
LayOutSettingStore.fold = !LayOutSettingStore.fold
}
</script>
<script lang="ts">
export default {
name: 'Breadcrumb'
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,42 @@
<template>
<div class="tabbar">
<div class="tabbar_left">
<Breadcrumb />
<!-- 左侧面包屑 -->
</div>
<div class="tabbar_right">
<Setting />
<!-- 右侧退出 -->
</div>
</div>
</template>
<script setup lang="ts">
import Breadcrumb from './breadcrumd/index.vue'
import Setting from './setting/index.vue'
</script>
<script lang="ts">
export default {
name: 'Tabbar'
}
</script>
<style scoped lang="scss">
.tabbar {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
// background-image: linear-gradient(to right, rgb(232, 223, 223), rgb(201, 178, 178), rgb(197, 165, 165));
.tabbar_left {
display: flex;
align-items: center;
margin-left: 20px;
}
.tabbar_right {
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<el-button size="small" :icon="Refresh" circle @click="updateRefsh"></el-button>
<el-button size="small" :icon="FullScreen" circle @click="fullScreen"></el-button>
<el-popover placement="bottom" title="主题设置" :width="300" trigger="click">
<!-- 表单元素 -->
<el-form>
<el-form-item label="主题颜色">
<el-color-picker @change="setColor" v-model="color" size="small" show-alpha :predefine="predefineColors" />
</el-form-item>
<el-form-item label="暗黑模式">
<!-- prompt: 无论图标或文本是否显示在点内只会呈现文本的第一个字符 -->
<el-switch
@change="changeDark"
v-model="dark"
class="mt-2"
style="margin-left: 24px"
inline-prompt
:active-icon="MoonNight"
:inactive-icon="Sunny"
/>
</el-form-item>
</el-form>
<template #reference>
<el-button size="small" :icon="Setting" circle></el-button>
</template>
</el-popover>
<img
src="https://q4.itc.cn/q_70/images03/20240528/298d4abda5e4469d98fa77e7cde46525.jpeg"
style="width: 24px; height: 24px; margin: 0px 10px; border-radius: 50%"
/>
<!-- 下拉菜单 -->
<el-dropdown>
<span class="el-dropdown-link">
sdy
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
//
import { Refresh, FullScreen, MoonNight, Sunny, Setting } from '@element-plus/icons-vue'
import { ref } from 'vue'
import useLayOutSettingStore from '@/stores/modules/setting'
//
let dark = ref<boolean>(false)
const layoutSettingStore = useLayOutSettingStore()
//
const updateRefsh = () => {
layoutSettingStore.refsh = !layoutSettingStore.refsh
}
//
const fullScreen = () => {
//DOM:[:true,:false]
let full = document.fullscreenElement
//
if (!full) {
//requestFullscreen,
document.documentElement.requestFullscreen()
} else {
//->退
document.exitFullscreen()
}
}
//
const color = ref('rgba(255, 69, 0, 0.68)')
const predefineColors = ref([
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'rgba(255, 69, 0, 0.68)',
'rgb(255, 120, 0)',
'hsv(51, 100, 98)',
'hsva(120, 40, 94, 0.5)',
'hsl(181, 100%, 37%)',
'hsla(209, 100%, 56%, 0.73)',
'#c7158577'
])
//
const changeDark = () => {
// HTML
let html = document.documentElement
//
if (dark.value) {
//
html.classList.add('dark')
} else {
//
html.classList.remove('dark')
}
}
//
const setColor = () => {
const html = document.documentElement
html.style.setProperty('--el-color-primary', color.value)
}
</script>
<script lang="ts">
export default {
name: 'Setting'
}
</script>
<style scoped></style>

22
src/main.ts Normal file
View File

@ -0,0 +1,22 @@
// import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
//引入element-plus插件与样式
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 引入暗黑模式样式
import 'element-plus/theme-chalk/dark/css-vars.css'
//引入模板的全局的样式
import '@/style/index.scss'
import './permisstion'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')

7
src/pages/404/index.vue Normal file
View File

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

7
src/pages/home/index.vue Normal file
View File

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

7
src/pages/home/test.vue Normal file
View File

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

109
src/pages/login/index.vue Normal file
View File

@ -0,0 +1,109 @@
<template>
<div class="login_container">
<el-row>
<el-col :span="12" :xs="0"></el-col>
<el-col :span="12" :xs="24">
<!-- 登录的表单 -->
<el-form class="login_form" :model="loginForm" ref="loginForms">
<h1>Hello</h1>
<h2>欢迎来到硅谷甄选</h2>
<el-form-item prop="username">
<el-input :prefix-icon="User" v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" :prefix-icon="Lock" v-model="loginForm.password" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button :loading="loading" class="login_btn" type="primary" size="default" @click="login">
登录
</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { User, Lock } from '@element-plus/icons-vue'
import { ElNotification } from 'element-plus'
//
import useUserStore from '@/stores/modules/user'
//
import { useRouter, useRoute } from 'vue-router'
const useStore = useUserStore()
//
let router = useRouter()
//
let route = useRoute()
let loginForm = ref({ username: 'admin', password: 'atguigu123' })
//
let loading = ref(false)
const login = async () => {
await loginForm.value
//:
loading.value = true
//?
//
//->
//->
try {
//
await useStore.userLogin(loginForm.value)
//
//,queryquery
let redirect: any = route.query.redirect
router.push({ path: redirect || '/' })
window.location.reload()
//
ElNotification({
type: 'success',
message: '欢迎回来'
})
//
loading.value = false
} catch (error) {
//
loading.value = false
//
ElNotification({
type: 'error',
message: '登陆失败'
})
}
}
</script>
<style scoped lang="scss">
.login_container {
width: 100%;
height: 100vh;
background: url('@/assets/background.jpg') no-repeat;
background-size: cover;
.login_form {
position: relative;
width: 80%;
top: 30vh;
background: url('@/assets/images/login_form.png') no-repeat;
background-size: cover;
padding: 40px;
h1 {
color: white;
font-size: 40px;
}
h2 {
font-size: 20px;
color: white;
margin: 20px 0px;
}
.login_btn {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<!--border:是否带有纵向边框 -->
<el-table :data="permission" style="width: 100%; margin-bottom: 20px" row-key="id" border>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="权限值" prop="code"></el-table-column>
<el-table-column label="修改时间" prop="updateTime"></el-table-column>
<el-table-column label="操作" prop="name">
<template #default="{ row }">
<!-- disabled禁用效果 -->
<el-button type="primary" size="small" @click="addPermission(row)" :disabled="row.level == 4 ? true : false">
{{ row.level == 3 ? '添加功能' : '添加菜单' }}
</el-button>
<el-button type="primary" size="small" @click="updatePermission(row)" :disabled="row.level == 1 ? true : false">
编辑
</el-button>
<el-popconfirm :title="`你确定要删除${row.name}?`" width="260px" @confirm="removeMenu(row.id)">
<template #reference>
<el-button type="primary" size="small" :disabled="row.level == 1 ? true : false">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 对话框组件 -->
<el-dialog v-model="dialogVisible" :title="menuData.id ? '更新菜单' : '添加菜单'">
<!-- 表单组件:收集新增与已有的菜单的数据 -->
<el-form>
<el-form-item label="名称">
<el-input placeholder="请你输入菜单名称" v-model="menuData.name"></el-input>
</el-form-item>
<el-form-item label="权限">
<el-input placeholder="请你输入权限数值" v-model="menuData.code"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { reqAllPermission, reqAddOrUpdateMenu, reqRemoveMenu } from '@/api/menu/index'
import type { PermisstionResponseData, PermisstionList, Permisstion } from '@/api/menu/type'
import { ElMessage } from 'element-plus'
//
const permission = ref<PermisstionList>([])
//
const dialogVisible = ref(false)
//
const menuData: any = ref({
code: '',
level: 0,
name: '',
pid: 0
})
//
const permissionList = async () => {
const res: PermisstionResponseData = await reqAllPermission()
permission.value = res.data
console.log(res.data)
}
//
const addPermission = (row: Permisstion) => {
Object.assign(menuData.value, {
id: 0,
code: '',
level: 0,
name: '',
pid: 0
})
dialogVisible.value = true
//level
menuData.value.level = row.level + 1
//
menuData.value.pid = row.id as number
}
//
const updatePermission = (row: Permisstion) => {
dialogVisible.value = true
//
Object.assign(menuData.value, row)
}
//
const save = async () => {
const result: any = await reqAddOrUpdateMenu(menuData)
if (result.code == 200) {
//
dialogVisible.value = false
//
ElMessage({ type: 'success', message: menuData.value.id ? '更新成功' : '添加成功' })
//
permissionList()
}
}
//
const removeMenu = async (id: number) => {
let result = await reqRemoveMenu(id)
if (result.code == 200) {
ElMessage({ type: 'success', message: '删除成功' })
permissionList()
}
}
onMounted(() => {
permissionList()
})
</script>
<style scoped></style>

View File

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

View File

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

69
src/permisstion.ts Normal file
View File

@ -0,0 +1,69 @@
//路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from '@/router'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import nprogress from 'nprogress'
//引入进度条样式
import 'nprogress/nprogress.css'
//获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './stores/modules/user'
import pinia from './stores'
const userStore = useUserStore(pinia)
//全局守卫:项目当中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
//to:你将要访问那个路由
//from:你从来个路由而来
//next:路由的放行函数
nprogress.start()
//获取token,去判断用户登录、还是未登录
const token = userStore.token
//获取用户名字
const username = userStore.username
//用户登录判断
if (token) {
//登录成功,访问login,不能访问,指向首页
if (to.path == '/login') {
next({ path: '/' })
} else {
//登录成功访问其余六个路由(登录排除)
//有用户信息
if (username) {
//放行
next()
} else {
try {
//获取用户信息
//放行
//万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
next({ ...to })
} catch (error) {
//token过期:获取不到用户信息了
//用户手动修改本地存储token
//退出登录->用户相关的数据清空
next({ path: '/login', query: { redirect: to.path } })
}
}
}
} else {
//用户未登录判断
if (to.path == '/login') {
next()
} else {
next({ path: '/login', query: { redirect: to.path } })
}
}
})
//全局后置守卫
// eslint-disable-next-line @typescript-eslint/no-unused-vars
router.afterEach((to: any, from: any) => {
nprogress.done()
})
//第一个问题:任意路由切换实现进度条业务 ---nprogress
//第二个问题:路由鉴权(路由组件访问权限的设置)
//全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
//用户未登录:可以访问login,其余六个路由不能访问(指向login)
//用户登录成功:不可以访问login[指向首页],其余的路由可以访问

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

@ -0,0 +1,17 @@
//通过vue-router插件实现模板路由配置
import { createRouter, createWebHashHistory } from 'vue-router'
import { constantRoute } from './routes'
//创建路由器
const router = createRouter({
//路由模式hash
history: createWebHashHistory(),
routes: constantRoute,
//滚动行为
scrollBehavior() {
return {
left: 0,
top: 0
}
}
})
export default router

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

@ -0,0 +1,115 @@
import Layout from '@/layout/index.vue'
export const constantRoute = [
{
//登录
path: '/login',
component: () => import('@/pages/login/index.vue'),
name: 'login',
meta: {
title: '登录', //菜单标题
hidden: true, //代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion' //菜单文字左侧的图标,支持element-plus全部图标
}
},
{
//首页
path: '/',
component: Layout,
name: 'home',
redirect: '/home',
meta: {
title: '首页', //菜单标题
hidden: false, //代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion' //菜单文字左侧的图标,支持element-plus全部图标
},
children: [
{
path: '/home',
component: () => import('@/pages/home/index.vue'),
name: 'index',
meta: {
title: '首页', //菜单标题
hidden: false, //代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion' //菜单文字左侧的图标,支持element-plus全部图标
}
},
{
path: '/test',
component: () => import('@/pages/home/test.vue'),
name: 'test',
meta: {
title: '测试', //菜单标题
hidden: false, //代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion' //菜单文字左侧的图标,支持element-plus全部图标
}
}
]
},
{
//菜单
path: '/permission',
component: Layout,
name: 'permission',
redirect: '/user',
meta: {
title: '权限管理', //菜单标题
hidden: false, //代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion' //菜单文字左侧的图标,支持element-plus全部图标
},
children: [
{
path: '/user',
component: () => import('@/pages/permission/user/index.vue'),
name: 'user',
meta: {
title: '用户管理', //菜单标题
hidden: false, //代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion' //菜单文字左侧的图标,支持element-plus全部图标
}
},
{
path: '/role',
component: () => import('@/pages/permission/role/index.vue'),
name: 'role',
meta: {
title: '角色管理', //菜单标题
hidden: false, //代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion' //菜单文字左侧的图标,支持element-plus全部图标
}
},
{
path: '/menu',
component: () => import('@/pages/permission/menu/index.vue'),
name: 'menu',
meta: {
title: '菜单管理', //菜单标题
hidden: false, //代表路由标题在菜单中是否隐藏 true:隐藏 false:不隐藏
icon: 'Promotion' //菜单文字左侧的图标,支持element-plus全部图标
}
}
]
},
{
//404
path: '/404',
component: () => import('@/pages/404/index.vue'),
name: '404',
meta: {
title: '404',
hidden: true,
icon: 'DocumentDelete'
}
},
{
//任意路由
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由',
hidden: true,
icon: 'DataLine'
}
}
]

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

@ -0,0 +1,6 @@
//仓库大仓库
import { createPinia } from 'pinia'
//创建大仓库
const pinia = createPinia()
//对外暴露:入口文件需要安装仓库
export default pinia

View File

@ -0,0 +1,13 @@
//小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'
const useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false, //用户控制菜单折叠还是收起控制
refsh: false //仓库这个属性用于控制刷新效果
}
}
})
export default useLayOutSettingStore

View File

@ -0,0 +1,42 @@
//创建用户相关的小仓库
import { defineStore } from 'pinia'
import { reqLogin } from '@/api/user'
import type { loginFormData, loginResponseData } from '@/api/user/type'
//引入路由(常量路由)
import { constantRoute } from '@/router/routes'
//创建用户小仓库
const useUserStore = defineStore('User', {
//小仓库存储数据地方
state: () => {
return {
token: localStorage.getItem('TOKEN'), //用户唯一标识token
menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由)
username: 'sdy',
avatar: 'https://q4.itc.cn/q_70/images03/20240528/298d4abda5e4469d98fa77e7cde46525.jpeg'
}
},
//异步|逻辑的地方
actions: {
// 用户登录的方法
async userLogin(data: loginFormData) {
//登录请求
const result: loginResponseData = await reqLogin(data)
//登录请求:成功200->token
//登录请求:失败201->登录失败错误的信息
if (result.code == 200) {
//pinia仓库存储一下token
this.token = result.data as string
//本地存储持久化存储一份
localStorage.setItem('TOKEN', (result.data as any).token)
//能保证当前async函数返回一个成功的promise
return 'ok'
} else {
return Promise.reject(new Error(result.data))
}
}
},
getters: {}
})
//对外暴露获取小仓库方法
export default useUserStore

2
src/style/index.scss Normal file
View File

@ -0,0 +1,2 @@
// 引入默认样式
@import '@/style/reset.scss';

186
src/style/reset.scss Normal file
View File

@ -0,0 +1,186 @@
*,
*:after,
*:before {
box-sizing: border-box;
outline: none;
}
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,
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,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
font: inherit;
font-size: 100%;
margin: 0;
padding: 0;
vertical-align: baseline;
border: 0;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
&:before,
&:after {
content: '';
content: none;
}
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
table {
border-spacing: 0;
border-collapse: collapse;
}
input,
textarea,
button {
font-family: inhert;
font-size: inherit;
color: inherit;
}
select {
text-indent: 0.01px;
text-overflow: '';
border: 0;
border-radius: 0;
-webkit-appearance: none;
-moz-appearance: none;
}
select::-ms-expand {
display: none;
}
code,
pre {
font-family: monospace, monospace;
font-size: 1em;
}

19
src/style/variable.scss Normal file
View File

@ -0,0 +1,19 @@
//项目提供scss全局变量
//定义项目主题颜色
//左侧的菜单的宽度
$base-menu-width: 260px;
//左侧菜单的背景颜色
$base-menu-background: #001529;
$base-menu-min-width: 50px;
// 顶部导航的高度
$base-tabbar-height: 50px;
//左侧菜单logo高度设置
$base-menu-logo-height: 50px;
//左侧菜单logo右侧文字大小
$base-logo-title-fontSize: 20px;
//左侧菜单文字颜色
$--active-color: red;

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

@ -0,0 +1,44 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
//创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 5000
})
//请求拦截器
request.interceptors.request.use((config) => {
return config
})
//响应拦截器
request.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
//处理网络错误
let msg = ''
const status = error.response.status
switch (status) {
case 401:
msg = 'token过期'
break
case 403:
msg = '无权访问'
break
case 404:
msg = '请求地址错误'
break
case 500:
msg = '服务器出现问题'
break
default:
msg = '无网络'
}
ElMessage({
type: 'error',
message: msg
})
return Promise.reject(error)
}
)
export default request

15
tsconfig.app.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"types": ["element-plus/global"],
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

29
vite.config.ts Normal file
View File

@ -0,0 +1,29 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
viteMockServe({
mockPath: 'mock',
enable: true
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
additionalData: '@import "@/style/variable.scss";'
}
}
}
})