目录
vite项目初始化
先自己安装nodejs和npm,代码编写用vscode
参考文档:https://cn.vitejs.dev/guide/ - 搭建第一个 Vite 项目
兼容性注意
Vite 需要 Node.js 版本 18+,20+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。
方式一:
npm create vite@latest
然后按照提示操作即可
方式二:
你还可以通过附加的命令行选项直接指定项目名称和想要使用的模板。例如,要构建一个 Vite + Vue 项目,运行:
# npm 7+, 需要额外加 --:
npm create vite@latest my-vue-app -- --template vue
然后
cd my-vue-app
npm install
npm run dev
项目结构
node_modules:管理依赖,
public:管理静态资源
src:源代码
App.vue:程序入口
main.js:js
style.css:样式
.gitignore:git忽略提交文件
package.json:依赖环境
vite.config.js:vite项目配置
Prettier代码格式化
1.vscode里安装插件Prettier - Code formatter
2.下载依赖
npm install --save-dev --save-exact prettier
npm install eslint --save-dev
3.配置
3.1项目根目录新建.prettierrc.json格式化配置文件和.prettierignore忽略格式化文件
.prettierrc.json格式化配置文件
{
"arrowParens": "always",
"bracketSameLine": true,
"bracketSpacing": true,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "all",
"singleAttributePerLine": false,
"htmlWhitespaceSensitivity": "css",
"vueIndentScriptAndStyle": false,
"proseWrap": "preserve",
"insertPragma": false,
"requirePragma": false,
"useTabs": false,
"embeddedLanguageFormatting": "auto",
"tabWidth": 2,
"printWidth": 200
}
可以在 https://prettier.io/playground/ 中测试效果,然后拷贝配置内容到自己的项目中
.prettierignore忽略格式化文件
/dist/*
/node_modules/**
**/*.svg
/public/*
3.2.勾选在保存时格式化文件
3.3.如果vscode中有多个格式化插件,可以右击 - 使用…格式化文档 - 配置默认格式化程序
ESLint编码规范检查
一、安装
vscode里安装插件ESLint
下载依赖
npm install --D eslint prettier eslint-plugin-vue eslint-config-prettier eslint-plugin-prettier
二、配置编码规范检查
2.1 .eslintrc.cjs
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
overrides: [],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['vue', 'prettier'],
rules: {
'prettier/prettier': 'error',
'vue/multi-word-component-names': 'off',
'vue/no-v-model-argument': 'off',
},
};
规则配置
规则中文解释:http://eslint.cn/docs/rules
rules: {
'规则名': '规则值'
// eg:
'no-undef': 'off'
}
规则值:
"off"或者0 => 关闭检测规则
"warn"或者1 => 打开并把打开的检测规则作为警告(不影响退出代码)
"error"或者2 => 打开并把检测规则作为一个错误(退出代码触发时为1)
2.2 .eslintignore 忽略检查
node_modules
*.md
.vscode
.idea
dist
public
*.js
*.cjs
三、配置vite项目启动时检查
npm install -D vite-plugin-eslint
vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import eslint from "vite-plugin-eslint";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
eslint({ lintOnStart: true, cache: false }), // 项目运行时进行eslint检查
],
});
四、检验
tips: 如果vscode中没有效果,可以尝试重启vscode看看。
$ref语法糖告别 .value-不推荐做
一、配置
安装依赖
npm i -D @vue-macros/reactivity-transform
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ReactivityTransform from '@vue-macros/reactivity-transform/vite';
export default defineConfig({
plugins: [
vue(),
ReactivityTransform(), // 启用响应式语法糖 $ref ...
]
})
二、测试
原本 .value 响应式
<template>
<h1>{{ count }}</h1>
<button @click="handleClick">click</button>
</template>
<script setup>
let count = ref(0);
function handleClick() {
count.value++;
}
</script>
现在 $ref 去除 .value
<template>
<h1>{{ count }}</h1>
<button @click="handleClick">click</button>
</template>
<script setup>
let count = $ref(0);
function handleClick() {
count++;
}
</script>
三、注意事项
$ref 在以下情况无法直接使用:
store pinia
watch 监听器
Vite项目配置
1.配置路径别名
2.反向代理解决跨域问题
3.配置项目运行端口
4.环境变量
5.解决 import { ref , reactive … } from ‘vue’ 大量引入的问题
6.$ref语法糖 告别 .value
一、下载
// 解决 `import { ref , reactive ..... } from 'vue'` 大量引入的问题
npm i -D unplugin-auto-import
二、环境变量
.env.dev
# 开发环境
NODE_ENV='dev'
# 为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。
# js中通过`import.meta.env.VITE_APP_BASE_API`取值
VITE_APP_PORT = 5173
VITE_APP_BASE_API = '/dev-api'
VITE_APP_BASE_FILE_API = '/dev-api/web/api/system/file/upload'
# 后端服务地址
VITE_APP_SERVICE_API = 'http://localhost:888'
.env.prod
# 开发环境
NODE_ENV='prod'
# 为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。
# js中通过`import.meta.env.VITE_APP_BASE_API`取值
VITE_APP_PORT = 5173
VITE_APP_BASE_API = '/prod-api'
VITE_APP_BASE_FILE_API = '/prod-api/web/api/system/file/upload'
# 后端服务地址
VITE_APP_SERVICE_API = 'http://localhost:888'
三、package.json
{
"scripts": {
"dev": "vite --mode dev",
"prod": "vite --mode prod",
"build": "vite build --mode prod"
}
}
四、vite.config.js
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import * as path from 'path';
import AutoImport from 'unplugin-auto-import/vite';
import ReactivityTransform from '@vue-macros/reactivity-transform/vite';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
// 获取`.env`环境配置文件
const env = loadEnv(mode, process.cwd());
return {
plugins: [
vue(),
ReactivityTransform(), // 启用响应式语法糖 $ref ...
// 解决 `import { ref , reactive ..... } from 'vue'` 大量引入的问题
AutoImport({
imports: ['vue', 'vue-router'],
}),
],
// 反向代理解决跨域问题
server: {
// host: 'localhost', // 只能本地访问
host: '0.0.0.0', // 局域网别人也可访问
port: Number(env.VITE_APP_PORT), // 生成配置好的端口,重复会自增,固定排序会报错
// 运行时自动打开浏览器
// open: true,
proxy: { //如匹配到'/dev-api'后从url中去掉
[env.VITE_APP_BASE_API]: {
target: env.VITE_APP_SERVICE_API,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), ''),
},
},
},
resolve: {
// 配置路径别名
alias: [
// @代替src
{
find: '@',
replacement: path.resolve('./src'),
},
],
},
// 引入scss全局变量
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/color.scss";@import "@/styles/theme.scss";`,
},
},
},
};
});
Sass
概述:可以支持在css中写嵌套
也可以支持在css中写判断 循环等逻辑
安装
npm install sass --save-dev
Vue Router
一、安装依赖
npm install vue-router@4
二、入门配置
src下新建路由文件,src/router/index.js
import {createRouter, createWebHashHistory} from 'vue-router';
// 本地静态路由
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: {
isParentView: true,
},
},
{
path: '/test',
component: () => import('@/views/test/index.vue'),
},
{
// path: '/404',
path: '/:pathMatch(.*)*', // 防止浏览器刷新时路由未找到警告提示: vue-router.mjs:35 [Vue Router warn]: No match found for location with path "/xxx"
component: () => import('@/views/error-page/404.vue'),
},
];
// 创建路由
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes,
});
export default router;
main.js导入和使用路由
// 路由
import router from '@/router';
const app = createApp(App);
app.use(router); // 使用路由
app.mount('#app');
三、测试
3.1 准备页面
新建src/views/test/index.vue
<template>
<h1>hello</h1>
</template>
3.2 配置路由规则和使用路由
src/router/index.js
import {createRouter, createWebHashHistory} from 'vue-router';
// 本地静态路由
export const constantRoutes = [
{
path: '/test',
component: () => import('@/views/test/index.vue'),
},
];
// 创建路由
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes,
});
export default router;
main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router';
const app = createApp(App);
app.use(router); // 使用路由
app.mount('#app');
3.3 页面使用
App.vue替换新内容
<template>
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件进行导航 -->
<!-- 通过传递 `to` 来指定链接 -->
<!-- `<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签 -->
<router-link to="/">Go to Home</router-link><br />
<router-link to="/test">Go to Test</router-link><br />
<router-link to="/adout">Go to Adout</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</template>
<script setup></script>
<style scoped></style>
mixin混入
全局混入是一种在多个组件中共享相同逻辑的方式,可以将一些通用的方法、生命周期钩子等混入到所有页面和组件中,以简化代码的编写和维护。
等于抽取公共属性、方法…
一、新建src/utils/mixin.js
// 抽取公用的实例 - 操作成功与失败消息提醒内容等
export default {
data() {
return {
sexList: [
{ name: '不想说', value: 0 },
{ name: '男', value: 1 },
{ name: '女', value: 2 },
],
};
},
methods: {
// 操作成功消息提醒内容
submitOk(msg, cb) {
// this.$notify({
// title: '成功',
// message: msg || '操作成功!',
// type: 'success',
// duration: 2000,
// onClose: function () {
// cb && cb();
// },
// });
console.log("成功");
},
// 操作失败消息提醒内容
submitFail(msg) {
// this.$message({
// message: msg || '网络异常,请稍后重试!',
// type: 'error',
// });
console.log("失败");
},
},
};
二、混入
2.1 全局混入
src/main.js
// 混入 -- 抽取公用的实例(操作成功与失败消息提醒内容等)
import mixin from '@/utils/mixin';
app.mixin(mixin);
使用
<template>
<h1>{{ sexList }}</h1>
<button @click="handleClick">click</button>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
async function handleClick() {
proxy.submitOk('保存成功');
proxy.submitFail('操作失败');
}
</script>
2.2 局部混入
<script>
import mixin from '@/utils/mixin.js';
export default {
mixins: [mixin],
};
</script>
<script setup>
const { proxy } = getCurrentInstance();
async function submit() {
proxy.submitOk('保存成功');
}
</script>
全局过滤器
比如用来把后端返回的数据统一调整为前端想要的格式
一、vue2
import { filters } from '@/utils/filters.js';
Object.keys(filters).forEach((key) => Vue.filter(key, filters[key]));
二、vue3
通过 app.config.globalProperties 来注册一个全局都能访问到的属性
1.src/utils下新建filters.js
export const filters = {
// 获取性别值
sexName: (sex) => {
// 拿到mixin混入的属性值
const { proxy } = getCurrentInstance();
let result = proxy.sexList.find((obj) => obj.value == sex);
return result ? result.name : '数据丢失';
},
};
2.mian.js添加代码
// 全局过滤器
import { filters } from '@/utils/filters.js';
app.config.globalProperties.$filters = filters;
3.使用
<h1>{{ $filters.sexName(0) }}</h1>
<h1>{{ $filters.sexName(1) }}</h1>
<h1>{{ $filters.sexName(2) }}</h1>
Element Plus集成
官网:https://element-plus.org/zh-CN/
一、安装
npm install element-plus --save
npm install @element-plus/icons-vue
二、配置
src/main.js
// element-plus
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
app.use(ElementPlus);
// 注册所有图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
三、测试
<template>
<h1>hello</h1>
<el-row class="mb-4">
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</el-row>
<el-icon :size="100" color="red">
<Edit />
</el-icon>
</template>
Pinia
官网:https://pinia.vuejs.org/zh
是store的状态管理库,类似于java中的变量存储,缓存变量值
把变量值放入浏览器缓存中,页面跳转或组件通讯的时候能在任意地方拿到变量值
一、安装
npm install pinia
二、配置
src/main.js
// pinia
import { createPinia } from 'pinia';
const pinia = createPinia();
app.use(pinia);
// store
import store from '@/store'; // vite.config.js里已配置@代替src
app.config.globalProperties.$store = store;
三、使用
src下新建store文件夹和index.js
store模块化
// 拿到modules下的所有文件
// vite5中import.meta.globEager已经被废弃,改成使用import.meta.glob('*', { eager: true })
// const modulesFiles = import.meta.globEager('./modules/*.*');
const modulesFiles = import.meta.glob('./modules/*.*', { eager: true });
const modules = {};
for (const key in modulesFiles) {
const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
const value = modulesFiles[key];
modules[moduleName] = value;
// console.log(modules);
}
export default modules;
src/store下新建modules文件夹和test.js
定义测试的store
import { defineStore } from 'pinia';
export const useTestStore = defineStore('test', () => {
const count = ref(0);
function add() {
count.value++;
}
return { count, add };
});
四、测试
<template>
<h1>{{ count }}</h1>
<button @click="handleClick">click</button>
<br />
<!-- 在模版里取store值的方式 -->
<h1>{{ $store.test.useTestStore().count }}</h1>
<button @click="$store.test.useTestStore().add">click</button>
</template>
<script setup>
// 在js里取store值的方式
const { proxy } = getCurrentInstance();
let useTestStore = proxy.$store.test.useTestStore();
let { count } = toRefs(useTestStore); // 响应式
let { add } = useTestStore;
function handleClick() {
add();
}
</script>
<style lang="scss" scoped></style>
五、Pinia持久化存储
上面的配置浏览器一刷新数据就丢了,所以配置下持久化存储。
安装插件
插件官网:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
npm i pinia-plugin-persistedstate
src/main.js
// pinia
import { createPinia } from 'pinia';
const pinia = createPinia();
// 持久化存储放这里比较好
import { createPersistedState } from 'pinia-plugin-persistedstate';
pinia.use(
createPersistedState({
auto: true, // 启用所有 Store 默认持久化
}),
);
app.use(pinia); // 这个放下面是为了让所有的store都默认持久化
tips: pinia持久化数据的无法通过 window.localStorage.clear(); 一键清空数据
window.localStorage.setItem('user2', 'hello');
// window.localStorage.removeItem('user2');
// tips: pinia持久化的无法通过这种方式清空数据,只能删除同样方式存储的值 eg: window.localStorage.setItem('user2', 'hello');
window.localStorage.clear();
window.sessionStorage.clear();
解决方案:可以重置后再清空
https://pinia.vuejs.org/zh/core-concepts/state.html#resetting-the-state
import store from '@/store';
// 退出登录
function logout() {
isLogin.value = false;
// 清空当前store在pinia中持久化存储的数据
this.$reset();
// 其它store
store.settings.useSettingsStore().$reset();
// 最终真正清空storage数据
window.localStorage.clear();
window.sessionStorage.clear();
}
但是组合式api中直接使用 $reset() 会报错:
解决:1.改用选项式api,2.重写 $reset 方法
src/main.js
// pinia
import { createPinia } from 'pinia';
const pinia = createPinia();
// 持久化存储放这里比较好
import { createPersistedState } from 'pinia-plugin-persistedstate';
pinia.use(
createPersistedState({
auto: true, // 启用所有 Store 默认持久化
}),
);
// 重写 $reset 方法 => 解决组合式api中无法使用问题
pinia.use(({ store }) => {
const initialState = JSON.parse(JSON.stringify(store.$state));
store.$reset = () => {
store.$patch(initialState);
};
});
app.use(pinia); // 这个放下面是为了让所有的store都默认持久化
axios和api封装
axios中文文档 http://www.axios-js.com/zh-cn/docs
一、安装
npm install axios
二、axios工具封装
src/utils/request.js
import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import store from '@/store';
import { localStorage } from '@/utils/storage';
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_SERVICE_API,
timeout: 50000, // 请求超时时间:50s
headers: { 'Content-Type': 'application/json;charset=utf-8' },
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
if (!config.headers) {
throw new Error(`Expected 'config' and 'config.headers' not to be undefined`);
}
// const { isLogin, tokenObj } = toRefs(store.user.useUserStore());
// if (isLogin.value) {
// // 授权认证
// config.headers[tokenObj.value.tokenName] = tokenObj.value.tokenValue;
// // 租户ID
// config.headers['TENANT_ID'] = '1';
// // 微信公众号appId
// config.headers['appId'] = localStorage.get('appId');
// }
return config;
},
(error) => {
return Promise.reject(error);
},
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data;
const { code, msg } = res;
if (code === 200) {
return res;
} else {
// token过期
if (code === -1) {
handleError();
} else {
ElMessage({
message: msg || '系统出错',
type: 'error',
duration: 5 * 1000,
});
}
return Promise.reject(new Error(msg || 'Error'));
}
},
(error) => {
console.log('请求异常:', error);
// const { msg } = error.response.data;
// // 未认证
// if (error.response.status === 401) {
// handleError();
// } else {
// ElMessage({
// message: '网络异常,请稍后再试!',
// type: 'error',
// duration: 5 * 1000,
// });
// return Promise.reject(new Error(msg || 'Error'));
// }
},
);
// 统一处理请求响应异常
function handleError() {
// const { isLogin, logout } = store.user.useUserStore();
// if (isLogin) {
// ElMessageBox.confirm('您的登录账号已失效,请重新登录', {
// confirmButtonText: '再次登录',
// cancelButtonText: '取消',
// type: 'warning',
// }).then(() => {
// logout();
// });
// }
}
// 导出实例
export default service;
src/utils/storage.js
/**
* window.localStorage => 浏览器永久存储,用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。
*/
export const localStorage = {
set(key, val) {
window.localStorage.setItem(key, JSON.stringify(val));
},
get(key) {
const json = window.localStorage.getItem(key);
return JSON.parse(json);
},
remove(key) {
window.localStorage.removeItem(key);
},
clear() {
window.localStorage.clear();
},
};
/**
* window.sessionStorage => 浏览器本地存储,数据保存在当前会话中,在关闭窗口或标签页之后将会删除这些数据。
*/
export const sessionStorage = {
set(key, val) {
window.sessionStorage.setItem(key, JSON.stringify(val));
},
get(key) {
const json = window.sessionStorage.getItem(key);
return JSON.parse(json);
},
remove(key) {
window.sessionStorage.removeItem(key);
},
clear() {
window.sessionStorage.clear();
},
};
三、api封装
src/api/index.js
// 拿到所有api
const modulesFiles = import.meta.glob('./*/*.*', { eager: true });
const modules = {};
for (const key in modulesFiles) {
const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
const value = modulesFiles[key];
if (value.default) {
// 兼容js
modules[moduleName] = value.default;
} else {
// 兼容ts
modules[moduleName] = value;
}
}
// console.log(666, modules);
export default modules;
src/main.js
// 配置全局api
import api from '@/api'
app.config.globalProperties.$api = api
四、测试
src/api/test/demo.js
import request from '@/utils/request';
export default {
time() {
return request({
url: '/api/test/time',
method: 'get',
});
},
};
页面使用去发请求
<template>
<button @click="handleClick">click</button>
<h1>{{ res }}</h1>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
let res = $ref(null);
async function handleClick() {
res = await proxy.$api.demo.time();
}
</script>
后端启动 -> 后端接口http:localhost:8083/api/test/time
流程
全局组件
一、全局组件注册
src/components/index.js
const modulesFiles = import.meta.glob('./*/*.vue', { eager: true });
const modules = {};
for (const key in modulesFiles) {
const moduleName = key.replace(/(.*\/)*([^.]+).*/gi, '$2');
const value = modulesFiles[key];
modules[moduleName] = value.default;
}
// console.log(666, modules);
export default modules;
src/main.js
// 全局组件注册
import myComponent from '@/components/index';
Object.keys(myComponent).forEach((key) => {
app.component(key, myComponent[key]);
});
二、使用全局组件示例
准备自定义全局组件:src/components/base/BaseNoData.vue
<template>
<div>
<!-- 插槽 -->
<slot>暂无数据</slot>
</div>
</template>
引用自定义全局组件:
<base-no-data>请先选择数据</base-no-data>
其它常用全局组件可从外部拷贝
vs code页面模版模块化
设置 - 用户代码片段 - vue
"vue3": {
"prefix": "vue3",
"body": [
"<template>",
"<div>$1</div>",
"</template>",
"<script setup>",
"import { getCurrentInstance } from 'vue';",
"const { proxy } = getCurrentInstance();",
"</script>",
"<style lang="scss" scoped></style>",
],
"description": "vue3 setup模版"
}
然后在编辑器中输入vue3可以快速生成页面模版代码
登录页面与事件流程
结合路由
{
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: {
isParentView: true,
},
},
一、测试
1.1 新建src/views/login/index.vue
<template>
<div>666</div>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
</script>
<style lang="scoped"></style>
1.2 app.vue里写显示位置
<template>
<router-view></router-view>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
</script>
<style lang="scoped"></style>
浏览器访问:http://localhost:5173/#/login
二、正式写登录页面
2.1 页面
<template>
<base-wrapper class="bg-color-primary flex-center-center">
<div class="flex-c-center-center bg-color-white w-400 h-300 b-rd-10">
<h1 class="font-size-lg">SmallBoot</h1>
<div class="m-t-20">
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules">
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="User" placeholder="请输入账号" maxlength="30" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" prefix-icon="Lock" placeholder="请输入密码" show-password maxlength="30" />
</el-form-item>
</el-form>
<div class="tips">
<span>用户名: admin</span>
<span class="m-l-20"> 密码: 123456</span>
</div>
<el-button type="primary" class="m-t-10 w-full" @click="handleLogin">登 录</el-button>
</div>
</div>
<div class="copyright">
<p>IF I WERE YOU</p>
</div>
</base-wrapper>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
// 组件实例
const { proxy } = getCurrentInstance();
const { login } = proxy.$store.user.useUserStore();
const loginForm = $ref({});
const loginRules = {
username: [{ required: true, trigger: 'change', message: '请输入账号' }],
password: [{ required: true, trigger: 'change', validator: validatePassword }],
};
function validatePassword(rule, value, callback) {
if (!value || value.length < 6) {
callback(new Error('密码最少6位'));
} else {
callback();
}
}
function handleLogin() {
proxy.$refs.loginFormRef.validate((valid) => {
if (valid) {
login(loginForm).then(() => {
let fullPath = proxy.$route.fullPath;
if (fullPath.startsWith('/login?redirect=')) {
let lastPath = fullPath.replace('/login?redirect=', '');
// 跳转到上次退出的页面
proxy.$router.push({ path: lastPath });
} else {
// 跳转到首页
proxy.$router.push({ path: '/' });
}
});
}
});
}
</script>
<style lang="scss" scoped>
.copyright {
width: 100%;
position: absolute;
bottom: 0;
font-size: 12px;
text-align: center;
color: #ccc;
}
</style>
2.2 部分属性解释
base-wrapper:在外面包裹一层
flex-center-center:flex布局居中
2.3 样式可在浏览器元素窗口先调试
2.4 其他
store/user.js:存储用户的权限信息
登录请求 - 返回token - 请求拦截器(判断是否登录,是就把token放到请求头) -
然后跳转到首页(新建src/views/dashboard/index.vue并配置路由) -
动态路由
概述:从后端获取路由信息添加到前端静态路由使用
src/router/permission.js
import router from '@/router';
import store from '@/store';
// 进度条
// import NProgress from 'nprogress'; // 导入 nprogress模块
// import 'nprogress/nprogress.css'; // 导入样式
// NProgress.configure({ showSpinner: true }); // 显示右上角螺旋加载提示
// 白名单路由
const whiteList = ['/login', '/test', '/test-layout'];
// 是否存在路由
let hasRouter = false;
/**
* 全局前置守卫 https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
* next();放行 -- 其它的路由/页面跳转在没放行之前都会走 router.beforeEach()
*/
router.beforeEach(async (to, from, next) => {
// NProgress.start(); // 开启进度条
let useUserStore = store.user.useUserStore();
const { getUserInfo, logout } = useUserStore;
let { isLogin, routerList } = toRefs(useUserStore); // 响应式
if (isLogin.value) {
// 已经登录后的操作
if (to.path === '/login') {
if (to.fullPath.startsWith('/login?redirect=')) {
let lastPath = to.fullPath.replace('/login?redirect=', '');
next({ path: lastPath }); // 跳转到上次退出的页面
} else {
next({ path: '/' }); // 跳转到首页
}
} else {
try {
if (hasRouter) { // 是否获取过后端返回的路由信息
next(); // 放行
} else {
// 请求接口数据,动态添加可访问路由表
await getUserInfo(); // 获取用户信息/获取用户权限 - 获取路由信息
routerList.value.forEach((e) => router.addRoute(e)); // 路由添加进去之后不会及时更新,需要重新加载一次
// console.log('全部路由数据:', router.getRoutes());
hasRouter = true;
next({ ...to, replace: true }); // 跳转
}
} catch (error) {
console.log('刷新页面时获取权限异常:', error);
// ElMessage.error('错误:' + error || 'Has Error');
}
}
} else {
// 未登录
if (whiteList.indexOf(to.path) !== -1) {
next(); // 白名单页面(eg: 登录页面)放行
} else {
next(`/login?redirect=${to.path}`); // 不在白名单页面/无权限 跳转到登录页面,后面拼接的path是提供给登录成功后跳转到要访问的页面
}
}
});
// 全局后置钩子
router.afterEach(() => {
// NProgress.done(); // 完成进度条
});
导入到main.js
// 路由权限
import '@/router/permission';
tips1:前置钩子与后置钩子:可以加例如加载动画与取消加载动画的功能
tips2:后端路由权限数据
tips3:meta的数据主要用于左侧的动态菜单
页面布局
概述:APP.vue加载src/layout下的布局视图
拷贝layout文件夹到工程
App.vue引用
store/modules/settings
sidebar.vue:侧边栏
sidebar-item.vue:展示一二级菜单等
navbar.vue:导航栏
tabs-view.vue:选项卡式导航界面
app-main.vue:主页,显示路由视图
自定义指令实现按钮权限/角色权限
一、自定义指令
模块化 src/directive/index.js
// export { hasPerm, hasRole } from './btn-perm.js';
// 拿到当前目录下所有指令数据
const modulesFiles = import.meta.glob('./*.js', { eager: true });
const result = {};
for (const fileKey in modulesFiles) {
const value = modulesFiles[fileKey];
Object.keys(value).forEach((key) => {
result[key] = value[key];
});
}
// console.log(666, result);
export default result;
按钮权限指令 src/directive/v-has-perm.js
import store from '@/store';
/**
* 自定义按钮权限指令 `v-has-perm`
* single: v-has-perm="'sys:user:add'"
* array : v-has-perm="['sys:user:add','sys:user:edit']"
*/
export const hasPerm = {
mounted(el, binding) {
// 拿到DOM绑定需要的按钮权限
const { value } = binding;
if (!value) {
throw new Error("need perms! Like v-has-perm=\"['sys:user:add','sys:user:edit']\"");
}
const requiredPermList = value instanceof Array ? value : [value];
let { isLogin, userObj } = toRefs(store.user.useUserStore());
let hasPerm = false;
if (isLogin.value) {
const currentRouteUrl = window.location.hash.replace('#', ''); // eg: /system/user
const btnPermList = getBtnPermList(currentRouteUrl, userObj.value.permissionTreeList, []);
// console.log('按钮权限判断-当前路由:', currentRouteUrl, '拥有的按钮权限:', btnPermList);
hasPerm = btnPermList.some((btnPerm) => {
return requiredPermList.includes(btnPerm);
});
}
if (!hasPerm) {
el.parentNode && el.parentNode.removeChild(el);
}
},
};
/**
* 获取当前路由下的按钮权限
* @param currentRouteUrl 当前路由url eg: system/user/index
* @param permissionTreeList 权限菜单树
* @param btnPermList 按钮权限
* @returns 按钮权限
*/
function getBtnPermList(currentRouteUrl, permissionTreeList, btnPermList) {
if (permissionTreeList.length === 0) {
return btnPermList;
}
permissionTreeList.forEach((e) => {
if (e.meta.fullPath === currentRouteUrl || e.meta.fullPath === currentRouteUrl + '/index') {
e.permList.forEach((item) => {
btnPermList.push(item.btnPerm);
});
}
const childList = e.children;
if (childList) {
getBtnPermList(currentRouteUrl, childList, btnPermList);
} else {
return btnPermList;
}
});
return btnPermList;
}
角色权限指令 src/directive/v-has-role.js
import store from '@/store';
/**
* 自定义角色权限指令 `v-has-role`
* single: v-has-role="'admin'"
* array : v-has-role="['admin','test']"
*/
export const hasRole = {
mounted(el, binding) {
// 拿到DOM绑定需要的角色编码
const { value } = binding;
if (!value) {
throw new Error("need roles! Like v-has-role=\"['admin','test']\"");
}
const requiredCodeList = value instanceof Array ? value : [value];
let { isLogin, userObj } = toRefs(store.user.useUserStore());
let hasRole = false;
if (isLogin.value) {
hasRole = userObj.value.roleCodeList.some((roleCode) => {
return requiredCodeList.includes(roleCode);
});
}
if (!hasRole) {
el.parentNode && el.parentNode.removeChild(el);
}
},
};
二、注册自定义指令
src/main.js
// 注册自定义指令(eg:按钮权限)
import directive from '@/directive/index.js';
Object.keys(directive).forEach((key) => {
app.directive(key, directive[key]);
});
三、页面中测试使用
<el-button v-has-perm="'sys:user:add'" type="primary">添加</el-button>
<el-button v-has-perm="['sys:user:add', 'sys:user:edit']" type="primary">编辑</el-button>
<el-button v-has-role="'super_admin'" type="primary">添加</el-button>
<el-button v-has-role="['super_admin', 'test']" type="primary">编辑</el-button>
页面CRUD
例如:src/views/system/role/list.vue
<template>
<base-wrapper>
<base-header>
<el-input v-model="listQuery.name" clearable placeholder="角色名称" style="width: 200px" @clear="refreshTableData" />
<el-button type="primary" @click="refreshTableData">查询</el-button>
<template #right>
<el-button type="primary" @click="add">添加</el-button>
</template>
</base-header>
<base-table-p ref="baseTableRef" api="sys_role.listPage" :params="listQuery">
<el-table-column prop="name" label="角色名" />
<el-table-column prop="code" label="角色编码" />
<el-table-column label="操作" align="center" width="250">
<template #default="scope">
<el-button link @click="update(scope.row)">编辑</el-button>
<router-link :to="{ path: '/system/role-edit', query: { id: scope.row.roleId } }">
<el-button link>权限</el-button>
</router-link>
<base-delete-btn @ok="deleteData(scope.row.roleId)" />
</template>
</el-table-column>
</base-table-p>
<base-dialog v-model="dialogVisible" :title="textMap[dialogStatus]" width="50%">
<el-form ref="roleFormRef" :model="roleForm" :rules="rules" label-width="100px">
<el-form-item label="角色名:" prop="name">
<el-input v-model="roleForm.name" placeholder="请输入角色名" />
</el-form-item>
<el-form-item label="角色编码:" prop="code">
<el-input v-model="roleForm.code" placeholder="请输入角色编码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="saveForm">确 定</el-button>
</template>
</base-dialog>
</base-wrapper>
</template>
<script setup>
const { proxy } = getCurrentInstance();
let roleForm = $ref({});
let dialogVisible = $ref(false);
let listQuery = $ref({});
let rules = {
code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
};
let dialogStatus = $ref('');
let textMap = $ref({ update: '编辑', add: '添加' });
async function refreshTableData() {
proxy.$refs.baseTableRef.refresh();
}
function saveForm() {
proxy.$refs.roleFormRef.validate(async (valid) => {
if (valid) {
let res = await proxy.$api.sys_role[roleForm.roleId ? 'update' : 'add'](roleForm);
proxy.submitOk(res.msg);
refreshTableData();
dialogVisible = false;
}
});
}
function update(row) {
roleForm = Object.assign({}, row);
dialogVisible = true;
dialogStatus = 'update';
}
function add() {
dialogVisible = true;
dialogStatus = 'add';
roleForm.roleId = null;
roleForm.name = '';
roleForm.code = '';
}
async function deleteData(id) {
let res = await proxy.$api.sys_role.delete(id);
proxy.submitOk(res.msg);
refreshTableData();
}
</script>
<style scoped></style>
docker+nginx快速部署项目
拷贝Docker文件夹到工程中
Docker/nginx/nginx.conf,修改proxy_pass线上接口地址
打包工程,npm run build:prod
# 将dist文件中的内容复制到 `/usr/share/nginx/html/` 这个目录下面
COPY dist/ /usr/share/nginx/html/
# 用本地配置文件来替换nginx镜像里的默认配置
COPY Docker/nginx/nginx.conf /etc/nginx/nginx.conf
# 通过docker.file构建docker镜像
docker build -f ./Docker/Dockerfile -t "registry.cn-hangzhou.aliyuncs.com/zhengqingya/smallboot-web:prod" . --no-cache
# 推送到远程仓库
docker push registry.cn-hangzhou.aliyuncs.com/zhengqingya/smallboot-web:prod
# 运行
docker run -d --name web -p 80:80 --restart=always registry.cn-hangzhou.aliyuncs.com/zhengqingya/smallboot-web:prod