2024年Web前端最全Vue3项目配置,成功入职阿里月薪45K

结束

一次完整的面试流程就是这样啦,小编综合了腾讯的面试题做了一份前端面试题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 缓存及缓存代理?

  • 为什么产生代理缓存?

  • 源服务器的缓存控制

  • 客户端的缓存控制

  • 什么是跨域?浏览器如何拦截响应?如何解决?

    开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值