目录
一、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)提取通用路由文件
把 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,编写远程获取数据代码:
在任何页面中都可以使用数据,比如在页面中直接展示:
顶部导航栏右侧展示登录状态:
全局权限管理
需求:能够灵活配置每个页面所需要的用户权限,由全局权限管理系统自动校验和拦截,而不需要在每个页面中编写权限校验代码,提高开发效率。
实现方案:
- 在路由配置文件, 定义某个路由的访问权限
- 使用全局路由监听器,每次访问页面时,根据用户要访问页面的路由权限信息,判断用户是否有对应的访问权限,并进行相应的拦截处理。
新建 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)编写全局权限校验核心文件。
逻辑如下:
- 首先判断页面是否需要登录权限,如果不需要,直接放行。
- 如果页面需要登录权限
-
- 如果用户未登录,则跳转到登录页面。
- 如果已登录,判断登录用户的权限是否符合要求,否则跳转到 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)再开发搜索表单
创建和更新功能其实就是表单页面。