源本科技 | 码上会

Vue3 联调后端 - 登录

2026/05/08
74
0

配置代理服务

  • 修改 vite.config.ts 配置文件,追加 proxy 配置

server: {
  proxy: {
    "/api": {
      target: "http://localhost:8080",
      changeOrigin: true,
      ws: true,
      rewrite: path => path.replace(/^\/api/, "")
    }
  },
},
  • 修改 src/utils/http/index.ts 追加基础请求路径,以配置上面的 proxy

const defaultConfig: AxiosRequestConfig = {
  baseURL: "/api", // 追加这个配置
  timeout: 10000,
  headers: {
    Accept: "application/json, text/plain, */*",
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  },
  paramsSerializer: {
    serialize: stringify as unknown as CustomParamsSerializer
  }
};

定义请求接口

  • 创建 src/api/login/LoginApi.ts

import { http } from "@/utils/http";

export type SysUserLoginVo = {
  success: boolean;
  message: string;
  code: string;
  data: {
    /** token */
    token: string;
    /** token 类型,如:Bearer */
    tokenType: string;
    /** 过期时间(单位:秒),如:604800 */
    expiration: number;
    /** 刷新 token */
    refreshToken: string;
  };
};

/**
 * 登录
 * @param data 参数名为 data 才能适配后端 Request Body
 */
export const login = (data?: object) => {
  return http.request<SysUserLoginVo>("post", "/auth/login", { data });
};

修改状态管理

  • 修改 src/store/modules/user.ts 并做好适配

// ****** 已有代码 ******
// 使用我们自己的接口
import { type SysUserLoginVo, login } from "@/api/login/LoginApi";
// ****** 已有代码 ******

export const useUserStore = defineStore("pure-user", {
  state: (): userType => ({
    // ****** 已有代码 ******
  }),
  actions: {
    // ****** 已有代码 ******
    async loginByUsername(data) {
      // 使用我们自己的接口
      return new Promise<SysUserLoginVo>((resolve, reject) => {
        login(data)
          .then(data => {
            if (data?.success) {
              // 适配前端 token 的数据结构
              const tokenData = {
                accessToken: data.data.token,
                expires: new Date(Date.now() + data.data.expiration * 1000),
                refreshToken: data.data.refreshToken
              };
              setToken(tokenData);
            }
            resolve(data);
          })
          .catch(error => {
            reject(error);
          });
      });
    },
    // ****** 已有代码 ******
  }
});

export function useUserStoreHook() {
  return useUserStore(store);
}

响应错误处理

  • 完成上面的修改后,点击登录如果报错并不会弹出错误提示

  • 修改 src/utils/http/index.ts 在响应拦截器中追加异常输出

// 追加导入
import { message } from "@/utils/message";

private httpInterceptorsResponse(): void {
    // 已有代码...
    (error: PureHttpError) => {
      const $error = error;
      $error.isCancelRequest = Axios.isCancel($error);

      // 追加代码:统一处理 HTTP 错误
      if (error.response) {
        const { status, data } = error.response;
        // @ts-ignore
        message(data?.message || `请求出错 ${status}`, { type: "error" });
      }

      // 所有的响应异常 区分来源为取消请求/非取消请求
      return Promise.reject($error);
    }
  );
}

异步菜单修复

  • 登录成功后,前端模板需要加载后端的异步路由,此时我们没有这个接口会报错,修改 src/api/routes.ts

import { http } from "@/utils/http";

type Result = {
  success: boolean;
  data: Array<any>;
};

export const getAsyncRoutes = () => {
  // 修改一下接口地址
  return http.request<Result>("get", "/sys/menu/get-async-routes");
};
  • 修改 com.lusifer.crmeb.controller.SysMenuController 增加前端所需的路由菜单

package com.lusifer.crmeb.controller;

import com.lusifer.crmeb.component.security.util.SecurityUtils;
import com.lusifer.crmeb.entity.SysMenu;
import com.lusifer.crmeb.mapper.SysMenuMapper;
import com.lusifer.crmeb.rule.pojo.response.ResponseData;
import com.lusifer.crmeb.rule.pojo.response.SuccessResponseData;
import com.lusifer.crmeb.service.SysMenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.*;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * 用户权限表 前端控制器
 *
 * @author Lusifer
 * @since 2026-05-04
 */
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/sys/menu")
@Tag(name = "菜单管理")
public class SysMenuController {

  private final SysMenuService sysMenuService;
  private final SysMenuMapper sysMenuMapper;

  @Operation(summary = "获取异步路由")
  @GetMapping("/get-async-routes")
  public ResponseData<List<Map<String, Object>>> getAsyncRoutes() {
    Long userId = SecurityUtils.getUserId();

    log.info("========== 开始获取用户 {} 的异步路由 ==========", userId);

    List<SysMenu> allMenus = sysMenuService.list();
    log.info("数据库中共有 {} 个菜单", allMenus.size());

    List<SysMenu> userMenus =
        allMenus.stream()
            .filter(menu -> menu.getStatusFlag() != null && menu.getStatusFlag() == '1')
            .filter(menu -> menu.getType() != null && (menu.getType() == 0 || menu.getType() == 1))
            .sorted(Comparator.comparingInt(SysMenu::getSort))
            .collect(Collectors.toList());

    log.info("过滤后用户可用菜单数量: {}", userMenus.size());

    List<Map<String, Object>> routes = buildRoutes(userMenus, 0L);

    log.info("构建完成的路由数量: {}", routes.size());
    log.info("路由数据结构: {}", routes);
    log.info("========== 异步路由获取完成 ==========");

    return new SuccessResponseData<>(routes);
  }

  private List<Map<String, Object>> buildRoutes(List<SysMenu> menus, Long parentId) {
    List<Map<String, Object>> routes = new ArrayList<>();

    List<SysMenu> childMenus =
        menus.stream()
            .filter(menu -> menu.getPid() != null && menu.getPid().equals(parentId))
            .sorted(Comparator.comparingInt(SysMenu::getSort))
            .collect(Collectors.toList());

    for (SysMenu menu : childMenus) {
      Map<String, Object> route = new LinkedHashMap<>();

      if (menu.getType() == 0) {
        // 目录类型
        String path = menu.getPath() != null ? menu.getPath() : "";
        if (!path.startsWith("/")) {
          path = "/" + path;
        }
        route.put("path", path);

        Map<String, Object> meta = new LinkedHashMap<>();
        meta.put("title", menu.getName() != null ? menu.getName() : "未命名");
        if (menu.getIcon() != null && !menu.getIcon().isEmpty()) {
          meta.put("icon", menu.getIcon());
        }
        if (menu.getSort() != null) {
          meta.put("rank", menu.getSort());
        }
        meta.put("showLink", true);
        route.put("meta", meta);

        List<Map<String, Object>> children = buildRoutes(menus, menu.getMenuId());
        if (!children.isEmpty()) {
          route.put("children", children);
        }
      } else {
        // 菜单类型
        String path = menu.getPath() != null ? menu.getPath() : "";
        if (!path.startsWith("/")) {
          path = "/" + path;
        }
        route.put("path", path);
        route.put("name", menu.getName() != null ? menu.getName() : "");

        // component 字段:只有当 uri 不为空时才添加
        if (menu.getUri() != null && !menu.getUri().isEmpty()) {
          route.put("component", menu.getUri());
        }

        Map<String, Object> meta = new LinkedHashMap<>();
        meta.put("title", menu.getName() != null ? menu.getName() : "未命名");
        if (menu.getIcon() != null && !menu.getIcon().isEmpty()) {
          meta.put("icon", menu.getIcon());
        }
        if (menu.getSort() != null) {
          meta.put("rank", menu.getSort());
        }
        meta.put("showLink", true);
        route.put("meta", meta);
      }

      routes.add(route);
    }

    return routes;
  }
}