xh's blog
文章
分类
标签
时间线
文章
分类
标签
时间线
  • 全栈开发总结

全栈开发总结

这是对于计算机设计大赛的项目进行的简略撰写以及技巧记录

前提准备

数据库格式

数据库user-list中的存储方式类似于:

{
  "username": "aaa",
  "password": "123",
  "love": [],
  "commit": []
}

例如:

{
  "username": "wanghan443",
  "password": "aaaaaa",
  "love": [
    {
      "_id": "67b16d8ede2aa1f4dcd6773c",
      "id": "google-scholar",
      "name": "Google Scholar",
      "url": "https://scholar.google.com ",
      "logo": "https://tse4-mm.cn.bing.net/th/id/OIP-C.6eJ9QJxJu2XxfjqeCVPYjgHaHa?w=179&h=184&c=7&r=0&o=5&dpr=1.5&pid=1.7",
      "description": "Google Scholar 是一个广受欢迎的免费学术搜索引擎,它索引了数百万篇学术文章,包括期刊论文、学位论文、书籍、预印本和报告等。这个平台特别适合研究人员、学生和教育工作者,他们可以利用这个工具来获取最新的研究成果和历史文献。此外,Google Scholar 还提供了引文功能,用户可以查看某篇文章被引用了多少次,以及被哪些文章引用,这对于文献综述和研究的深度分析非常有帮助。",
      "category": "research",
      "tags": [
        "学术",
        "文献",
        "搜索"
      ],
      "rating": 4.8,
      "views": 15000,
      "isPaid": false,
      "language": [
        "English"
      ],
      "accessSpeed": "快速"
    },
    {
      "_id": "67b16d8ede2aa1f4dcd6773f",
      "id": "leetcode",
      "name": "LeetCode",
      "url": "https://leetcode.com ",
      "logo": "https://leetcode.com/favicon.ico ",
      "description": "LeetCode 是一个在线编程学习平台,它提供了大量的算法题和数据结构题,供程序员练习和提高编程技能。这个平台特别适合准备技术面试和希望提高算法能力的人。LeetCode 的题目覆盖了各种难度级别,从简单到困难,用户可以根据自己的水平选择合适的题目进行练习。此外,LeetCode 还提供了一个讨论区,用户可以在其中讨论解题策略、分享知识,以及与其他程序员建立联系。",
      "category": "learning",
      "tags": [
        "算法",
        "编程",
        "练习"
      ],
      "rating": 4.9,
      "views": 17000,
      "isPaid": true,
      "language": [
        "English"
      ],
      "accessSpeed": "快速"
    },
    {
      "_id": "67b16d8ede2aa1f4dcd67740",
      "id": "bilibilibili",
      "name": "Bilibili(哔哩哔哩)",
      "url": "https://www.bili.com ",
      "logo": "https://p1.ssl.qhimg.com/t011cc0073d3c813d6b.png ",
      "description": "Bilibili不仅是一个年轻人喜欢的二次元视频平台,也是一个充满技术分享内容的学习平台。在Bilibili上,你可以找到许多编程学习视频,这些视频多由技术达人和编程专家分享,内容通俗易懂,适合新手入门。此外,Bilibili还提供了许多编程相关的直播和互动课程,可以与讲师进行实时互动,解答学习中的疑问。",
      "category": "learning",
      "tags": [
        "编程学习",
        "视频教程",
        "互动"
      ],
      "rating": 4.5,
      "views": 10000,
      "isPaid": false,
      "language": [
        "中文",
        "English"
      ],
      "accessSpeed": "快速"
    },
    {
      "_id": "67b16d8ede2aa1f4dcd6773b",
      "id": "sci-hub",
      "name": "Sci-Hub",
      "url": "https://sci-hub.se ",
      "logo": "https://sci-hub.se/favicon.ico ",
      "description": "提供免费的学术论文获取服务。",
      "category": "research",
      "tags": [
        "学术",
        "论文",
        "免费"
      ],
      "rating": 4.9,
      "views": 12000,
      "isPaid": false,
      "language": [
        "English"
      ],
      "accessSpeed": "需要科学上网"
    }
  ],
  "commit": [
    {
      "name": "是",
      "url": "https:wx.com",
      "description": "对对对",
      "category": "learning",
      "reason": "对对对"
    },
    {
      "name": "是",
      "url": "https:wx.com",
      "description": "对对对",
      "category": "learning",
      "reason": "对对对"
    },
    {
      "name": "百度",
      "url": "https:wx.com",
      "description": "读到",
      "category": "research",
      "reason": "读到"
    }
  ]
}

style.css文件的解释

当然可以,下面是对您提供的 style.css 文件中每一部分的解释:

:root 选择器

:root 是 CSS 中的伪类,代表文档的根元素(在 HTML 中是 <html> 标签)。在这里,它被用来设置一些全局样式变量和样式:

  • font-family 设置默认字体。
  • line-height 设置行高。
  • font-weight 设置字体的粗细。
  • color-scheme 定义了支持的颜色方案,这里是“light”(亮色)和“dark”(暗色)。
  • color 设置文本颜色。
  • background-color 设置背景颜色。
  • font-synthesis 禁止字体合成,以确保文本的清晰度。
  • text-rendering 提供优化的文本渲染。
  • -webkit-font-smoothing 和 -moz-osx-font-smoothing 提供字体平滑效果,改善字体在屏幕上的显示效果。

a 选择器

a 选择器用于设置超链接的样式:

  • font-weight 设置链接文本的粗细。
  • color 设置链接文本的颜色。
  • text-decoration 继承父元素的文本装饰,这里没有添加下划线。

a:hover 伪类选择器定义了当鼠标悬停在链接上时的样式:

  • color 设置悬停时的链接文本颜色。

body 选择器

body 选择器定义了页面主体的样式:

  • margin 设置页面的外边距。
  • display 设置为 flex,启用弹性盒模型布局。
  • flex-direction 设置弹性容器的主轴方向为列。
  • align-items 和 justify-content 设置弹性容器中项目的对齐方式。
  • min-width 和 min-height 设置最小宽度和高度。
  • padding 设置页面的内边距。

h1 选择器

h1 选择器定义了一级标题的样式:

  • font-size 设置标题的字体大小。
  • line-height 设置标题的行高。
  • margin 设置标题的外边距。

button 选择器

button 选择器定义了按钮的样式:

  • border-radius 设置按钮的圆角。
  • border 设置按钮的边框。
  • padding 设置按钮的内边距。
  • font-size 和 font-weight 设置按钮文本的大小和粗细。
  • font-family 设置按钮文本的字体。
  • background-color 设置按钮的背景颜色。
  • cursor 设置鼠标悬停时的光标样式。
  • transition 设置过渡效果,这里是边框颜色的过渡。

button:hover 伪类选择器定义了鼠标悬停时按钮的样式:

  • border-color 设置悬停时按钮的边框颜色。

button:focus 和 button:focus-visible 伪类选择器定义了按钮获得焦点时的样式:

  • outline 设置按钮的轮廓样式。

.card 类选择器

.card 类选择器定义了卡片的样式:

  • padding 设置卡片内边距。
  • background-color 设置卡片背景颜色。
  • border-radius 设置卡片圆角。
  • box-shadow 设置卡片阴影。
  • margin 设置卡片外边距。

#app 选择器

#app 选择器定义了 ID 为 app 的元素的样式:

  • max-width 设置最大宽度。
  • margin 设置外边距,自动水平居中。
  • padding 设置内边距。
  • text-align 设置文本对齐方式。

img 选择器

img 选择器定义了图片的样式:

  • max-width 设置图片最大宽度。
  • height 设置图片高度。
  • display 设置为 block,使图片独占一行。
  • margin 设置外边距。
  • border-radius 设置圆角。
  • box-shadow 设置阴影。

.icon 类选择器

.icon 类选择器定义了图标的样式:

  • width 和 height 设置图标大小。
  • display 设置为 flex,启用弹性盒模型布局。
  • align-items 和 justify-content 设置弹性容器中项目的对齐方式。
  • background-color 设置背景颜色。
  • border-radius 设置圆角。
  • color 设置文本颜色。
  • font-size 设置字体大小。
  • text-align 设置文本对齐方式。
  • cursor 设置鼠标悬停时的光标样式。
  • transition 设置过渡效果。
  • margin 设置外边距。

.icon:hover 伪类选择器定义了鼠标悬停时图标的样式:

  • background-color 设置悬停时的背景颜色。

@media 规则

@media 规则定义了在不同颜色方案下(亮色或暗色)的样式:

  • :root 伪类选择器在不同颜色方案下设置不同的颜色和背景颜色。
  • a:hover 伪类选择器设置悬停时链接文本的颜色。
  • button 选择器设置按钮的背景颜色。
  • .icon 类选择器设置图标的背景颜色和文本颜色。
  • .icon:hover 伪类选择器设置悬停时图标的背景颜色。

这些样式共同作用,为网页提供了一个美观、响应式的界面。

Day 1 登录实现

Pinia的使用

新版Vue3最好还是使用pinia来进行数据管理而不是vuex

Pinia 是 Vue.js 的官方状态管理库,用于集中管理和共享应用程序的状态。它是 Vuex 的替代方案,专为 Vue 3 设计,提供了更直观、简洁的 API 和更好的 TypeScript 支持。

核心概念

1. Store

Store 是 Pinia 的核心概念,用于保存状态和业务逻辑。它包含三个主要部分:

  • State(状态):响应式对象,存储应用的状态数据,类似于 Vue 组件中的 data。
  • Getters(计算属性):用于派生状态,类似于 Vue 组件中的 computed,是缓存的,只有在依赖的状态发生变化时才会重新计算。
  • Actions(动作):用于修改状态或执行复杂业务逻辑,可以包含异步操作。

例如我在对于登录时创建的pinia

注意authStore.ts文件要先引入需要的文件:

import { defineStore } from 'pinia';
import axios from 'axios';
import { User, Credentials } from '../types/types';

这里的 { User, Credentials }是ts中为了定义类型的接口,在后面函数中对于函数形参类型的定义需要用到:

// src/types.ts
export interface User {
    username: string;
    // 其他用户信息可以根据需要添加
  }
  
  export interface Credentials {
    username: string;
    password: string;
  }

回到authStore.ts文件,我这里创建了状态,这里的isAuthenticated是用于判断用户在当前页面是否处于登录状态的关键状态数据,这里的user则作为登录成功后显示用户名的数据:

state: () => ({
    isAuthenticated: false,
    user: null as User | null
  }),

然后就是对应的action方法,用于用户登录,注册,登出时调用的函数:

 actions: {
    async login(credentials: Credentials) {
      try {
        // 发送登录请求到后端
        const response = await axios.post('https://jy8b5cnnmg.hzh.sealos.run/login', credentials);
        console.log(response)
        // 返回的用户名是存在data.user.username中的
        // 如果登录成功,更新状态
        this.isAuthenticated = true;
        this.user = response.data.user.username;

        // 可选:保存用户信息到 localStorage 或其他存储
        localStorage.setItem('user', JSON.stringify(response.data));
      } catch (error) {
        // 如果登录失败,抛出错误
        throw new Error('Login failed. Please check your credentials.');
      }
    },

    //添加一个 register action,用于处理注册逻辑。
    async register(credentials: Credentials) {
      try {
        const response = await axios.post('https://jy8b5cnnmg.hzh.sealos.run/adduser', credentials);
        // console.log(response)
        if (response.data.code == 401 || response.data.code == 400) {
          throw new Error('注册失败,用户名已存在');
        } else {
          this.isAuthenticated = true;
          this.user = response.data;
          localStorage.setItem('user', JSON.stringify(response.data));
        }

      } catch (error) {
        throw new Error('注册失败,用户名已存在');
      }
    },

    // 处理退出登录逻辑
    logout() {
      // 登出逻辑
      this.isAuthenticated = false;
      this.user = null;
      // 可选:清除用户信息
      localStorage.removeItem('user');
    }
  }

下面是完整代码:

// src/stores/authStore.ts


export const useAuthStore = defineStore('auth', {
  state: () => ({
    isAuthenticated: false,
    user: null as User | null
  }),
  actions: {
    async login(credentials: Credentials) {
      try {
        // 发送登录请求到后端
        const response = await axios.post('https://jy8b5cnnmg.hzh.sealos.run/login', credentials);
        console.log(response)
        // 返回的用户名是存在data.user.username中的
        // 如果登录成功,更新状态
        this.isAuthenticated = true;
        this.user = response.data.user.username;

        // 可选:保存用户信息到 localStorage 或其他存储
        localStorage.setItem('user', JSON.stringify(response.data));
      } catch (error) {
        // 如果登录失败,抛出错误
        throw new Error('Login failed. Please check your credentials.');
      }
    },

    //添加一个 register action,用于处理注册逻辑。
    async register(credentials: Credentials) {
      try {
        const response = await axios.post('https://jy8b5cnnmg.hzh.sealos.run/adduser', credentials);
        // console.log(response)
        if (response.data.code == 401 || response.data.code == 400) {
          throw new Error('注册失败,用户名已存在');
        } else {
          this.isAuthenticated = true;
          this.user = response.data;
          localStorage.setItem('user', JSON.stringify(response.data));
        }

      } catch (error) {
        throw new Error('注册失败,用户名已存在');
      }
    },

    // 处理退出登录逻辑
    logout() {
      // 登出逻辑
      this.isAuthenticated = false; //这里将登录的状态标识改为false了
      this.user = null;
      // 可选:清除用户信息
      localStorage.removeItem('user');
    }
  }
});

2. defineStore

defineStore 是定义 Store 的函数,需要传入一个唯一的名称作为 ID。Store 可以通过 setup 函数或 option 对象来定义。

import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++;
    },
  },
});

3. 使用 Store

在对应的登录文件中使用store,注意要先引入:

await store.login({ username: username.value, password: password.value });执行的操作是找到store中对应名字为login的action函数并传入参数{ username: username.value, password: password.value }来执行它,执行成功没出问题就跳转到主页面。

<script setup>
  import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '../stores/authStore';

const store = useAuthStore();
const router = useRouter();
const username = ref('');
const password = ref('');

const handleLogin = async () => {
  try {
    await store.login({ username: username.value, password: password.value });
    router.push('/home'); // 登录成功后跳转到主页
  } catch (error) {
    alert('登录失败,请检查你的账号密码');
  }
};
  </script>

状态标识也有大作用,可以根据当前store中状态标识的值来显示不同的内容。

const store = useAuthStore();

// 三元表达式,用于判断登录状态的store.$state.isAuthenticated值为true的话就显示'退出登录' 
// 使用 computed 属性来动态计算 title使得它是响应式的
const title = computed(() => {
  return store.$state.isAuthenticated ? '退出登录' : '登录';
});	

特点

  1. 类型安全:Pinia 提供了对 TypeScript 的良好支持,包括类型推断和自动补全。
  2. DevTools 集成:支持 Vue DevTools,方便调试。
  3. 模块化和灵活性:支持将状态分割为不同的模块,便于大型应用的状态管理。
  4. 轻量级:代码库比 Vuex 更小。
  5. 插件支持:可以通过插件扩展功能,例如状态持久化。

使用场景

Pinia 适用于跨组件或页面共享状态的场景,例如全局主题、购物车数据等。不过,它也有适用边界,不能滥用,否则可能增加性能消耗或内存泄漏。

示例:状态持久化

通过插件,Pinia 可以实现状态持久化,例如使用 pinia-plugin-persistedstate。

JavaScript复制

import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

总结

Pinia 提供了一个现代化、高效且灵活的状态管理解决方案,特别适合 Vue 3 项目。它简化了状态管理的复杂性,同时提供了强大的功能和良好的开发体验。

路由守卫

在完成登录的功能时,最重要的就是路由守卫了

在现代前端开发中,登录操作是常见的功能之一,而路由守卫(Route Guard)是实现登录状态管理的重要工具。它主要用于控制用户在不同路由之间的访问权限,确保用户在未登录时无法访问某些受保护的页面,或者在登录后被重定向到合适的页面。以下是关于路由守卫的实现和作用的详细讲解。

简单点说,就是利用前面的pinia定义的状态变量来使用全局路由守卫(如 beforeEach)来实现登录状态的检查。当登录状态为flase的时候就不给访问页面的权限.仍然跳到登陆界面.

成功了就继续next();

router.beforeEach((to, _from, next) => {
  const authStore = useAuthStore();
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  if (requiresAuth && !authStore.isAuthenticated) {
    next({ name: 'Login' });
  } else {
    next();
  }
});

1. 路由守卫的作用

路由守卫的作用是拦截用户的路由跳转请求,根据用户的登录状态或其他条件,决定是否允许用户访问目标路由。主要作用包括:

  • 权限控制:确保用户只能访问他们有权限的页面。例如,未登录用户不能访问用户中心或管理页面。
  • 用户体验优化:避免用户在未登录状态下访问受保护的页面后被强制跳转到登录页,然后再跳转回原页面。
  • 安全性:防止用户通过直接输入 URL 绕过登录验证,访问敏感信息。
  • 状态管理:根据用户的登录状态动态调整路由行为。

2. 实现路由守卫的步骤

以 Vue.js 为例,结合 Vue Router 和 Pinia(或其他状态管理库),实现登录操作中的路由守卫,当登录状态为flase的时候就不给访问页面的权限

2.1 定义登录状态

假设我们使用 Pinia 来管理用户的登录状态,定义一个 authStore:

JavaScript复制

// stores/auth.js
import { defineStore } from 'pinia';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    isAuthenticated: false,
    user: null,
  }),
  actions: {
    login(user) {
      this.isAuthenticated = true;
      this.user = user;
    },
    logout() {
      this.isAuthenticated = false;
      this.user = null;
    },
  },
});

2.2 设置路由守卫

在 Vue Router 中,可以使用全局路由守卫(如 beforeEach)来实现登录状态的检查。

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }, // 标记需要登录才能访问的路由
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

router.beforeEach((to, from, next) => {
  const authStore = useAuthStore();
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);

  if (requiresAuth && !authStore.isAuthenticated) {
    // 如果目标路由需要登录,但用户未登录,重定向到登录页
    next({ name: 'Login' });
  } else if (to.name === 'Login' && authStore.isAuthenticated) {
    // 如果用户已登录,但尝试访问登录页,重定向到主页
    next({ name: 'Home' });
  } else {
    // 其他情况允许访问
    next();
  }
});

export default router;

2.3 在登录组件中处理登录逻辑

在登录组件中,调用 Pinia 的 login 方法,并通过路由跳转到目标页面。

vue复制

<template>
  <div>
    <h1>Login</h1>
    <button @click="handleLogin">Login</button>
  </div>
</template>

<script setup>
import { useAuthStore } from '@/stores/auth';
import { useRoute, useRouter } from 'vue-router';

const authStore = useAuthStore();
const router = useRouter();
const route = useRoute();

const handleLogin = () => {
  // 模拟登录逻辑
  authStore.login({ name: 'John Doe' });
  const redirectPath = route.query.redirect || '/';
  router.push(redirectPath);
};
</script>

3. 路由守卫的实现原理

路由守卫的核心是 Vue Router 提供的全局前置守卫 beforeEach。它会在路由跳转之前被触发,允许开发者执行自定义逻辑:

  • to:目标路由的信息。
  • from:当前路由的信息。
  • next:一个回调函数,用于决定是否允许跳转。next() 允许跳转,next(false) 阻止跳转,next({ path: '/' }) 重定向到其他路由。

通过检查目标路由的元信息(如 meta.requiresAuth)和用户的登录状态,路由守卫可以灵活地控制用户的访问权限。


4. 其他路由守卫类型

除了全局前置守卫 beforeEach,Vue Router 还提供了其他类型的路由守卫:

  • 全局后置守卫:afterEach,在路由跳转完成后执行,常用于日志记录或页面标题设置。
  • 路由独享守卫:beforeEnter,定义在路由对象中,仅对该路由生效。
  • 组件内守卫:beforeRouteEnter、beforeRouteUpdate 和 beforeRouteLeave,用于在组件内部控制路由行为。

5. 总结

路由守卫是实现登录状态管理和权限控制的关键工具。通过结合 Vue Router 和 Pinia,可以灵活地控制用户的路由跳转行为,确保应用的安全性和用户体验。路由守卫不仅可以用于登录状态管理,还可以扩展到其他复杂的权限控制场景,例如角色权限管理或动态路由加载。

后端

基于云开发

利用sealos部署数据库和云函数,真好用。。。

Sealos Cloud

云函数

网站数据的云函数

就是基本的增删改查,基于mongodb的

当然不可避免的一步就是,导入数据库,指定集合名

// 获取 MongoDB 实例
    const db = cloud.mongo.db;

    // 指定集合名称(根据您的数据库集合名称修改)
    const collectionName = 'weblist'; // 替换为您的集合名称
    const collection = db.collection(collectionName);

然后进行基本的查询。注意find方法内是{"键名":键值},返回的是对应的json对象,只有{}默认是查询所有对象

.toArray();转化为数组方便数据返回为[{对象1},{对象2}]的形式

查询一个就是await collection.findOne()

插入一个就是await collection.insertOne()

删除一个就是await collection.deleteOne()

多个就去掉One就行

import cloud from '@lafjs/cloud';

export default async function (ctx: FunctionContext) {
  try {
    // 获取 MongoDB 实例
    const db = cloud.mongo.db;

    // 指定集合名称(根据您的数据库集合名称修改)
    const collectionName = 'weblist'; // 替换为您的集合名称
    const collection = db.collection(collectionName);

    // 查询所有数据
    const result = await collection.find({}).toArray();

    // 返回查询结果
    return {
      code: 200,
      data: result,
      message: 'Data retrieved successfully'
    };
  } catch (error) {
    console.error('Error retrieving data:', error);
    return {
      code: 500,
      message: 'Error retrieving data'
    };
  }
}

基本逻辑

登录逻辑就是,前端对对应的接口进行post请求,后端根据const { username, password } = ctx.request.body;来获取请求体内数据,再通过用户名将数据库中的用户名对应的密码与请求体中的密码对比,对比成功就返回用户名与200状态码,前端根据返回的值来进行操作就行,不必赘述。

对于请求体的类型效验以及存在检查什么的也不赘述了。

import cloud from '@lafjs/cloud';

export default async function (ctx: FunctionContext) {
  try {
    // 从请求体中获取用户名和密码
    const { username, password } = ctx.request.body;

    // 校验请求参数
    if (!username || typeof username !== 'string') {
      return {
        code: 400,
        message: 'Invalid username. Username must be a non-empty string.'
      };
    }

    if (!password || typeof password !== 'string') {
      return {
        code: 400,
        message: 'Invalid password. Password must be a non-empty string.'
      };
    }

    // 获取 MongoDB 实例
    const db = cloud.mongo.db;

    // 指定集合名称
    const collectionName = 'userlist'; // 替换为您的用户集合名称
    const collection = db.collection(collectionName);

    // 根据 username 查询用户信息
    const user = await collection.findOne({ username });

    if (!user) {
      return {
        code: 401,
        message: 'Invalid credentials. Please check your username and password.'
      };
    }

    // 验证密码
    // 在实际应用中,密码应该使用加密算法(如 bcrypt)进行存储和验证
    if (user.password !== password) {  //如果user在集合中找到的密码与输入的密码不相同就返回401
      return {
        code: 401,
        message: 'Invalid credentials. Please check your username and password.'
      };
    }

    // 如果验证成功,返回用户信息
    return {
      code: 200,
      user: {
        username: user.username,
        // 其他用户信息可以根据需要返回
      },
      message: 'Login successful'
    };

  } catch (error) {
    //异常处理
    console.error('Error during login:', error);
    return {
      code: 500,
      message: 'Internal server error'
    };
  }
}

Day2 收藏夹动态实现

一点知识

可通过ref来动态绑定元素标签

<template>
  <div ref="contentRef">
    你好! {{ store.user }}
  </div>
</template>

然后再在js中相应声明响应式变量来对应绑定的标签,再利用绑定好的标签来获取标签内包含的文本内容

注意是在挂载后来更新你需要的变量

<script setup>
import { ref, onMounted } from 'vue';
import { useStore } from './store'; // 假设 store 是通过 Pinia 定义的
const store = useStore();
const contentRef = ref(null); // 创建一个 ref 来引用模板中的元素

onMounted(() => {
  if (contentRef.value) {
    const textContent = contentRef.value.textContent; // 获取元素的文本内容
    console.log('Element text content:', textContent);
  }
});
</script>

username的随时获取

在任何地方都能通过下面代码来获取当前登录用户的用户名:

import { useAuthStore } from '../stores/authStore';
const store = useAuthStore();
let usernames:any = store.user// 获取元素的文本内容

收藏夹保存更新

回到项目,借此能对当前收藏夹进行动态的更新,当点击保存按钮的时候会上传当前favorites数组内的数据到服务器并对数据库对应username的对象的love属性进行更新

const updateFavorites = async (username:string) => {
    
    // 构造发送到后端的数据
    let data:lovedata ={
      username:username,
      love:favorites.value  //传递的是包含喜爱网站的对象数组因为定义就是const favorites = ref<Website[]>([])
    }
    console.log(data)

    try {
      // 发送 POST 请求
      const response = await axios.post('https://jy8b5cnnmg.hzh.sealos.run/addlove', data, {
        headers: {
          'Content-Type': 'application/json'
        }       
      });
  
      // 更新 favorites 的值
      // favorites.value = response.data;
  
      console.log('Favorites updated successfully:', response.data);
    } catch (error:any) {
      // 错误处理
      console.error('Error updating favorites:', error);
  
      if (error.response) {
        // 请求已发出,但服务器响应的状态码不在 2xx 范围内
        console.warn(
      `Failed to update favorites. Server responded with status: ${error.response.status}`
        );
      } else if (error.request) {
        // 请求已发出,但没有收到响应
        console.warn('No response received from the server.');
      } else {
        // 在设置请求时触发了错误
        console.warn('Error setting up the request:', error.message);
      }
    }
  };
  

这是后端服务器的部分:

import cloud from '@lafjs/cloud'

export default async function (ctx: FunctionContext) {
  try {
    // 从请求体中获取用户名和喜好
    const { username, love} = ctx.request.body;

    // 校验请求参数
    if (!username) {
      return {
        code: 400,
        message: 'Invalid username. Username must be a non-empty string.'
      };
    }

    if (love== null) {
      return {
        code: 400,
        message: 'Invalid,please upload love'
      };
    }

    // 获取 MongoDB 实例
    const db = cloud.mongo.db;

    // 指定集合名称
    const collectionName = 'userlist'; // 替换为您的用户集合名称
    const collection = db.collection(collectionName);
    const res =await collection.updateOne(
      { username: username}, // 查询条件,匹配需要更新的文档
      { $set: {love:love } } // 更新操作,使用 $set 修改字段
    );
    if (res.matchedCount == 0){
      return{
        code:404,
        massage:"not find user"
      }
    }else{
      // 如果验证成功,返回用户信息
      return {
        code: 200,
        user: {
          username: username,
          // 其他用户信息可以根据需要返回
        },
        message: 'update love successfully'
      };

    }
    
  } catch (error) {
    //异常处理
    console.error('Error during login:', error);
    return {
      code: 500,
      message: 'Internal server error'
    };
  }
}

请求收藏夹数据并展示

然后是收藏夹展示部分

定义函数来通过利用传的username参数来请求数据以便于更新收藏夹

const fetchFavorites = async (username:any) =>{
  try {
    const response = await axios.post<any>(
      'https://jy8b5cnnmg.hzh.sealos.run/getlove', // 替换为你的 API 地址
      { username }
    );
    // console.log(response.data)
    // 喜爱的网站的对象数组是存在返回的reponse的data这个json对象的love属性里面的
    favorites.value = response.data.love;
  } catch (error) {
    console.error('Error fetching favorites:', error);
    throw error;
  }
}

然后关键的点是在登录的时候就对收藏夹内容进行读取请求,来展示对应账号的收藏夹

<script setup>
  import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '../stores/authStore';
import { useFavoritesStore } from '../stores/favorites';

const store = useAuthStore();
const favoritesstore =useFavoritesStore();
const router = useRouter();
const username = ref('');
const password = ref('');

const handleLogin = async () => {
  try {
    await store.login({ username: username.value, password: password.value });
    // 登录成功后更新收藏夹
    await favoritesstore.fetchFavorites(username.value);
    alert('登录成功'); // 登录成功后弹出提示
    router.push('/home'); // 登录成功后跳转到主页
      
  } catch (error) {
    alert('登录失败,请检查你的账号密码');
  }
};
  </script>

这是后端服务器部分,注意数据库的集合对象对应的属性别写错了

import cloud from '@lafjs/cloud';

export default async function (ctx) {
  try {
    // 从请求体中获取 username
    const { username } = ctx.request.body;

    // 校验 username 是否存在
    if (!username || typeof username !== 'string') {
      ctx.body = {
        code: 400,
        message: 'Invalid username. Username must be a non-empty string.'
      };
      return;
    }

    // 获取 MongoDB 实例
    const db = cloud.mongo.db;

    // 指定集合名称
    const collectionName = 'userlist'; // 替换为你的用户集合名称
    const collection = db.collection(collectionName);

    // 根据 username 查询用户信息
    const user = await collection.findOne({ username: username });
    // console.log(user.love)
    if (!user) {
      return {
        code: 404,
        message: 'User not found.'
      };
    }

    // 获取 love 属性,如果不存在则返回空数组
    const loveArray = user.love ;
    // console.log(loveArray)
    // 返回结果
    return {
      code: 200,
      love: loveArray,
      message: 'Love array retrieved successfully.'
    };
  } catch (error) {
    console.error('Error retrieving love array:', error);
    return {
      code: 500,
      message: 'Internal server error'
    };
  }
}

提交网站提交记录存入数据库

表单数据的绑定

尤其注意数据的互传注意提前定义好一个form:

const form = ref({
  name: '',
  url: '',
  description: '',
  category: '',
  reason: ''
})

然后在表单的每一个选项中利用 v-model="form.url"这种来绑定好form对象的属性值,这样数据就会随着用户的操作更新。例如网站地址:

 <div class="form-group">
          <label for="url">网站地址</label>
          <input
            type="url"
            id="url"
            v-model="form.url"
            required
            class="form-input"
          />
  </div>

提交表单的数据

在进行表单提交后,利用@submit.prevent="submitWebsite"在表单提交时触发函数,函数先判断用户是否登录,如果已经登录了就获取表单数据以及用户名(注意此处的username的类型必须是any,因为可能为null),再将这些数据封装好为对象传给submitStores里面的函数:

const submitWebsite = () => {
  // 检查用户是否登录
  if (!Authstore.$state.isAuthenticated) {
    alert("请先登录");
    router.push('/');
  } else {
    // 获取表单数据
    const formData = form.value; // 获取绑定的表单数据
    //console.log("提交的表单数据:", formData);
    let username:any = Authstore.user// 获取元素的文本内容
   // console.log(username)
    // 向后端提交数据,直接根据现有的数据封装传给函数
    submitStores.addsubmits({username: username,submit: formData});
    // 在这里可以将 formData 发送到后端
    // 示例:使用 axios 发送数据
   
    // 模拟成功提交
    alert("感谢您的提交,我们会在一周内审核并返回结果到您的邮箱");
    router.push('/home');
  }
};

post表单数据

这是submitStores里面的函数,注意因为ts的严格性,记得定义type接口自定义类型,注意异步函数async 与await 共同出现,'Content-Type': 'application/json'设置请求头表面你post的是一个json对象:

​

import { defineStore } from 'pinia'
import axios from 'axios'
import { Mysub } from '../types/types'

export const usesubmitstore = defineStore('submits', () => {
    

    const API_URL = 'https://jy8b5cnnmg.hzh.sealos.run/addsubmit';

    // 直接利用传来的封装好的对象发送请求
    const addsubmits = async (sub:Mysub) => {
        try {
          //  console.log('Submitting data:', sub);
            // 发送请求
            const response = await axios.post(API_URL, sub, {
                headers: {
                    'Content-Type': 'application/json'
                }
            });
           // console.log('Response from server:', response.data);
            
        } catch (e) {
            console.error('Error submitting data:', e);
        }
    };
    return {
        //暴露函数让其他文件可用
        addsubmits
        // updateFavorites,
        // fetchFavorites
    }
})

后端接收请求

后端的逻辑就很简单了,利用const { username, submit } = ctx.request.body;解析请求体,利用 const userExists = await collection.findOne({ username: username });判断用户是否存在,注意是新增的操作,不是覆盖原有的:

const updateResult = await collection.updateOne(
      { username: username },
      {
        $push: {
          commit: submit // 直接追加单个对象
        }
      }
    );

Day3 Promise

promise

在前端开发中,Promise 是一种用于处理异步操作的机制,它使得异步代码的编写更加清晰和易于管理。以下是 Promise 的详细用法:

1. Promise 的基本概念

  • 定义:Promise 是 ES6 中引入的一种用于处理异步操作的对象。它代表了一个异步操作的最终完成(或失败)及其结果值。

  • 状态:Promise 有三种状态:

    • Pending(进行中):初始状态,表示异步操作尚未完成。
    • Fulfilled(已成功):表示异步操作成功完成。
    • Rejected(已失败):表示异步操作失败。
  • 状态特点:Promise 的状态一旦从 Pending 变为 Fulfilled 或 Rejected,就不可再变。

2. Promise 的基本用法

创建一个 Promise

const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true; // 模拟异步操作成功或失败
    if (success) {
      resolve("操作成功"); // 调用 resolve 表示成功
    } else {
      reject("操作失败"); // 调用 reject 表示失败
    }
  }, 1000);
});

使用 Promise

promise
  .then((value) => {
    console.log(value); // 输出 "操作成功"
  })
  .catch((error) => {
    console.error(error); // 如果有错误,输出 "操作失败"
  });

3. Promise 的常用方法

1. then()

  • 用于处理 Promise 成功的情况。
  • 可以链式调用,返回一个新的 Promise。
const promise = new Promise((resolve) => {
  resolve(1);
});

promise
  .then((result) => {
    console.log(result); // 输出 1
    return result + 1; // 返回新的值
  })
  .then((newResult) => {
    console.log(newResult); // 输出 2
  });

2. catch()

  • 用于处理 Promise 失败的情况。
const promise = new Promise((resolve, reject) => {
  reject("出错了");
});

promise
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error); // 输出 "出错了"
  });

3. finally()

  • 无论 Promise 成功还是失败,都会执行。
const promise = new Promise((resolve) => {
  resolve("成功");
});

promise
  .then((result) => {
    console.log(result); // 输出 "成功"
  })
  .catch((error) => {
    console.error(error);
  })
  .finally(() => {
    console.log("无论成功还是失败,都会执行");
  });

4. Promise 的静态方法

1. Promise.all()

  • 用于处理多个 Promise,当所有 Promise 都成功时,返回一个成功的 Promise,结果是所有 Promise 的结果数组。
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log(results); // 输出 [1, 2, 3]
  });

2. Promise.race()

  • 用于处理多个 Promise,当其中一个 Promise 完成时,返回一个成功的 Promise,结果是第一个完成的 Promise 的结果。
const promise1 = new Promise((resolve) => setTimeout(resolve, 1000, "第一个"));
const promise2 = new Promise((resolve) => setTimeout(resolve, 500, "第二个"));

Promise.race([promise1, promise2])
  .then((result) => {
    console.log(result); // 输出 "第二个"
  });

3. Promise.allSettled()

  • 用于处理多个 Promise,等待所有 Promise 完成(无论成功或失败),返回一个包含每个 Promise 状态和结果的数组。
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject("出错了");
const promise3 = Promise.resolve(3);

Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    console.log(results); // 输出 [{ status: "fulfilled", value: 1 }, { status: "rejected", reason: "出错了" }, { status: "fulfilled", value: 3 }]
  });

5. Promise 的实际应用场景

1. 封装 AJAX 请求

function fetch_data(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(xhr.responseText);
      } else {
        reject(xhr.status);
      }
    };
    xhr.send();
  });
}

fetch_data("https://api.example.com/data")
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error);
  });

2. 封装文件操作

const fs = require("fs");

function readFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

readFile("example.txt")
  .then((data) => {
    console.log(data.toString());
  })
  .catch((error) => {
    console.error(error);
  });

3. 封装定时器

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

delay(1000)
  .then(() => {
    console.log("延迟了1秒");
  });

6. Promise 的优点

  • 避免回调地狱:通过链式调用,避免了多层嵌套的回调函数。
  • 更好的错误处理:通过 catch() 方法集中处理错误。
  • 代码更清晰:异步代码的逻辑更加直观和易于维护。

7. 注意事项

  • 状态不可逆:Promise 的状态一旦确定,就不可更改。
  • 微任务:Promise 的回调会在当前事件循环结束后执行,属于微任务。
  • 避免滥用:对于简单的同步操作,不需要使用 Promise。

通过以上内容,你可以更好地理解和使用 Promise 来处理前端中的异步操作。

fetch

在前端开发中,Promise 和 fetch 是处理异步操作的常用工具。以下将结合 fetch 探讨 Promise 在循环中和多个 await 中的用法。

1. fetch 的基本用法

fetch 是一个基于 Promise 的异步函数,用于从服务器获取资源。其基本用法如下:

fetch('https://api.example.com/data')
  .then(response => {
    // 处理响应
    if (response.ok) {
      return response.json();
    } else {
      throw new Error('请求失败');
    }
  })
  .then(data => {
    // 处理数据
    console.log(data);
  })
  .catch(error => {
    // 处理错误
    console.error(error);
  });

2. 在循环中使用 Promise

在循环中使用 Promise 需要考虑两种情况:并行执行 和 串行执行。

2.1 并行执行

如果希望同时发起多个请求,并等待所有请求完成,可以使用 Promise.all()。

const urls = [
  'https://api.example.com/data1',
  'https://api.example.com/data2',
  'https://api.example.com/data3'
];

// 使用 Promise.all 并行执行
Promise.all(urls.map(url => fetch(url)))
  .then(responses => {
    // 所有请求完成
    console.log(responses);
    // 处理响应
    return Promise.all(responses.map(response => response.json()));
  })
  .then(data => {
    // 所有数据加载完成
    console.log(data); // [data1, data2, data3]
  })
  .catch(error => {
    console.error('其中一个请求失败', error);
  });

2.2 串行执行

如果希望按顺序依次执行请求,可以使用 for...of 循环和 async/await。

const urls = [
  'https://api.example.com/data1',
  'https://api.example.com/data2',
  'https://api.example.com/data3'
];

async function fetchDataSequentially() {
  const results = [];
  for (const url of urls) {
    const response = await fetch(url);
    const data = await response.json();
    results.push(data);
  }
  console.log(results); // [data1, data2, data3]
}

fetchDataSequentially();

3. 在多个 await 中使用 Promise

在 async/await 中,每个 await 会暂停函数的执行,直到对应的操作完成。可以使用多个 await 来处理复杂的异步流程。

async function fetchData() {
  // 第一次 fetch
  const response1 = await fetch('https://api.example.com/data1');
  const data1 = await response1.json();
  console.log('数据1加载完成', data1);

  // 第二次 fetch
  const response2 = await fetch('https://api.example.com/data2');
  const data2 = await response2.json();
  console.log('数据2加载完成', data2);

  // 联合数据
  const combinedData = { ...data1, ...data2 };
  return combinedData;
}

fetchData()
  .then(data => console.log('最终数据', data))
  .catch(error => console.error('发生错误', error));

4. 注意事项

  • 错误处理:fetch 请求可能会失败,需要使用 .catch() 或 try...catch 捕获错误。
  • 资源释放:在获取 fetch 响应后,记得调用 .json(), .text(), 等方法读取数据,否则响应体不会被解析。
  • 并行 vs 串行:
    • Promise.all() 适用于需要同时处理多个请求的场景。
    • for...of 适用于需要按顺序逐个处理请求的场景。

5. 总结

  • 在循环中使用 Promise.all() 可以并行处理多个请求。
  • 使用 async/await 与 for...of 可以串行处理多个请求。
  • fetch 和 Promise 的结合使用可以使异步代码更清晰、更易维护。

await

await 是 JavaScript 中用于处理异步操作的关键字,尤其是在 async 函数中使用。以下是一些关于 await 使用的注意事项:

1. 必须在 async 函数中使用

  • await 只能在 async 函数中使用,否则会报错。
  • async 函数会自动将返回值包装成 Promise,可以使用 await 等待异步操作的结果。
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

2. await 的本质

  • await 是基于 Promise 的,它暂停当前 async 函数的执行,等待 Promise 结果。
  • 如果 await 的表达式不是 Promise,它会被包装成一个已经解析的 Promise。
async function example() {
  const result = await 123; // 等价于 await Promise.resolve(123)
  console.log(result); // 输出 123
}

3. 错误处理

  • 使用 try...catch 捕获 await 的错误。
  • 如果 await 的 Promise 被拒绝,错误会立即触发 catch 块。
async function example() {
  try {
    const response = await fetch('https://api.not-exist.com');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('请求失败', error);
  }
}

4. await 的执行顺序

  • await 会暂停当前 async 函数的执行,但不会暂停整个 JavaScript 线程。
  • 在 async 函数中,多个 await 是按顺序执行的。
async function example() {
  console.log('开始');
  const response = await fetch('https://api.example.com/data1');
  console.log('请求 1 完成');
  const data1 = await response.json();
  
  const response2 = await fetch('https://api.example.com/data2');
  console.log('请求 2 完成');
  const data2 = await response2.json();
  
  console.log('所有请求完成');
}
// 输出顺序:
// 开始
// 请求 1 完成
// 请求 2 完成
// 所有请求完成

5. await 的性能考虑

  • 如果多个异步操作之间没有依赖关系,应使用 Promise.all() 并行化操作,而不是串行执行。
async function example() {
  const [data1, data2] = await Promise.all([
    fetch('https://api.example.com/data1').then(res => res.json()),
    fetch('https://api.example.com/data2').then(res => res.json())
  ]);
  console.log(data1, data2);
}

6. await 的使用场景

  • 单次异步请求:适用于简单的异步操作。
  • 多次异步请求:可结合 Promise.all()、for...of 循环等处理多个请求。
async function example() {
  for (const url of ['url1', 'url2', 'url3']) {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data);
  }
}

7. await 的替代方案

  • 如果不使用 async/await,可以用 .then() 和 .catch() 处理异步操作。
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

8. 调试时的注意事项

  • 在调试 async/await 时,注意代码的执行顺序和异步操作的依赖关系。
  • 使用 console.log() 或调试工具定位异步操作的执行时间点。

总结

await 是处理异步操作的有力工具,但在使用时需要注意以下几点:

  • 必须在 async 函数中使用。
  • 使用 try...catch 处理错误。
  • 理解 await 的执行顺序和性能影响。
  • 合理选择 async/await 和其他异步处理方式(如 Promise)。

浏览器缓存

cookie需要配合后端使用,这里偷懒的方法就是

在每次登录请求成功之后,将当前用户的名称存入浏览器缓存。localStorage.setItem('user', JSON.stringify(response.data.user.username));

async login(credentials: Credentials) {
      try {
        // 发送登录请求到后端
        const response = await axios.post('https://jy8b5cnnmg.hzh.sealos.run/login', credentials);
        // console.log(response)
        // 返回的用户名是存在data.user.username中的
        // 如果登录成功,更新状态
        this.isAuthenticated = true;
        this.user = response.data.user.username;

        // 可选:保存用户信息到 localStorage 或其他存储
        localStorage.setItem('user', JSON.stringify(response.data.user.username));

      } catch (error) {
        // 如果登录失败,抛出错误
        throw new Error('Login failed. Please check your credentials.');
      }
    },

然后定义一个初始化函数,调用时通过浏览器缓存来识别用户名并向后端发送请求来实现初始化当前用户的喜好,提交记录等等。

需要注意的是,const favoritesStore = useFavoritesStore();这几个store的调用必须要在工具函数内,以免pinia没有正确初始化就调用导致异常

在 Vue 应用中,确保在调用 useStore() 之前已经挂载了 Pinia 实例。所以将其封装到函数中,避免在模块初始化时直接调用。

// 初始化用户
    initUser() {
      // 从缓存中获取用户信息
      const userStr = localStorage.getItem('user');
      // 如果存在则设置为当前用户
      if (userStr) {
        this.user = JSON.parse(userStr);
        this.isAuthenticated = true;
        const favoritesStore = useFavoritesStore();
        const submitstore = usesubmitstore();
        favoritesStore.fetchFavorites(this.user);
        submitstore.fetchSubmissions(this.user);
      }
    },

当然在页面成功挂载后就立即调用函数(main.ts文件)

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import './style.css'
import App from './App.vue'
import { useAuthStore } from './stores/authStore'
 
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

// 初始化用户
useAuthStore().initUser()

同时,对应logout函数应该在退出登录之后删除浏览器缓存

// 处理退出登录逻辑
    logout() {
      // 登出逻辑
      this.isAuthenticated = false;
      this.user = null;
      // 清除用户信息
      localStorage.removeItem('user');
    }

当然,sessionStorage也是,只不过专门用来存各种token

sessionStorage.setItem("accessToken", JSON.stringify(res.data.data.accessToken));

图片与异常处理

当图片出现异常,自动调用函数handleimgError我,将:src="imgSrc"绑定的imgSrc更改为备用链接

<img :src="imgSrc" :alt="website.name"  @error="handleimgError"/>

​

overflow: hidden;

在制作类似点击产生下拉框的效果时,千万不能将外部子容器设置成这个属性

这是源代码:

<td class="action-cell">
                            <div class="act">
                                <button class="action-btn" @click="toggleActionMenu(template.id)">
                                    <span>详情</span>
                                    <i class="dropdown-icon">▼</i>
                                </button>
                                <div class="action-menu" v-if="showActionMenu === template.id">
                                    <div class="action-item" @click="viewFileDetails(template.id)">
                                        <i class="view-icon"></i>
                                        <span>查看</span>
                                    </div>
                                    <div class="action-item" @click="downloadFile(template.id)">
                                        <i class="download-icon"></i>
                                        <span>下载</span>
                                    </div>
                                    <div class="action-item delete" @click="renameFile(template.id)">
                                        <i class="delete-icon"></i>
                                        <span>重命名</span>
                                    </div>
                                    <div class="action-item delete" @click="deleteFile(template.id)">
                                        <i class="delete-icon"></i>
                                        <span>删除</span>
                                    </div>
                                </div>
                            </div>
                        </td>

overflow: hidden; 是一个 CSS 属性,用于控制元素内容超出其容器时的行为。当元素的内容超出其容器的宽度或高度时,overflow: hidden; 会隐藏超出的部分。

在你的场景中,如果父元素(td)设置了 overflow: hidden;,那么子元素(div)即使使用了 position: absolute; 或 position: fixed;,也可能被裁剪掉超出父元素的部分。

解决方案:

要让子元素(div)覆盖父元素(td)并且不受 overflow: hidden; 的限制,可以通过以下方法:

方法 1:移除父元素的 overflow: hidden;

最直接的解决方案是移除父元素(td)上的 overflow: hidden; 样式。这样子元素(div)就可以正常显示,而不会被裁剪。

<table>
  <tr>
    <td id="parent">
      这是父元素(td)
      <div id="child">这是子元素(div)</div>
    </td>
  </tr>
</table>
#parent {
  position: relative; /* 创建定位上下文 */
  width: 200px;
  height: 100px;
  border: 1px solid black;
  /* overflow: hidden; */ /* 移除这一行 */
}

#child {
  position: absolute;
  top: -20px;
  left: -20px;
  width: 240px;
  height: 120px;
  background-color: red;
  z-index: 1;
}

方法 2:调整子元素的定位

如果无法移除父元素的 overflow: hidden;,可以通过调整子元素的定位,使其相对于页面根元素定位,而不是相对于父元素。

#parent {
  position: relative;
  width: 200px;
  height: 100px;
  border: 1px solid black;
  overflow: hidden; /* 父元素仍然有 overflow: hidden */
}

#child {
  position: fixed; /* 使用 fixed 定位 */
  top: 0; /* 相对于页面顶部 */
  left: 0; /* 相对于页面左侧 */
  width: 240px;
  height: 120px;
  background-color: red;
  z-index: 1;
}

方法 3:使用负的 margin

如果子元素的内容被裁剪,可以通过负的 margin 将其部分移出父元素的范围。

#parent {
  position: relative;
  width: 200px;
  height: 100px;
  border: 1px solid black;
  overflow: hidden;
}

#child {
  margin-top: -20px; /* 向上移动 */
  margin-left: -20px; /* 向左移动 */
  width: 240px;
  height: 120px;
  background-color: red;
  z-index: 1;
}

总结

  • 如果可以移除父元素的 overflow: hidden;,这是最简单的解决方案。
  • 如果不能移除,可以使用 position: fixed; 或负的 margin 来调整子元素的位置,使其显示在父元素之外。

动态样式布局

盒子样式要多用align-items、justify-content和text-align以及display:flex

关于CSS布局三剑客的用法,我们可以用一场「元素排列舞会」的比喻来理解它们的分工:


1. align-items:垂直方向的领舞者

角色:控制舞池(flex容器)中所有舞者(子元素)在侧轴(垂直方向)的队形
舞步示例:

  • center:所有人保持垂直中线对齐(集体站军姿)
  • flex-start:集体紧贴天花板(瑜伽倒立式)
  • flex-end:集体蹲在舞台底部(谢幕鞠躬式)
  • stretch:全员拉伸到同高度(芭蕾踮脚式)
    经典场景:
    当需要让导航栏的LOGO和菜单项垂直居中时,就像让不同身高的舞者踩着隐形台阶达到统一高度:
.nav-bar {
  display: flex;
  align-items: center; /* 垂直居中魔法 */
}

2. justify-content:水平队列指挥官

角色:编排舞者们在主轴(水平方向)的分布方式
战术手势:

  • center:向中看齐(国庆阅兵方阵)
  • space-between:头尾贴边,中间等距(地铁早高峰排队)
  • space-around:每人自带等距气泡(超市货架陈列)
  • flex-end:向右看齐(候车室排队检票)
    实战案例:
    实现电商网站的卡片等间距排列,就像把商品在货架上均匀铺开:
.product-grid {
  display: flex;
  justify-content: space-around; /* 呼吸感布局 */
}

3. text-align:文字排版指挥家

角色:指挥文本乐符在五线谱(块级元素)中的水平位置
指挥棒动作:

  • left:左起奏鸣曲(西方文本默认序章)
  • right:右起进行曲(阿拉伯语系篇章)
  • center:中央C大调(诗歌标题华彩段)
  • justify:两端对齐赋格曲(报纸专栏严谨排版)
    经典乐章:
    让博客正文呈现优雅的两端对齐效果,如同印刷书籍的精致排版:
.article-content {
  text-align: justify; /* 学术论文级排版 */
}

🌟 组合技演示

场景:制作一个垂直水平居中且文字居中的按钮

/* 外层flex双居中 */
.modal-box {
  display: flex;
  align-items: center;     /* 垂直领舞 */
  justify-content: center; /* 水平指挥 */
  height: 300px;
}

/* 内部文字居中 */
.confirm-btn {
  text-align: center;     /* 文字指挥家 */
  padding: 1rem 2rem;
}

这就像让按钮在舞台中央聚光灯下完成360度完美亮相,内外三层对齐系统各司其职


🧭 属性定位指南

场景选用属性如同
多行文本垂直居中align-items调整书架每层的书脊对齐
图标水平等距排列justify-content琴键均匀分布在钢琴盖板
段落首行缩进text-indent中文段落开头的两个空格
导航菜单右对齐margin-left: auto把最后一道菜推到餐桌右侧

⚠️ 常见踩坑点

  1. 混淆主轴方向:flex容器的默认主轴是水平方向,但flex-direction: column会让主轴变垂直,此时justify-content控制垂直排列,align-items控制水平排列
  2. 多行布局失效:align-items只对单行flex项目生效,多行布局需要用align-content
  3. 图片对齐陷阱:给<img>设置text-align无效,需改用flex布局或margin: 0 auto

这三个属性就像CSS布局世界的XYZ三轴坐标系,掌握它们的配合使用,就能让元素在三维空间跳出优雅的排列之舞💃

axios拦截器

个人总结

个人写的

拦截器本质上就是,你自定义一个axios对象(这里是axiosService),在每次发送axios请求的时候对请求进行加工,一般是加上token,同时对于请求的响应状态进行错误处理,比如转到401的专门页面什么的

import axios from "axios";
import host from "../config/hostname";

// 在拦截器中使用时显式传递
const hostname = host();


// 创建独立Axios实例[7,10](@ref)
const service = axios.create({
    baseURL: hostname, // 建议从环境变量获取[4](@ref)
    timeout: 15000,
});

// ==================== 请求拦截器 ====================
service.interceptors.request.use(
    config => {
        const url = config.url;
        if (sessionStorage.getItem("accessToken") && url?.indexOf("renewal") === -1) {
            try {
                const token = JSON.parse(sessionStorage.getItem("accessToken")!);
                config.headers["Authorization"] = `Bearer ${token}`; 
            } catch (e) {
                console.error("Token解析失败:", e);
                sessionStorage.removeItem("accessToken");
            }
        }
        return config;
    },
    error => Promise.reject(error)
);

// ==================== Token刷新机制 ==================== 
const userIds = localStorage.getItem("userinfo")
async function getNewToken() {
    try {
        if (userIds) {
            const refreshToken = JSON.parse(sessionStorage.getItem("refreshToken")!);
            return await service.post("/auth/token/renewal",
                { id: JSON.parse(userIds) },
                {
                    headers: {
                        accessToken: refreshToken, // 修正header字段拼写
                        isRefresh: true
                    }
                }
            );
        }

    } catch (error) {
        console.error("刷新Token失败:", error);
        throw error;
    }
}

// ==================== 响应拦截器 ====================
service.interceptors.response.use(  
    async response => {
        
        if (response.status === 401 && !response.config.headers.isRefresh) {
            try {
                const res = await getNewToken();
                if (res && res.data && res.data.code === 200 && res.data.data && res.data.data.status) {
                    sessionStorage.setItem("accessToken", JSON.stringify(res.data.data.accessToken));

                    // Retry the original request
                    return service({
                        ...response.config,
                        headers: {
                            ...response.config.headers,
                            Authorization: `Bearer ${res.data.data.accessToken}`
                        }
                    });
                }

            } catch (error) {
                window.location.href = "/login";
                return Promise.reject(error);
            }
        }
        return response.data;
    },
    error => {
        if (error.response?.status === 404) {
            console.error("资源未找到:", error.config.url);
            // router.push("/404");  // 建议使用vue-router跳转[4](@ref)
        }
        return Promise.reject(error);
    }
);

export default service; // 添加默认导出[5,10](@ref)

然后如果要使用,就直接导入就行,就不要用axios发请求了而是用你自定义的axiosService发请求

import axiosService from '../../utils/axios-test' // 导入配置好的axios实例

const response = await axiosService.post(hostname + '/api/login', {  
          email: email,
          password: password,
        });

还有就是=要注意的点是如果你自己的请求就带有请求头,会产生覆盖,记得带上token

同时要注意,拦截器不要去拦截refreshToken的那个请求,因为那时候的accessToken已经过期,不能再带着她请求了,要将原有的accessToken换成refreshToken在请求头里面。

AI分析

嗯,用户现在想让我结合他们提供的TypeScript代码,讲讲Axios拦截器的用法。首先,我需要回顾他们给出的代码,这是请求和响应拦截器的基本结构。然后,结合搜索结果里的信息,看看有什么可以补充的。

根据搜索结果,网页1和网页3都提到了拦截器的基本写法,特别是请求拦截器中添加token,响应拦截器处理错误状态码。用户提供的代码里已经有这些部分,但可能需要更详细的解释。例如,在请求拦截器里,如何修改config,比如添加headers的Authorization字段,这在网页1的例子中也有提到。

另外,网页2和网页9提到使用TypeScript进行封装,需要处理类型定义。用户的代码是TypeScript,所以可能需要强调类型安全,比如AxiosRequestConfig和AxiosResponse的类型使用。网页2中的代码示例展示了如何引入这些类型,并应用到拦截器中,确保配置和响应的类型正确。

网页5和网页7讨论了如何封装axios实例,并添加拦截器。用户代码里使用的是全局拦截器,但可能可以建议创建实例,以便更好的模块化管理,如网页3中的示例,创建service实例并添加拦截器,这样可以针对不同的API端点配置不同的拦截逻辑。

响应拦截器部分,用户代码处理了response和error,但可能需要更详细的错误处理策略,比如根据HTTP状态码进行不同的处理,如401跳转登录,500显示服务器错误,这在网页1、网页3和网页6中都有提到。特别是网页6中的代码示例展示了如何处理不同状态码,并给出用户提示。

还要注意拦截器的执行顺序,如网页8提到的,多个请求拦截器按添加顺序反向执行,响应拦截器按添加顺序执行。用户可能需要了解这一点,以便安排拦截器的逻辑顺序,比如先添加的请求拦截器后执行,这在处理headers或token时可能有影响。

此外,TypeScript中的类型增强,如扩展AxiosRequestConfig接口,添加自定义配置项,这在网页2和网页7中有示例。用户可能需要在拦截器中使用自定义配置,比如silent选项来禁止全局loading,这需要类型定义的支持。

最后,错误处理部分,用户代码在响应拦截器的error部分返回了Promise.reject,确保错误能继续传递到调用处,以便在具体的请求中处理,如网页5和网页7提到的,统一处理HTTP错误,并在需要时跳转登录页面。

综合这些点,我需要分结构讲解代码,强调TypeScript的类型安全、拦截器的应用场景、错误处理策略、实例化封装以及执行顺序等,确保用户能全面理解并应用拦截器。

根据你提供的TypeScript代码和搜索结果中的最佳实践,以下是Axios拦截器的核心用法解析(结合企业级项目经验):

一、拦截器架构解析

// 请求拦截器(所有axios请求生效)
axios.interceptors.request.use(
  config => { // 类型推断为AxiosRequestConfig
    // 此处可添加认证令牌
    config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`; // 
    return config;
  },
  error => { // 类型为AxiosError
    return Promise.reject(error); // 必须抛出错误
  }
)

// 响应拦截器(所有axios响应生效)
axios.interceptors.response.use(
  response => { // 类型为AxiosResponse
    // 数据脱壳处理(根据后端数据结构调整)
    return response.data.payload; // 
  },
  error => {
    // 统一错误处理(网络错误/业务错误)
    handleError(error.response?.status); // 
    return Promise.reject(error); 
  }
)

二、TypeScript强化实践

  1. 类型增强声明(参考网页2/网页7):
declare module 'axios' {
  interface AxiosRequestConfig {
    silent?: boolean;  // 禁用全局Loading提示
    retryCount?: number; // 自动重试次数
  }
  
  interface AxiosResponse<T = any> {
    traceId: string; // 链路追踪ID
  }
}
  1. 多拦截器执行顺序(参考网页8):
// 请求拦截器执行顺序:后添加的先执行
axios.interceptors.request.use(config => {
  console.log('拦截器2'); // 最后执行
  return config;
});

axios.interceptors.request.use(config => {
  console.log('拦截器1'); // 最先执行
  return config;
});

// 响应拦截器执行顺序:先添加的先执行
axios.interceptors.response.use(res => {
  console.log('响应1'); // 先处理
  return res;
});

axios.interceptors.response.use(res => {
  console.log('响应2'); // 后处理
  return res;
});

三、企业级功能扩展

  1. 请求签名(参考网页7):
config => {
  const timestamp = Date.now();
  config.headers['X-Sign'] = createSign({
    url: config.url,
    method: config.method,
    timestamp
  });
  return config;
}
  1. 性能监控(参考网页9):
const startTime = Date.now();
return response => {
  const latency = Date.now() - startTime;
  trackPerformance({
    url: response.config.url,
    latency,
    status: response.status
  });
  return response;
}
  1. 自动重试机制(参考网页5):
error => {
  const config = error.config;
  if(!config.retryCount) config.retryCount = 0;
  
  if(config.retryCount < 3 && error.code === 'ECONNABORTED') {
    config.retryCount++;
    return axios(config); // 自动重试
  }
  return Promise.reject(error);
}

四、开发注意事项

  1. 配置污染问题
    避免直接修改原始config对象,建议使用扩展运算符:

    return { 
      ...config,
      headers: { ...config.headers, 'X-Trace': uuid() }
    }
    
  2. 异步操作处理
    使用async/await处理异步逻辑时需保持链式调用:

    service.interceptors.request.use(async config => {
      const token = await refreshToken();
      config.headers.Authorization = token;
      return config;
    });
    
  3. 类型安全校验
    通过泛型强化响应数据类型:

    interface ApiResponse<T> {
      code: number;
      data: T;
      message: string;
    }
    
    axios.interceptors.response.use(
      (response: AxiosResponse<ApiResponse<any>>) => {
        if(response.data.code !== 200) throw new Error(response.data.message);
        return response.data.data;
      }
    )
    

建议结合Vue3的Composition API封装成可复用的Hook,并通过单元测试验证拦截器逻辑的健壮性。具体项目示例可参考网页7中Auth类的实现,实现Token的自动化管理。

一、拦截器基础架构

1. 全局拦截器写法

// 请求拦截器(所有axios请求生效)
axios.interceptors.request.use(
  config => {
    // 发送请求前的处理逻辑
    return config // 必须返回修改后的配置
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器(所有axios响应生效)
axios.interceptors.response.use(
  response => {
    // 处理响应数据(2xx状态码)
    return response 
  },
  error => {
    // 处理错误响应(非2xx状态码)
    return Promise.reject(error)
  }
)

引用示例:网页1、网页3、网页5均展示了基础拦截器结构


二、企业级封装方案

1. 创建自定义实例(推荐)

const service = axios.create({
  baseURL: 'http://api.example.com',
  timeout: 5000,
  headers: {'Content-Type': 'application/json'}
})

2. 实例专属拦截器

// 请求拦截器
service.interceptors.request.use(config => {
  // 添加认证token(典型场景)
  if (store.getters.token) {
    config.headers['X-Token'] = getToken() 
  }
  return config
})

// 响应拦截器
service.interceptors.response.use(
  response => {
    const res = response.data
    // 统一处理业务错误码(如401跳登录)
    if (res.code !== 200) {
      showErrorMessage(res.msg)
      return Promise.reject(new Error(res.msg))
    }
    return res.data // 数据脱壳
  },
  error => {
    // 处理HTTP状态码错误
    handleHttpError(error.response.status)
    return Promise.reject(error)
  }
)

最佳实践参考:网页5的权限验证方案和网页3的实例封装


三、六大核心应用场景

  1. 认证令牌注入
    自动添加JWT到请求头,避免手动传token

  2. 请求参数预处理
    序列化参数、添加时间戳等公共参数

  3. 全局Loading状态
    通过拦截器统一管理请求加载提示

  4. 响应数据格式化
    提取嵌套数据(如response.data.data -> data)

  5. 错误统一处理
    根据状态码显示对应错误提示(401跳登录/500显示服务异常)

  6. 性能监控
    记录请求耗时,优化慢接口


四、高级使用技巧

1. 拦截器执行顺序

// 添加顺序决定执行顺序
axios.interceptors.request.use(config => {
  console.log('第一个请求拦截器')
  return config
})

axios.interceptors.request.use(config => {
  console.log('第二个请求拦截器') 
  return config
})

2. 动态移除拦截器

const myInterceptor = axios.interceptors.request.use(...)
axios.interceptors.request.eject(myInterceptor) // 移除

参考网页3的拦截器管理方案

3. 多实例隔离

const api1 = axios.create({/* 配置A */})
const api2 = axios.create({/* 配置B */})

// 各实例独立维护拦截器
api1.interceptors.request.use(...)
api2.interceptors.response.use(...) 

五、开发注意事项

  1. 配置必须返回
    请求拦截器中忘记return config会导致请求卡死

  2. 避免循环调用
    在拦截器内调用axios可能造成死循环

  3. 异步处理
    需要异步操作时使用async/await:

    service.interceptors.request.use(async config => {
      config.headers.token = await getTokenAsync()
      return config
    })
    
  4. 错误传播
    响应拦截器中需要将错误继续抛出,否则.catch无法捕获


六、TypeScript强化(进阶)

declare module 'axios' {
  interface AxiosRequestConfig {
    silent?: boolean // 自定义配置项
  }
}

// 使用增强类型
service.interceptors.request.use(config => {
  if (!config.silent) {
    showLoading()
  }
  return config
})

通过合理使用拦截器,可减少30%以上的重复代码量。建议结合Vuex/Pinia进行状态管理,实现更优雅的全局请求控制。具体项目示例可参考网页5的权限管理方案。

双Token验证技术

一、双Token的诞生:临时工牌与长期签证

[场景]
想象你进入一家高科技园区:
• Access Token 是 临时工牌(有效期2小时),每次进出闸机都要刷卡验证
• Refresh Token 是 长期签证(有效期7天),用于在工牌过期时快速补办新工牌

[实现步骤]

// 登录接口返回示例
interface TokenPair {
  accessToken: string     // 门禁卡(易碎品)
  refreshToken: string    // 园区通行证(保险柜存放)
  expiresIn: number       // 2小时后需更新
}

二、令牌存储:保险柜与随身包

[安全策略]
• Access Token 存内存或sessionStorage(类似随身包,方便取用但易丢失)
• Refresh Token 存HttpOnly Cookie(类似保险柜,防XSS盗窃)

[代码示例]

// 安全存储工具类
const storeTokens = (tokens: TokenPair) => {
  sessionStorage.setItem('accessToken', tokens.accessToken) // 随身携带
  document.cookie = `refreshToken=${tokens.refreshToken}; Path=/; Secure` // 保险柜
}

三、请求拦截:智能闸机系统

[工作流程]

  1. 正常通行:每次请求自动携带Access Token
  2. 过期检测:当闸机提示"工牌失效"(401错误)
  3. 自动续期:用Refresh Token呼叫园区服务中心换新工牌

[axios拦截器实现]

// 请求拦截器 - 自动佩戴工牌
service.interceptors.request.use(config => {
  if (!isRefreshRequest(config.url)) { // 排除刷新Token的请求
    config.headers.Authorization = `Bearer ${getAccessToken()}` 
  }
  return config
})

// 响应拦截器 - 智能续期
service.interceptors.response.use(null, async error => {
  if (error.response?.status === 401 && !error.config._retry) {
    error.config._retry = true
    const newTokens = await refreshToken() // 调用刷新接口
    resetAuthHeader(newTokens.accessToken) // 更新请求头
    return service(error.config) // 重新发送原请求
  }
  return Promise.reject(error)
})

四、无感刷新:隐形维修工

[幕后机制]
• 请求队列:当多个请求同时发现Token过期,首个请求触发刷新,其他请求排队等候新Token
• 静默刷新:在用户无感知时完成续期,避免操作中断(如同电梯维修不影响使用)

[代码逻辑]

let isRefreshing = false // 刷新状态锁
const queue: ((token: string) => void)[] = [] // 等待队列

const addToQueue = (resolve: (value: unknown) => void) => {
  queue.push((newToken: string) => {
    resolve(service.request({ ...originalConfig, headers: { Authorization: `Bearer ${newToken}` } }))
  })
}

五、安全防线:多层安保系统

  1. 加密存储:用AES加密Token,如同给保险箱上密码锁
  2. 设备指纹:绑定设备特征,防止令牌异地使用
  3. 黑名单机制:服务端记录已注销的Refresh Token,如同挂失的护照

[安全增强示例]

// 生成设备指纹
const generateFingerprint = () => {
  return navigator.userAgent + screen.width + screen.colorDepth // 组合设备特征
}

// 请求携带指纹
service.interceptors.request.use(config => {
  config.headers['X-DeviceID'] = deviceFingerprint
  return config
})

六、异常处理:应急预案手册

  1. 双Token同时过期:跳转登录页,如同园区全面安检
  2. 刷新失败:清除本地存储,提示重新登录
  3. 高频刷新报警:监控异常请求,防御恶意攻击

[路由守卫示例]

// 页面访问拦截
router.beforeEach(async (to) => {
  if (to.meta.requiresAuth) {
    try {
      await silentRefresh() // 尝试静默刷新
      return true
    } catch {
      return { path: '/login', query: { redirect: to.path } }
    }
  }
})

技术选型建议:
• 有效期设置:Access Token(15-30分钟),Refresh Token(7-30天)
• 加密库推荐:crypto-js用于前端加密,jsonwebtoken用于服务端签发
• 监控指标:记录Token刷新频率、异常设备请求等安全日志

通过这套机制,用户就像在拥有隐形安保的智能园区中畅行无阻——既享受无缝体验,又获得银行级安全防护。

默认响应式

在 Pinia 中,state 中定义的所有属性默认都是响应式的。您代码中的 isAuthenticated、user 和 url 都会自动成为响应式数据,无需额外处理。以下是具体分析:


响应式规则说明

属性类型响应式行为
isAuthenticated布尔值✅ 响应式
user字符串✅ 响应式
url字符串✅ 响应式

验证示例

// 在组件中测试
import { useAuthStore } from './stores/auth'

const authStore = useAuthStore()

// 修改属性会触发响应式更新
authStore.isAuthenticated = true // 触发更新
authStore.user = 'newUser'       // 触发更新
authStore.url = '/new-path'      // 触发更新

注意事项

  1. 避免直接解构
    解构会破坏响应式:

    // ❌ 错误方式(失去响应式)
    const { user } = useAuthStore()
    
    // ✅ 正确方式
    const authStore = useAuthStore()
    const user = computed(() => authStore.user)
    
  2. 嵌套对象处理
    如果属性是对象,嵌套属性也保持响应式:

    state: () => ({
      profile: { name: '' } // ✅ 嵌套属性也是响应式
    })
    
  3. 数组更新
    使用标准数组方法保持响应式:

    // ✅ 正确
    authStore.items.push('newItem')
    
    // ❌ 错误(不会触发更新)
    authStore.items = [...authStore.items, 'newItem']
    

完整示例代码

// stores/auth.js
export const useAuthStore = defineStore('auth', {
  state: () => ({
    isAuthenticated: false,
    user: '',
    url: ''
  }),

  actions: {
    // 修改状态的方法
    login(user) {
      this.isAuthenticated = true
      this.user = user
      this.url = '/dashboard'
    }
  }
})

// 组件中使用
import { useAuthStore } from './stores/auth'

export default {
  setup() {
    const authStore = useAuthStore()

    // 响应式修改
    const handleLogin = () => {
      authStore.login('Alice') 
      // 触发所有相关组件更新
    }

    return { authStore, handleLogin }
  }
}

调试技巧

在浏览器控制台中检查响应式性:

const authStore = useAuthStore()
console.log(authStore) // 查看属性是否被Proxy包装

如果您遇到响应式失效的情况,通常是以下原因:

  1. 直接替换了整个 state 对象
    // ❌ 错误方式
    authStore.$state = { ... }
    
  2. 使用了非响应式 API 修改数据
  3. 未在 Vue 组件上下文中使用 store
最近更新:: 2025/12/9 09:39
Contributors: ksldnasx