前端开发(基础)

目录

一、Web前端项目初始化

环境准备

创建项目

前端工程化配置

引入组件库

开发规范

全局通用布局

基础布局结构

全局底部栏

动态替换内容

全局顶部栏

通用路由菜单

支持多套布局

请求

请求工具库

全局自定义请求

自动生成请求代码

全局状态管理

全局权限管理

全局项目入口

通用组件 - Markdown 编辑器组件

通用组件 - 图片上传

二、前端基础页面开发

用户模块

用户登录页面

用户注册页面

用户管理页面

管理模块

应用管理

题目管理

评分结果管理

回答管理


一、Web前端项目初始化

环境准备

nodeJS 版本:v18.16.0

检测命令:

node -v

切换和管理 node 版本的工具:GitHub - nvm-sh/nvm: Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions

npm 版本:9.5.1

npm -v

创建项目

使用 Vue-CLI 脚手架快速创建 Vue3 的项目:Vue CLI

安装脚手架工具

npm install -g @vue/cli

检测是否安装成功:

vue -V

如果找不到命令,那么建议去重新到安装 npm,重新帮你配置环境变量。

创建项目:

vue create yudada-frontend

手动选择特性:

选择如下特性:

会自动生成代码并安装依赖,然后用 WebStorm 打开项目,在终端执行 npm run serve,能访问网页就成功了。

前端工程化配置

脚手架已经帮我们配置了 Prettier 代码美化、ESLint 自动校验、TypeScript 类型校验、格式化插件等,无需再自行配置。

但是需要在 webstorm 里开启代码美化插件:

在 vue 文件中执行格式化快捷键(CTRL+ALT+L),不报错,表示配置工程化成功。

如果想关闭 ESLint 校验导致的编译错误(项目无法运行),可以关闭 lintOnsave:配置参考 | Vue CLI

在vue.config.js中

const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
  transpileDependencies: true,
    lintOnSave: "warning",
});

修改 .eslintrc.js 和 tsconfig.json 可以改变校验规则。

如果不使用脚手架,就需要自己整合这些工具:

  • 代码规范:Getting Started with ESLint - ESLint - Pluggable JavaScript Linter
  • 代码美化:Install · Prettier
  • 直接整合:https://github.com/prettier/eslint-plugin-prettier#recommended-configuration(包括了 https://github.com/prettier/eslint-config-prettier#installation)

引入组件库

引入 Arco Design 组件库:Arco Design Vue1808505663815548929_0.34677924671772464

参考官方文档快速上手:Arco Design Vue

注意版本号要求:vue >= 3.2.0

在WebStorm的终端执行安装:

npm install --save-dev @arco-design/web-vue

改变主入口文件 main.ts:

import { createApp } from "vue";
import App from "./App.vue";
import ArcoVue from "@arco-design/web-vue";
import { createPinia } from "pinia";
import "@arco-design/web-vue/dist/arco.css";
import router from "./router";
import "@/access";

const pinia = createPinia();

createApp(App).use(ArcoVue).use(pinia).use(router).mount("#app");

开发规范

遵循 Vue3 的组合式 API (Composition API):https://cn.vuejs.org/guide/introduction.html#composition-api

示例代码:

<template>
  <div id="xxPage">

  </div>
</template>

<script setup lang="ts">

</script>

<style scoped>
#xxPage {
}

</style>

全局通用布局

基础布局结构

在 layouts 目录下新建一个布局 BasicLayout.vue, 在 App.vue 全局页面入口文件中引入。

App.vue 代码如下:

<template>
  <div id="app">
    <BasicLayout />
  </div>
</template>

<script setup lang="ts">
import BasicLayout from "@/layouts/BasicLayout.vue";
</script>

移除页面内的默认样式:

<style>
#app {
}
</style>

选用 Arco Design 组件库的 Layout 组件:Arco Design Vue

先把【上中下】布局编排好,然后再填充内容:

BasicLayout代码如下:

<template>
  <div id="basicLayout">
    <a-layout style="min-height: 100vh">
      <a-layout-header class="header">头部</a-layout-header>
      <a-layout-content class="content">内容</a-layout-content>
      <a-layout-footer class="footer">底部</a-layout-footer>
    </a-layout>
  </div>
</template>

样式:

<style scoped>
#basicLayout {
}
</style>

全局底部栏

通常用于展示版权信息(BasicLayout代码如下):

<a-layout-footer class="footer">
  <a href="https://www.code-nav.cn" target="_blank">
    编程导航 by 程序员鱼皮
  </a>
</a-layout-footer>

样式:

#basicLayout .footer {
  background: #efefef;
  padding: 16px;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  text-align: center;
}

动态替换内容

使用 Vue Router,在 router包下index.ts 配置路由,能够根据访问的页面地址找到不同的文件并加载渲染。

Vue Router:介绍 | Vue Router

BasicLayout代码如下:

<a-layout-content class="content">
  <router-view />
</a-layout-content>

样式,要和底部栏保持一定的外边距,否则内容会被遮住(BasicLayout代码如下):

<style scoped>
#basicLayout .content {
  background: linear-gradient(to right, #fefefe, #fff);
  margin-bottom: 28px;
  padding: 20px;
}
</style>

全局顶部栏

基于 Arco Design 的菜单组件来创建 GlobalHeader 全局顶部栏组件:Arco Design Vue

在基础布局中引入(BasicLayout代码如下):

<a-layout-header class="header">
  <GlobalHeader />
</a-layout-header>

样式:

#basicLayout .header {
  margin-bottom: 16px;
  box-shadow: #eee 1px 1px 5px;
}

GlobalHeader 组件定制,补充网站图标:1808505663815548929_0.3092622000802707

阿里云盘:阿里云盘分享 提取码: 28gu

在根目录下的components包下创建GlobalHeader.vue(写顶部栏的样式):

模板代码:

        <a-menu-item
          key="0"
          :style="{ padding: 0, marginRight: '38px' }"
          disabled
        >
          <div class="titleBar">
            <img class="logo" src="../assets/logo.png" />
            <div class="title">鱼答答</div>
          </div>
        </a-menu-item>

样式:

<style scoped>
#globalHeader {
}

.titleBar {
  display: flex;
  align-items: center;
}

.title {
  margin-left: 16px;
  color: black;
}

.logo {
  height: 48px;
}
</style>

顶部导航栏右侧展示登录状态,左右布局:

  <a-row id="globalHeader" align="center" :wrap="false">
    <a-col flex="auto">
      <a-menu
        mode="horizontal"
      >
        。。。
      </a-menu>
    </a-col>
    <a-col flex="100px">
      <div>
        <a-button type="primary" href="/user/login">登录</a-button>
      </div>
    </a-col>
  </a-row>

通用路由菜单

目标:根据路由配置信息,自动生成菜单内容。实现更通用、更自动的菜单配置。

通用路由菜单组件实现步骤:

  1. 提取通用路由文件
  2. 菜单组件读取路由,动态渲染菜单项
  3. 绑定跳转事件
  4. 同步路由的更新到菜单项高亮
  5. 按需补充更多能力

依次实现:

1)提取通用路由文件

把 router/index.ts 中的路由变量定义为单独的文件 routes.ts,代码如下:

export const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
 {
    path: "/about",
    name: "about",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
  },
];

然后在 router/index.ts 中引入 routes。

import { routes } from "@/router/routes";

2)菜单组件读取路由,动态渲染菜单项

GlobalHeader.vue中:

<script setup lang="ts">
import { routes } from "@/router/routes";
</script>

模板中根据路由数组渲染菜单:

<a-menu-item v-for="item in visibleRoutes" :key="item.path">
          {{ item.name }}
        </a-menu-item>
      </a-menu>

3)绑定跳转事件

4)同步路由的更新到菜单项高亮

同步高亮原理:首先点击菜单项 => 触发点击事件并跳转更新路由 => 更新路由后,同步去更新菜单栏的高亮状态。1808505663815548929_0.016312673248611853

使用 Vue Router 的 afterEach 路由钩子实现:

const router = useRouter();
// 当前选中的菜单项
const selectedKeys = ref(["/"]);
// 路由跳转时,自动更新选中的菜单项
router.afterEach((to, from, failure) => {
  selectedKeys.value = [to.path];
});

模板引入变量:

<a-menu
        mode="horizontal"
        :selected-keys="selectedKeys"
        @menu-item-click="doMenuClick"
      >
</a-menu>

还可以给路由菜单组件增加更多能力。

5)按需补充更多能力(可以参考网上的框架),比如根据配置控制菜单的显隐。

利用 routes 配置的 meta 属性实现。routes.ts 中给路由配置新增一个标志位 hideInMenu,用于判断路由是否显隐:

然后根据该标志位过滤路由数组,仅保留需要展示的元素。

不要用 v-for + v-if 去条件渲染元素,这样会先循环所有的元素,导致性能的浪费。

支持多套布局

需求:不是所有页面都能统一布局,比如用户登录注册页不需要导航栏,因此模板需要多套布局能力。

新建布局 UserLayout,代码如下:

<template>
  <div id="userLayout">
    <a-layout style="height: 100vh">
      <a-layout-header class="header">
        <a-space>
          <img class="logo" src="../assets/logo.png" />
          <div>鱼答答 AI 答题应用平台</div>
        </a-space>
      </a-layout-header>
      <a-layout-content class="content">
        <router-view />
      </a-layout-content>
      <a-layout-footer class="footer">
        <a href="https://www.code-nav.cn" target="_blank">
          编程导航 by 程序员鱼皮
        </a>
      </a-layout-footer>
    </a-layout>
  </div>
</template>

<script setup lang="ts"></script>

<style scoped>
#userLayout {
  text-align: center;
  background: url("https://gw.alipayobjects.com/zos/rmsportal/FfdJeJRQWjEeGTpqgBKj.png")
    0% 0% / 100% 100%;
}

#userLayout .logo {
  width: 48px;
  height: 48px;
}

#userLayout .header {
  margin-top: 16px;
  font-size: 21px;
}

#userLayout .content {
  margin-bottom: 16px;
  padding: 20px;
}

.footer {
  padding: 16px;
  text-align: center;
  background: #efefef;
}
</style>

实现多套布局的思路:

1)使用 vue-router 自带的子路由机制,天然实现布局和嵌套路由,不同的父路由指定不同的 Layout 即可。

比如用户登录注册相关的路由配置:

需要新建 UserLayout、UserLoginView、UserRegisterView 页面,并且在 routes 中引入。

3)在 App.vue 根页面文件,根据路由的路径选择是否使用全局基础布局。

注意,当前这种 app.vue 中通过 if else 区分布局的方式,不是最优雅的。可以通过配置路由的 meta.layout 参数决定是否开启全局基础布局;或者给需要全局基础布局的页面指定 component: BasicLayout。

请求

引入 Axios 请求库 + ⭐️ OpenAPI 前端代码生成

请求工具库

安装请求工具类 Axios

官方文档:https://axioshttp.com/docs/intro

终端:

npm install axios

全局自定义请求

需要自定义全局请求地址等,参考 Axios 官方文档,编写请求配置文件 request.ts。包括全局接口请求地址、超时时间、自定义请求响应拦截器等。

比如可以在全局响应拦截器中,读取出结果中的 data,并校验 code 是否合法,如果是未登录状态,则自动登录。

示例代码如下,其中 withCredentials: true 一定要写,否则无法在发请求时携带 Cookie,就无法完成登录。

request.ts代码如下:

import axios from "axios";
import { Message } from "@arco-design/web-vue";

const myAxios = axios.create({
  baseURL: "http://localhost:8101",
  timeout: 10000,
  withCredentials: true,
});

// 全局请求拦截器
myAxios.interceptors.request.use(
  function (config) {
    // Do something before request is sent
    return config;
  },
  function (error) {
    // Do something with request error
    return Promise.reject(error);
  }
);

// 全局响应拦截器
myAxios.interceptors.response.use(
  function (response) {
    console.log(response);
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    const { data } = response;

    // 未登录
    if (data.code === 40100) {
      // 不是获取用户信息的请求,并且用户目前不是已经在用户登录页面,则跳转到登录页面
      if (
        !response.request.responseURL.includes("user/get/login") &&
        !window.location.pathname.includes("/user/login")
      ) {
        Message.warning("请先登录");
        window.location.href = `/user/login?redirect=${window.location.href}`;
      }
    }

    return response;
  },
  function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
  }
);

export default myAxios;

自动生成请求代码

传统情况下,每个请求都要单独编写代码,很麻烦。

推荐使用 OpenAPI 工具,直接自动生成即可:@umijs/openapi - npm

按照官方文档的步骤,先安装:

npm i --save-dev @umijs/openapi

在项目根目录新建 openapi.config.ts,根据自己的需要定制生成的代码:

const { generateService } = require("@umijs/openapi");

generateService({
  requestLibPath: "import request from '@/request'",
  schemaPath: "http://localhost:8101/api/v2/api-docs",
  serversPath: "./src",
});

在 package.json 的 script 中添加 "openapi": "ts-node openapi.config.ts"

如果 ts-node 无法运行,改为 node

执行即可生成请求代码。

全局状态管理

什么是全局状态管理?

所有页面全局共享的变量,而不是局限在某一个页面中。

适合作为全局状态的数据:已登录用户信息(每个页面几乎都要用)

Pinia 是一个主流的状态管理库,使用更简单,入门文档:开始 | Pinia

1)先按照文档引入 Pinia

2)定义状态。在 store 目录下定义 user 模块,定义了用户的存储、远程获取、修改逻辑:

import { defineStore } from "pinia";
import { ref } from "vue";
import { getLoginUserUsingGet } from "@/api/userController";
import ACCESS_ENUM from "@/access/accessEnum";

/**
 * 登录用户信息全局状态
 */
export const useLoginUserStore = defineStore("loginUser", () => {
  const loginUser = ref<API.LoginUserVO>({
    userName: "未登录",
  });

  function setLoginUser(newLoginUser: API.LoginUserVO) {
    loginUser.value = newLoginUser;
  }

  async function fetchLoginUser() {
    const res = await getLoginUserUsingGet();
    if (res.data.code === 0 && res.data.data) {
      loginUser.value = res.data.data;
    } else {
      loginUser.value = { userRole: ACCESS_ENUM.NOT_LOGIN };
    }
  }

  return { loginUser, setLoginUser, fetchLoginUser };
});

3)使用状态。直接使用 store 中导出的状态变量和函数。

可以在首次进入到页面时,尝试获取登录用户信息。修改 App.vue,编写远程获取数据代码:

在任何页面中都可以使用数据,比如在页面中直接展示:

顶部导航栏右侧展示登录状态:

全局权限管理

需求:能够灵活配置每个页面所需要的用户权限,由全局权限管理系统自动校验和拦截,而不需要在每个页面中编写权限校验代码,提高开发效率。

实现方案:

  1. 在路由配置文件, 定义某个路由的访问权限
  2. 使用全局路由监听器,每次访问页面时,根据用户要访问页面的路由权限信息,判断用户是否有对应的访问权限,并进行相应的拦截处理。

新建 NoAuth 无权限页面,内容随便写。

新建 access 目录,所有权限管理相关的代码都放在该目录下,模块化。只要不引入,就不会生效。

1)定义权限枚举文件 accessEnum.ts:

/**
 * 权限定义
 */
const ACCESS_ENUM = {
  NOT_LOGIN: "notLogin",
  USER: "user",
  ADMIN: "admin",
};

export default ACCESS_ENUM;

2)routes.ts 中新增一个测试路由:

3)编写通用的权限校验方法。1808505663815548929_0.7997078501634929

为什么?因为菜单组件中要判断权限、权限拦截也要用到权限判断功能,所以抽离成公共模块。

checkAccess.ts 文件:

import ACCESS_ENUM from "@/access/accessEnum";

/**
 * 检查权限(判断当前登录用户是否具有某个权限)
 * @param loginUser 当前登录用户
 * @param needAccess 需要有的权限
 * @return boolean 有无权限
 */
const checkAccess = (
  loginUser: API.LoginUserVO,
  needAccess = ACCESS_ENUM.NOT_LOGIN
) => {
  // 获取当前登录用户具有的权限(如果没有 loginUser,则表示未登录)
  const loginUserAccess = loginUser?.userRole ?? ACCESS_ENUM.NOT_LOGIN;
  if (needAccess === ACCESS_ENUM.NOT_LOGIN) {
    return true;
  }
  // 如果用户要登录才能访问
  if (needAccess === ACCESS_ENUM.USER) {
    // 如果用户没登录,那么表示无权限
    if (loginUserAccess === ACCESS_ENUM.NOT_LOGIN) {
      return false;
    }
  }
  // 如果管理员才能访问
  if (needAccess === ACCESS_ENUM.ADMIN) {
    // 如果不是管理员,表示无权限
    if (loginUserAccess !== ACCESS_ENUM.ADMIN) {
      return false;
    }
  }
  return true;
};

export default checkAccess;

4)编写全局权限校验核心文件。

逻辑如下:

  1. 首先判断页面是否需要登录权限,如果不需要,直接放行。
  2. 如果页面需要登录权限
    1. 如果用户未登录,则跳转到登录页面。
    2. 如果已登录,判断登录用户的权限是否符合要求,否则跳转到 401 无权限页面。

access包下的index.ts实现代码如下:

import router from "@/router";
import { useLoginUserStore } from "@/store/userStore";
import ACCESS_ENUM from "@/access/accessEnum";
import checkAccess from "@/access/checkAccess";

// 进入页面前,进行权限校验
router.beforeEach(async (to, from, next) => {
  // 获取当前登录用户
  const loginUserStore = useLoginUserStore();
  let loginUser = loginUserStore.loginUser;

  // 如果之前没有尝试获取过登录用户信息,才自动登录
  if (!loginUser || !loginUser.userRole) {
    // 加 await 是为了等待用户登录成功并获取到值后,再执行后续操作
    await loginUserStore.fetchLoginUser();
    loginUser = loginUserStore.loginUser;
  }

  // 当前页面需要的权限
  const needAccess = (to.meta?.access as string) ?? ACCESS_ENUM.NOT_LOGIN;
  // 要跳转的页面必须登录
  if (needAccess !== ACCESS_ENUM.NOT_LOGIN) {
    // 如果没登录,跳转到登录页面
    if (
      !loginUser ||
      !loginUser.userRole ||
      loginUser.userRole === ACCESS_ENUM.NOT_LOGIN
    ) {
      next(`/user/login?redirect=${to.fullPath}`);
    }
    // 如果已经登录了,判断权限是否足够,如果不足,跳转到无权限页面
    if (!checkAccess(loginUser, needAccess)) {
      next("/noAuth");
      return;
    }
  }
  next();
});

在 main.ts 中引入,即可生效权限校验:

import "@/access";

注意,必须保证 pinia 初始化在这段代码执行前,所以需要将 useLoginUserStore() 函数放到 router.beforeEach 参数里。

参考:Using a store outside of a component | Pinia

5)支持全局自动登录。如果是 首次 进入页面,状态为未登陆,则自动登录。

如何区别是否为首次进入页面(还没尝试过获取登录用户)呢?

默认的 loginUser 是没有 userRole 的,只要获取过,哪怕未登录,也可以给设置一个 userRole 为 "notLogin"。

修改 user 状态管理代码:

在 access/index.ts 开头补充自动登录逻辑

之后记得移除 App.vue 中的登录逻辑。

附加需求:根据权限控制菜单显隐。

需求:只有具有权限的菜单,才对用户可见

原理:类似上面的控制路由显示隐藏,只要判断用户没有这个权限,就直接过滤掉。

修改 GlobalHeader 全局导航栏(通用菜单)组件,补充根据权限来过滤菜单的逻辑。

全局项目入口

app.vue 中预留一个可以编写全局初始化逻辑的代码:

通用组件 - Markdown 编辑器组件

为什么用 Markdown?

一套通用的文本编辑语法,可以在各大网站上统一标准、渲染出统一的样式,比较简单易学。

推荐的 Md 编辑器:GitHub - bytedance/bytemd: ByteMD v1 repository

阅读官方文档,下载编辑器主体、以及 gfm(表格支持)插件、highlight 代码高亮插件

npm i @bytemd/vue-next
npm i @bytemd/plugin-highlight @bytemd/plugin-gfm

新建 MdEditor 组件,编写代码:

<template>
  <Editor :value="value" :plugins="plugins" @change="handleChange" />
</template>

<script setup lang="ts">
import gfm from "@bytemd/plugin-gfm";
import highlight from "@bytemd/plugin-highlight";
import { Editor, Viewer } from "@bytemd/vue-next";
import { ref } from "vue";

const plugins = [
  gfm(),
  highlight(),
  // Add more plugins here
];

const value = ref("");

const handleChange = (v: string) => {
  value.value = v;
};
</script>

<style scoped></style>

隐藏编辑器中不需要的操作图标(比如 GitHub 图标):

.bytemd-toolbar-icon.bytemd-tippy.bytemd-tippy-right:last-child {
    display: none;
}

要把 MdEditor 当前输入的值暴露给父组件,便于父组件去使用,同时也是提高组件的通用性,需要定义属性,把 value 和 handleChange 事件交给父组件去管理:

MdEditor 示例代码:

有编辑器就有浏览器,MdViewer 示例代码如下:

通用组件 - 图片上传

先复制现成的组件代码:Arco Design Vue

然后进行修改,先重构为 setup 组合式 API 写法,自定义请求,跑通流程。

然后再改为受控组件,接受外层传来的 url 和 onChange 函数,之后的表单页面就可以引用了。

PictureUploader.vue 代码如下:

<template>
  <a-space direction="vertical" :style="{ width: '100%' }">
    <a-upload
      :fileList="file ? [file] : []"
      :show-file-list="false"
      :custom-request="customRequest"
    >
      <template #upload-button>
        <div
          :class="`arco-upload-list-item${
            file && file.status === 'error'
              ? ' arco-upload-list-item-error'
              : ''
          }`"
        >
          <div
            class="arco-upload-list-picture custom-upload-avatar"
            v-if="file && file.url"
          >
            <img :src="file.url" />
            <div class="arco-upload-list-picture-mask">
              <IconEdit />
            </div>
            <a-progress
              v-if="file.status === 'uploading' && file.percent < 100"
              :percent="file.percent"
              type="circle"
              size="mini"
              :style="{
                position: 'absolute',
                left: '50%',
                top: '50%',
                transform: 'translateX(-50%) translateY(-50%)',
              }"
            />
          </div>
          <div class="arco-upload-picture-card" v-else>
            <div class="arco-upload-picture-card-text">
              <IconPlus />
              <div style="margin-top: 10px; font-weight: 600">上传</div>
            </div>
          </div>
        </div>
      </template>
    </a-upload>
  </a-space>
</template>

<script setup lang="ts">
import { IconEdit, IconPlus } from "@arco-design/web-vue/es/icon";
import { ref, withDefaults, defineProps } from "vue";
import { uploadFileUsingPost } from "@/api/fileController";
import { Message } from "@arco-design/web-vue";

/**
 * 定义组件属性类型
 */
interface Props {
  biz: string;
  onChange?: (url: string) => void;
  value?: string;
}

/**
 * 给组件指定初始值
 */
const props = withDefaults(defineProps<Props>(), {
  value: () => "",
});

const file = ref();
if (props.value) {
  file.value = {
    url: props.value,
    percent: 100,
    status: "done",
  };
}

// 自定义请求
const customRequest = async (option: any) => {
  const { onError, onSuccess, fileItem } = option;

  const res: any = await uploadFileUsingPost(
    { biz: props.biz },
    {},
    fileItem.file
  );
  if (res.data.code === 0 && res.data.data) {
    const url = res.data.data;
    file.value = {
      name: fileItem.name,
      file: fileItem.file,
      url,
    };
    props.onChange?.(url);
    onSuccess();
    console.log(file.value);
  } else {
    Message.error("上传失败," + res.data.message || "");
    onError(new Error(res.data.message));
  }
};
</script>

二、前端基础页面开发

用户模块

各项目通用

用户模块路由配置:

用户登录页面

使用表单组件

<template>
  <div id="userLoginPage">
    <h2 style="margin-bottom: 16px">用户登录</h2>
    <a-form
      :model="form"
      :style="{ width: '480px', margin: '0 auto' }"
      label-align="left"
      auto-label-width
      @submit="handleSubmit"
    >
      <a-form-item field="userAccount" label="账号">
        <a-input v-model="form.userAccount" placeholder="请输入账号" />
      </a-form-item>
      <a-form-item field="userPassword" tooltip="密码不小于 8 位" label="密码">
        <a-input-password
          v-model="form.userPassword"
          placeholder="请输入密码"
        />
      </a-form-item>
      <a-form-item>
        <div
          style="
            display: flex;
            width: 100%;
            align-items: center;
            justify-content: space-between;
          "
        >
          <a-button type="primary" html-type="submit" style="width: 120px">
            登录
          </a-button>
          <a-link href="/user/register">新用户注册</a-link>
        </div>
      </a-form-item>
    </a-form>
  </div>
</template>

<script setup lang="ts">
import { reactive } from "vue";
import API from "@/api";
import { userLoginUsingPost } from "@/api/userController";
import { useLoginUserStore } from "@/store/userStore";
import message from "@arco-design/web-vue/es/message";
import { useRouter } from "vue-router";

const loginUserStore = useLoginUserStore();
const router = useRouter();

const form = reactive({
  userAccount: "",
  userPassword: "",
} as API.UserLoginRequest);

/**
 * 提交
 */
const handleSubmit = async () => {
  const res = await userLoginUsingPost(form);
  if (res.data.code === 0) {
    await loginUserStore.fetchLoginUser();
    message.success("登录成功");
    router.push({
      path: "/",
      replace: true,
    });
  } else {
    message.error("登录失败," + res.data.message);
  }
};
</script>

用户注册页面

参考用户登录页面,同样使用表单组件。1808505663815548929_0.27247751987834223

需要让两个页面之间能够相互跳转。

用户管理页面

需求:管理用户 - 增删改查(仅管理员可用)P11808505663815548929_0.04351610257466798

新增路由:

编写页面:上方搜索栏,下方表格。

1)先开发表格。

使用表格组件:

定义表格列:

自定义渲染表格列:

然后需要在 columns 中补充插槽配置:

搜索条件:

数据加载:

2)再开发搜索表单

创建和更新功能其实就是表单页面。

管理模块

应用管理

题目管理

评分结果管理

回答管理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/801076.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

跟着操作,解决iPhone怎么清理内存难题

在如今智能手机功能日益强大的时代&#xff0c;我们使用手机拍照、录制视频、下载应用、存储文件等操作都会占用手机内存。当内存空间不足时&#xff0c;手机运行会变得缓慢&#xff0c;甚至出现卡顿、闪退等现象。因此&#xff0c;定期清理iPhone内存是非常必要的。那么&#…

最新 taro v3 运行,报错 Error: [object Object] is not a PostCSS plugin 解决办法

报错如下&#xff1a; Error: [object Object] is not a PostCSS plugin 解决办法&#xff1a;pnpm install postcss -D 重新安装 postcss 依赖&#xff0c;重新运行即可。 结果&#xff1a;顺利运行

2000-2023年上市公司融资约束WW指数(含原始数据+计算结果)

2000-2023年上市公司融资约束WW指数&#xff08;含原始数据计算结果&#xff09; 1、时间&#xff1a;2000-2023年 2、来源&#xff1a;上市公司年报 3、指标&#xff1a;证券代码、证券简称、统计截止日期、是否发生ST或*ST或PT、是否发生暂停上市、行业代码、行业名称、上…

opengauss数据库兼容模式

一、官方说明 官方描述&#xff1a; 背景信息 初始时&#xff0c;openGauss包含两个模板数据库template0、template1&#xff0c;以及一个默认的用户数据库postgres。postgres默认的兼容数据库类型为O&#xff08;即DBCOMPATIBILITY A &#xff09;&#xff0c;该兼容类型下…

nginx的正向与反向代理

正向代理与反向代理的区别 虽然正向代理和反向代理都涉及代理服务器接收客户端请求并向服务端转发请求&#xff0c;但它们之间存在一些关键的区别&#xff1a; 正向代理&#xff1a; 在正向代理中&#xff0c;代理服务器代表客户端向服务器发送请求&#xff0c;并将服务…

怎么调整硬盘分区?让电脑运行更加高效!

硬盘分区是电脑存储管理的重要组成部分&#xff0c;合理的分区设置不仅能提高数据管理的效率&#xff0c;还能在一定程度上提升系统的运行性能。然而&#xff0c;随着使用需求的变化&#xff0c;我们可能需要对已有的硬盘分区进行调整。那么&#xff0c;我们该怎么调整硬盘分区…

PostgreSQL的学习心得和知识总结(一百四十八)|查看 PostgreSQL 17 中的新内置排序规则提供程序

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《PostgreSQL数据库内核分析》 2、参考书籍&#xff1a;《数据库事务处理的艺术&#xff1a;事务管理与并发控制》 3、PostgreSQL数据库仓库…

数码暴龙机(电波暴龙机)彩色复刻版!!| 使用Python、PySide6、pixilart自制windows桌面宠物

一、前言 数码暴龙机&#xff08;电波暴龙机&#xff09;是万代公司发售的一系列与《数码兽》系列相关的液晶玩具商品。这些产品融合了养成和对战元素&#xff0c;为玩家提供了一种虚拟养成和战斗的娱乐体验。也是很多人的童年回忆。最近在B站刷到讲解暴龙通关的教程和视频&…

ROS2 + 科大讯飞 初步实现机器人语音控制

环境配置&#xff1a; 电脑端&#xff1a; ubuntu22.04实体机作为上位机 ROS版本&#xff1a;ros2-humble 实体机器人&#xff1a; STM32 思岚A1激光雷达 科大讯飞语音SDK 讯飞开放平台-以语音交互为核心的人工智能开放平台 实现步骤&#xff1a; 1. 下载和处理科大讯飞语音模…

SQL Server的视图

SQL Server的视图 一、基础 SQL 视图&#xff08;Views&#xff09;是一种虚拟表&#xff0c;是基于 SQL 查询结果生成的。这些虚拟表可以包含来自一个或多个表的数据&#xff0c;并且可以像表一样查询&#xff1b;视图是一个表中的数据经过某种筛选后的显示方式&#xff0c;或…

Cornerstone3D导致浏览器崩溃的踩坑记录

WebGL: CONTEXT_LOST_WEBGL: loseContext: context lost ⛳️ 问题描述 在使用vue3vite重构Cornerstone相关项目后&#xff0c;在Mac本地运行良好&#xff0c;但是部署测试环境后&#xff0c;在window系统的Chrome浏览器中切换页面会导致页面崩溃。查看Chrome的任务管理器&am…

对话天润融通首席科学家:大模型的首要任务是为客户创造商业价值

2023年&#xff0c;AI大模型开启了企业数智化转型的新篇章。 不过前沿技术固然重要&#xff0c;但在增长见顶的存量背景下&#xff0c;先进技术带来的实用价值也尤为关键。 正如天润融通首席科学家田凤占所说&#xff1a;“现阶段最重要的是让大模型尽快和企业的业务相结合&a…

【Linux】进程间通信——消息队列和信号量

目录 消息队列&#xff08;message queue&#xff09; 信号量&#xff08;Semaphore&#xff09; system V版本的进程间通信方式有三种&#xff1a;共享内存&#xff0c;消息队列和信号量。之前我们已经说了共享内存&#xff0c;那么我们来看一下消息队列和信号量以及它们之间…

【嵌入式Linux】<总览> 网络编程(更新中)

文章目录 前言 一、网络知识概述 1. 网路结构分层 2. socket 3. IP地址 4. 端口号 5. 字节序 二、网络编程常用API 1. socket函数 2. bind函数 3. listen函数 4. accept函数 5. connect函数 6. read和recv函数 7. write和send函数 三、TCP编程 1. TCP介绍 2.…

新版本WPS不登录无法编辑的解决办法

原因分析&#xff1a;新版本的WPS因加入多种在线功能&#xff0c;建议登录账号获得更加体验 解决办法&#xff1a;首选第一种修改注册表后重启WPS&#xff0c;第二种仅作为临时满足工作需求&#xff0c;过一段时间会自动失效 方法一&#xff1a;键盘同时按下WINR键&#xff0c;…

【Python】基础语法(函数、列表和元组、字典、文件)

。一、函数 1、函数是什么 编程中的函数和数学中的函数有一定的相似之处。 数学上的函数&#xff0c;比如 y sin x&#xff0c;x 取不同的值&#xff0c;y 就会得到不同的结果。 编程中的函数是一段可以被重复使用的代码片段。 &#xff08;1&#xff09;求数列的和&…

MySQL索引特性(上)

目录 索引的重要 案例 认识磁盘 MySQL与存储 先来研究一下磁盘 扇区 定位扇区 结论 磁盘随机访问与连续访问 MySQL与磁盘交互基本单位 建立共识 索引的理解 建立测试表 插入多条记录 局部性原理 所有的MySQL的操作(增删查改)全部都是在MySQL当中的内存中进行的&am…

网友提问:HTML CSS JS很低级吗?

这话听起来就像有人在说披萨只是面团加奶酪&#xff0c;完全忽略了上面的美味配料和烘烤的艺术啊&#xff01;HTML、CSS、JS这三位可是前端开发的铁三角&#xff0c;它们一点都不“低级”&#xff0c;反而相当关键。 HTML就像是房子的骨架&#xff0c;没有它&#xff0c;网页就…

【iOS】——MRC

一、引用计数 内存管理的核心是引用计数器&#xff0c;用一个整数来表示对象被引用的次数&#xff0c;系统需要根据引用计数器来判断对象是否需要被回收。 在每次 RunLoop 迭代结束后&#xff0c;都会检查对象的引用计数器&#xff0c;如果引用计数器等于 0&#xff0c;则说明…

单链表算法 - 链表分割

链表分割_牛客题霸_牛客网现有一链表的头指针 ListNode* pHead&#xff0c;给一定值x&#xff0c;编写一段代码将所有小于x的。题目来自【牛客题霸】https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70思路: 代码: /* struct ListNode {int val;struct List…