结束
一次完整的面试流程就是这样啦,小编综合了腾讯的面试题做了一份前端面试题PDF文档,里面有面试题的详细解析,分享给小伙伴们,有没有需要的小伙伴们都去领取!
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
import { Store } from '@/store'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store;
}
}
package.json
package.json
{
"name": "small-tools-web",
"private": true,
"version": "1.0.1",
"type": "module",
"scripts": {
// 运行
"dev": "vite --mode dev",
// 构建生成 dist 文件夹
"build:prod": "vue-tsc --noEmit && vite build --mode prod",
// 在本地启动一个静态 Web 服务器,将 dist 文件夹运行在 http://localhost:8080
"preview": "vite preview --port 8080 --mode prod",
// eslint检查
"lint": "eslint --ext .js --ext .ts --ext .vue src",
// eslint自动修复
"lint-fix": "eslint --ext .js --ext .ts --ext .vue src --fix",
// prettier自动格式化代码
"prettier": "prettier --write ."
},
"dependencies": {
"vue": "^3.2.37",
"vue-router": "^4.1.2"
},
"devDependencies": {
"@types/node": "^18.0.5",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@vitejs/plugin-vue": "^3.0.0",
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.2.0",
"prettier": "^2.7.1",
"typescript": "^4.6.4",
"vite": "^3.0.0",
"vue-tsc": "^0.38.4"
}
}
# devDependencies: 里面的插件只用于开发环境,不用于生产环境
# dependencies: 需要发布到生产环境的
# 写入到 dependencies 对象
npm i module_name -S => npm install module_name --save
# 写入到 devDependencies 对象
npm i module_name -D => npm install module_name --save-dev
Sass
安装
npm install sass --save-dev
Vue-Router
https://router.vuejs.org/zh
安装
npm install vue-router@4
入门配置
src/router/index.ts
import {createRouter, createWebHashHistory} from 'vue-router';
// 静态路由
export const routes = [
{
path: '/login',
component: () => import('@/views/login/index.vue')
},
{
path: '/404',
component: () => import('@/views/error-page/404.vue')
},
];
// 创建路由
const router = createRouter({
history: createWebHashHistory(),
routes
});
export default router;
src/main.ts
// \*\*\*\*\*\* ↓↓↓ 路由 ↓↓↓ \*\*\*\*\*\*
import router from '@/router';
const app = createApp(App);
app.use(router);
app.mount('#app')
src/views/error-page/404.vue
<template>
<h1>404</h1>
</template>
src/App.vue
<template>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view/>
</template>
访问http://ip:端口/#/404
Element-Plus
https://element-plus.gitee.io/zh-CN
安装
npm install element-plus --save
npm install @element-plus/icons-vue
配置
main.ts
// \*\*\*\*\*\* ↓↓↓ element-plus ↓↓↓ \*\*\*\*\*\*
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import \* as ElementPlusIconsVue from '@element-plus/icons-vue'
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
Volar 支持
tsconfig.json
中通过compilerOptions.types
指定全局组件类型
{
"compilerOptions": {
"types": [
"element-plus/global"
]
}
}
demo
<template>
<el-button link>按钮</el-button>
<el-icon class="is-loading">
<Loading />
</el-icon>
<div>
<el-button :icon="Search" circle />
<el-button type="primary" :icon="Edit" circle />
<el-button type="success" :icon="Check" circle />
<el-button type="info" :icon="Message" circle />
<el-button type="warning" :icon="Star" circle />
<el-button type="danger" :icon="Delete" circle />
</div>
</template>
<script lang="ts" setup>
import { Check, Delete, Edit, Message, Search, Star, } from '@element-plus/icons-vue'
</script>
自定义样式
自定义样式
main.ts
import '@/styles/index.scss';
src/styles/index.scss
@import './element-plus-theme';
body {
background-color: #021b32;
}
src/styles/element-plus-theme.scss
// ****** ↓↓↓ 覆盖 element-plus 的样式 ↓↓↓ ******
// 按钮
.el-button--text {
// background-color: #8f6732 !important;
margin-left: 3px;
border: none !important;
}
Pinia
https://pinia.vuejs.org
安装
npm install pinia --save
配置
main.ts
const app = createApp(App)
// pinia
import { createPinia } from 'pinia';
const pinia = createPinia()
app.use(pinia)
// store
import useStore from "@/store";
app.config.globalProperties.$store = useStore();
app.mount('#app')
使用
src/store/index.ts
import useAppStore from './modules/app';
const useStore = () => ({
app: useAppStore()
});
export default useStore;
src/store/modules/app.ts
import { AppState } from '@/types/store/app';
import { localStorage } from '@/utils/storage';
import { defineStore } from 'pinia';
const useAppStore = defineStore({
id: 'app',
state: (): AppState => ({
name: localStorage.get('name') || 'Small Tools',
}),
actions: {
setName(name: string) {
this.name = name;
localStorage.set('name', name);
}
}
});
export default useAppStore;
src/utils/storage.ts
/\*\*
\* window.localStorage => 浏览器永久存储,用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。
\*/
export const localStorage = {
set(key: string, val: any) {
window.localStorage.setItem(key, JSON.stringify(val));
},
get(key: string) {
const json: any = window.localStorage.getItem(key);
return JSON.parse(json);
},
remove(key: string) {
window.localStorage.removeItem(key);
},
clear() {
window.localStorage.clear();
},
};
/\*\*
\* window.sessionStorage => 浏览器本地存储,数据保存在当前会话中,在关闭窗口或标签页之后将会删除这些数据。
\*/
export const sessionStorage = {
set(key: string, val: any) {
window.sessionStorage.setItem(key, JSON.stringify(val));
},
get(key: string) {
const json: any = window.sessionStorage.getItem(key);
return JSON.parse(json);
},
remove(key: string) {
window.sessionStorage.removeItem(key);
},
clear() {
window.sessionStorage.clear();
},
};
页面引用
<template>
<p>store: {{ name }}</p>
<p>store: {{ app.name }}</p>
<p>store: {{ $store.app.name }}</p>
<el-button @click="changeStore('666')">change store</el-button>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import useStore from "@/store";
const { app } = useStore()
// const name = ref(app.name)
// 响应式
const { name: name } = storeToRefs(app)
function changeStore(value: string) {
app.setName(value)
}
</script>
Axios和API封装
http://www.axios-js.com/zh-cn/docs
安装
cnpm install axios --save
axios工具封装
src/utils/request.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { localStorage } from '@/utils/storage';
import useStore from '@/store';
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE\_APP\_BASE\_API,
// 请求超时时间:50s
timeout: 50000,
headers: { 'Content-Type': 'application/json;charset=utf-8' },
});
// 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
if (!config.headers) {
throw new Error(
`Expected 'config' and 'config.headers' not to be undefined`
);
}
const { user } = useStore();
if (user.token) {
// 授权认证
config.headers.Authorization = user.token;
}
// 租户ID
config.headers['TENANT\_ID'] = '1'
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data;
const { code, msg } = res;
if (code === 200) {
return res;
} else {
// token过期
if (code === -1) {
ElMessageBox.confirm("您的登录账号已失效,请重新登录", {
confirmButtonText: "再次登录",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
// 清除浏览器全部缓存
localStorage.clear();
// 跳转登录页
window.location.href = '/';
location.reload();
});
} else {
ElMessage({
message: msg || '系统出错',
type: 'error',
duration: 5 \* 1000
});
}
return Promise.reject(new Error(msg || 'Error'));
}
},
(error) => {
const { msg } = error.response.data;
// 未认证
if (error.response.status === 401) {
ElMessageBox.confirm("您的登录账号已失效,请重新登录", {
confirmButtonText: "再次登录",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
// 清除浏览器全部缓存
localStorage.clear();
// 跳转登录页
window.location.href = '/';
location.reload();
});
} else {
ElMessage({
message: "网络异常,请稍后再试!",
type: "error",
duration: 5 \* 1000
});
return Promise.reject(new Error(msg || 'Error'));
}
}
);
// 导出实例
export default service;
api封装
`src/api/
// 拿到所有api
const modulesFiles = import.meta.globEager('./\*/\*.\*');
const modules: any = {};
for (const key in modulesFiles) {
const moduleName = key.replace(/(.\*\/)\*([^.]+).\*/gi, '$2');
const value: any = modulesFiles[key];
if (value.default) {
// 兼容js
modules[moduleName] = value.default;
} else {
// 兼容ts
modules[moduleName] = value;
}
}
// console.log(666, modules);
export default modules;
main.ts
const app = createApp(App);
// 配置全局api
import api from '@/api'
app.config.globalProperties.$api = api;
app.mount('#app')
api调用demo
src/api/system/sys_login.ts
import { Captcha } from '@/types/api/system/login';
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
// 获取验证码
export function getCaptcha(): AxiosPromise<Captcha> {
return request({
url: '/captcha?t=' + new Date().getTime().toString(),
method: 'get',
});
}
src/types/api/system/login.d.ts
// 验证码类型声明
export interface Captcha {
img: string;
uuid: string;
}
src/views/login/index.vue
<template>
<p>Hello...</p>
</template>
<script lang="ts" setup>
import { getCurrentInstance } from 'vue';
// 组件实例
const { proxy }: any = getCurrentInstance();
// 获取验证码
async function handleCaptcha() {
const res = await proxy.$api.sys_login.getCaptcha()
console.log('res:', res);
}
handleCaptcha()
</script>
权限
用户登录成功后将用户信息存储到store中
src/views/login/index.vue
function handleLogin() {
loginFormRef.value.validate((valid: boolean) => {
if (valid) {
state.loading = true;
user.login(state.loginForm).then(() => {
router.push({ path: state.redirect || '/', query: state.otherQuery });
state.loading = false;
}).catch(() => {
state.loading = false;
handleCaptchaGenerate();
});
} else {
return false;
}
});
}
store
src/store/modules/user.ts
import { defineStore } from 'pinia';
import { LoginFormData } from '@/types/api/system/login';
import { UserState } from '@/types/store/user';
import { localStorage } from '@/utils/storage';
import { login, logout } from '@/api/system/sys\_login';
import { getUserPerm } from '@/api/system/user';
import { resetRouter } from '@/router';
const useUserStore = defineStore({
id: 'user',
state: (): UserState => ({
userId: 0,
openId: '',
token: localStorage.get('token') || '',
nickname: '',
avatarUrl: '',
roleNames: [],
permissionTreeList: [],
}),
actions: {
async RESET\_STATE() {
this.$reset();
},
/\*\*
\* 登录
\*/
login(loginData: LoginFormData) {
const { username, password, code, uuid } = loginData;
return new Promise((resolve, reject) => {
login({
username: username.trim(),
password: password,
grant_type: 'captcha',
code: code,
uuid: uuid,
}).then((response) => {
const { tokenType, value } = response.data;
const token = tokenType + ' ' + value;
localStorage.set('token', token);
this.token = token;
resolve(token);
}).catch((error) => {
reject(error);
});
});
},
/\*\*
\* 获取用户信息(昵称、头像、角色集合、权限集合)
\*/
getUserInfo() {
return new Promise((resolve, reject) => {
getUserPerm().then(({ data }: any) => {
if (!data) {
return reject('Verification failed, please Login again.');
}
const { userId, openId, nickname, avatarUrl, roleNames, permissionTreeList } = data;
this.userId = userId;
this.openId = openId;
this.nickname = nickname;
this.avatarUrl = avatarUrl;
this.roleNames = roleNames;
this.permissionTreeList = permissionTreeList;
resolve(data);
}).catch((error: any) => {
reject(error);
});
});
},
/\*\*
\* 注销
\*/
logout() {
return new Promise((resolve, reject) => {
logout().then(() => {
localStorage.remove('token');
this.RESET\_STATE();
resetRouter();
resolve(null);
}).catch((error) => {
reject(error);
});
});
},
/\*\*
\* 清除 Token
\*/
resetToken() {
return new Promise((resolve) => {
localStorage.remove('token');
this.RESET\_STATE();
resolve(null);
});
},
},
});
export default useUserStore;
src/store/modules/permission.ts
import { PermissionState } from '@/types/store/permission';
import { RouteRecordRaw } from 'vue-router';
import { defineStore } from 'pinia';
import { constantRoutes } from '@/router';
import useStore from '@/store';
const modules = import.meta.glob('../../views/\*\*/\*\*.vue');
export const Layout = () => import('@/layout/index.vue');
export const parentView = () => import('@/layout/parentView.vue');
export const filterAsyncRoutes = (
routes: RouteRecordRaw[],
roleNames: string[]
) => {
const res: RouteRecordRaw[] = [];
routes.forEach((route) => {
const tmp = { ...route } as any;
if (tmp.component === 'Layout') {
tmp.component = Layout;
} else if (tmp.component === 'parentView') {
tmp.component = parentView
} else {
const component = modules[`../../views/${tmp.component}.vue`] as any;
if (component) {
tmp.component = modules[`../../views/${tmp.component}.vue`];
} else {
tmp.component = modules[`../../views/error-page/404.vue`];
}
}
res.push(tmp);
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roleNames);
}
});
return res;
};
/\*\*
\* 侧边栏权限路由
\*/
const usePermissionStore = defineStore({
id: 'permission',
state: (): PermissionState => ({
routes: [],
addRoutes: [],
}),
actions: {
setRoutes(routes: RouteRecordRaw[]) {
this.addRoutes = routes;
this.routes = constantRoutes.concat(routes);
},
generateRoutes(roleNames: string[]) {
const { user } = useStore();
const accessedRoutes = filterAsyncRoutes(user.permissionTreeList, roleNames);
return new Promise((resolve, reject) => {
this.setRoutes(accessedRoutes);
resolve(accessedRoutes);
});
},
},
});
export default usePermissionStore;
router
src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import useStore from '@/store';
// 静态路由
export const constantRoutes: Array<RouteRecordRaw> = [
{
path: '/login',
component: () => import('@/views/login/index.vue')
},
{
path: '/test',
component: () => import('@/views/test/index.vue')
},
{
path: '/404',
component: () => import('@/views/error-page/404.vue')
},
];
// 创建路由
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes as RouteRecordRaw[],
});
// 重置路由
export function resetRouter() {
const { permission } = useStore();
permission.routes.forEach((route) => {
const name = route.name;
if (name && router.hasRoute(name)) {
router.removeRoute(name);
}
});
}
export default router;
刷新路由时权限
permission.ts
import router from '@/router';
import { ElMessage } from 'element-plus';
import useStore from '@/store';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
NProgress.configure({ showSpinner: false }); // 进度环显示/隐藏
// 白名单路由
const whiteList = ['/login', '/auth-redirect'];
router.beforeEach(async (to, from, next) => {
NProgress.start();
const { user, permission } = useStore();
const hasToken = user.token;
if (hasToken) {
// 登录成功,跳转到首页
if (to.path === '/login') {
next({ path: '/' });
NProgress.done();
} else {
const hasGetUserInfo = user.roleNames.length > 0;
if (hasGetUserInfo) {
if (to.matched.length === 0) {
from.name ? next({ name: from.name as any }) : next('/401');
} else {
next();
}
} else {
try {
await user.getUserInfo();
const roleNames = user.roleNames;
const accessRoutes: any = await permission.generateRoutes(roleNames);
accessRoutes.forEach((route: any) => {
router.addRoute(route);
});
next({ ...to, replace: true });
} catch (error) {
// 移除 token 并跳转登录页
await user.resetToken();
ElMessage.error((error as any) || 'Has Error');
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
}
} else {
// 未登录可以访问白名单页面(登录页面)
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
});
router.afterEach(() => {
NProgress.done();
});
main.ts
// 路由权限
import '@/permission';
动态路由布局
动态路由布局相关页面
具体见源码src/layout
部分
src/layout/index.vue
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<Sidebar class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView }" class="main-container">
<div :class="{ 'fixed-header': true }">
<navbar />
<tags-view v-if="needTagsView" />
</div>
<app-main />
</div>
</div>
</template>
<script setup lang="ts">
import { computed, watchEffect } from 'vue';
import { useWindowSize } from '@vueuse/core';
import { AppMain, Navbar, TagsView } from './components/index';
import Sidebar from './components/Sidebar/index.vue';
import useStore from '@/store';
const { width } = useWindowSize();
const WIDTH = 992;
const { app, setting } = useStore();
const sidebar = computed(() => app.sidebar);
const device = computed(() => app.device);
const needTagsView = computed(() => setting.tagsView);
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
openSidebar: sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation,
mobile: device.value === 'mobile',
}));
watchEffect(() => {
if (width.value < WIDTH) {
app.toggleDevice('mobile');
app.closeSideBar(true);
} else {
app.toggleDevice('desktop');
}
});
function handleClickOutside() {
app.closeSideBar(false);
}
</script>
<style lang="scss" scoped>
@import '@/styles/mixin.scss';
@import '@/styles/variables.module.scss';
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.mobile .fixed-header {
width: 100%;
}
</style>
src/layout/parentView.vue
<template>
<router-view />
</template>
<script lang="ts">
export default {
name: 'ParentView',
data() {
return {};
},
methods: {},
};
</script>
<style lang="scss" scoped></style>
src/layout/components/AppMain.vue
<template>
<section class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="router-fade" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import useStore from '@/store';
const { tagsView } = useStore();
const cachedViews = computed(() => tagsView.cachedViews);
</script>
<style lang="scss" scoped>
.app-main {
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header+.app-main {
padding-top: 50px;
}
.hasTagsView {
.app-main {
min-height: calc(100vh - 84px);
}
.fixed-header+.app-main {
padding-top: 84px;
}
}
</style>
<style lang="scss">
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>
src/layout/components/index.ts
export { default as Navbar } from './Navbar.vue';
export { default as AppMain } from './AppMain.vue';
export { default as TagsView } from './TagsView/index.vue';
src/layout/components/Navbar.vue
<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container"
@toggleClick="toggleSideBar" />
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
<div class="right-menu">
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div>
<el-avatar :src="avatarUrl" />
<div class="user-info">
<span>{{ nickname }}</span>
<CaretBottom style="width: 0.6em; height: 0.6em; margin-left: 5px" />
</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/">
<el-dropdown-item>首页</el-dropdown-item>
</router-link>
<router-link to="/system/personal-center">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<a target="\_blank" href="https://gitee.com/zhengqingya">
<el-dropdown-item>Gitee</el-dropdown-item>
</a>
<router-link to="/other/anonymity">
<el-dropdown-item>提建议</el-dropdown-item>
</router-link>
<el-dropdown-item divided @click="logout"> 退出 </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessageBox } from 'element-plus';
import useStore from '@/store';
// 组件依赖
import Breadcrumb from '@/components/Breadcrumb/index.vue';
import Hamburger from '@/components/Hamburger/index.vue';
const { app, user, tagsView } = useStore();
const route = useRoute();
const router = useRouter();
const sidebar = computed(() => app.sidebar);
const device = computed(() => app.device);
const avatarUrl = computed(() => user.avatarUrl);
const nickname = computed(() => user.nickname);
function toggleSideBar() {
app.toggleSidebar();
}
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
user
.logout()
.then(() => {
tagsView.delAllViews();
})
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<style lang="scss" scoped>
ul {
list-style: none;
margin: 0;
padding: 0;
}
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: $dark\_main\_color;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.breadcrumb-container {
float: left;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
}
.avatar-container {
margin-right: 60px;
.user-info {
cursor: pointer;
position: absolute;
right: -50px;
top: 15px;
font-size: 18px;
}
}
}
}
</style>
src/layout/components/Sidebar/index.vue
<template>
<div :class="{ 'has-logo': true }">
<logo :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu :default-active="activeMenu" :collapse="isCollapse" :background-color="variables.menuBg"
:text-color="variables.menuText" :active-text-color="variables.menuActiveText" :unique-opened="true"
:collapse-transition="false" mode="vertical">
<sidebar-item v-for="route in routes" :item="route" :key="route.path" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import SidebarItem from './SidebarItem.vue';
import Logo from './Logo.vue';
import variables from '@/styles/variables.module.scss';
import useStore from '@/store';
const { permission, app } = useStore();
const route = useRoute();
const routes = computed(() => permission.routes);
const isCollapse = computed(() => !app.sidebar.opened);
const activeMenu = computed(() => {
const { meta, path } = route;
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu as string;
}
return path;
});
</script>
src/layout/components/Sidebar/Link.vue
<template>
<a v-if="isExternal(to)" :href="to" target="\_blank" rel="noopener">
<slot />
</a>
<div v-else @click="push">
<slot />
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { isExternal } from '@/utils/validate';
import { useRouter } from 'vue-router';
import useStore from '@/store';
const { app } = useStore();
const sidebar = computed(() => app.sidebar);
const device = computed(() => app.device);
export default defineComponent({
props: {
to: {
type: String,
required: true,
},
},
setup(props) {
const router = useRouter();
const push = () => {
if (device.value === 'mobile' && sidebar.value.opened == true) {
app.closeSideBar(false);
}
router.push(props.to).catch((err) => {
console.log(err);
});
};
return {
push,
isExternal,
};
},
});
</script>
src/layout/components/Sidebar/Logo.vue
<template>
<div class="sidebar-logo-container" :class="{ collapse: isCollapse }">
<transition name="sidebarLogoFade">
<router-link
v-if="collapse"
key="collapse"
class="sidebar-logo-link"
to="/"
>
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 v-else class="sidebar-title">{{ title }}</h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 class="sidebar-title">{{ title }}</h1>
</router-link>
</transition>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, toRefs } from 'vue';
const props = defineProps({
collapse: {
type: Boolean,
required: true,
},
});
const state = reactive({
isCollapse: props.collapse,
});
const { isCollapse } = toRefs(state);
const title = ref('Small Tools');
const logo = ref(
'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
);
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
.sidebar-logo-container {
position: relative;
width: 100%;
height: 50px;
line-height: 50px;
background: #00284c;
text-align: center;
overflow: hidden;
& .sidebar-logo-link {
height: 100%;
width: 100%;
& .sidebar-logo {
width: 32px;
height: 32px;
vertical-align: middle;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
margin-left: 12px;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
}
</style>
src/layout/components/Sidebar/SidebarItem.vue
<template>
<div v-if="!item.hidden">
<template v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
(!item.meta || !item.meta.alwaysShow)
">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<el-icon v-if="onlyOneChild.meta && onlyOneChild.meta.icon">
<component :is="onlyOneChild.meta.icon" />
</el-icon>
<template #title>
{{ onlyOneChild.meta.title }}
</template>
</el-menu-item>
</app-link>
</template>
<el-sub-menu v-else :index="resolvePath(item.path)">
<template #title>
<el-icon v-if="item.meta && item.meta.icon">
<component :is="item.meta.icon" />
</el-icon>
<span v-if="item.meta && item.meta.title">{{
item.meta.title
}}</span>
</template>
<sidebar-item v-for="child in item.children" :key="child.path" :item="child" :is-nest="true"
:base-path="resolvePath(child.path)" class="nest-menu" />
</el-sub-menu>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import path from 'path-browserify';
import { isExternal } from '@/utils/validate';
import AppLink from './Link.vue';
const props = defineProps({
item: {
type: Object,
required: true,
},
isNest: {
type: Boolean,
required: false,
},
basePath: {
type: String,
required: true,
},
});
const onlyOneChild = ref();
function hasOneShowingChild(children = [] as any, parent: any) {
if (!children) {
children = [];
}
const showingChildren = children.filter((item: any) => {
if (item.meta && item.meta.hidden) {
return false;
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item;
return true;
}
});
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true;
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true };
return true;
}
return false;
}
function resolvePath(routePath: string) {
if (isExternal(routePath)) {
return routePath;
}
if (isExternal(props.basePath)) {
return props.basePath;
}
return path.resolve(props.basePath, routePath);
}
</script>
<style lang="scss" scoped>
</style>
src/layout/components/TagsView/index.vue
HTTP
-
HTTP 报文结构是怎样的?
-
HTTP有哪些请求方法?
-
GET 和 POST 有什么区别?
-
如何理解 URI?
-
如何理解 HTTP 状态码?
-
简要概括一下 HTTP 的特点?HTTP 有哪些缺点?
-
对 Accept 系列字段了解多少?
-
对于定长和不定长的数据,HTTP 是怎么传输的?
-
HTTP 如何处理大文件的传输?
-
HTTP 中如何处理表单数据的提交?
-
HTTP1.1 如何解决 HTTP 的队头阻塞问题?
-
对 Cookie 了解多少?
-
如何理解 HTTP 代理?
-
如何理解 HTTP 缓存及缓存代理?
-
为什么产生代理缓存?
-
源服务器的缓存控制
-
客户端的缓存控制
-
什么是跨域?浏览器如何拦截响应?如何解决?