前端:Vue3,Vite,TypeScript,SCSS,Element Plus,Router,axios
后端:.NET6,Minimal API,Sql sugar(CodeFirst)
根据哔哩哔哩视频整理的,比较详细了,建议先根据文档写,有问题再看代码,记忆比较深刻。
代码已上传.
哔哩哔哩视频地址:001.教学演示和技术栈介绍_哔哩哔哩_bilibili
------------------------------------------------------------安装环境---------------------------------------------------
安装nodejs:
开发vue项目需要通过那npm指令,安装这个只是为了使用npm命令
下载地址:http://nodejs.cn/download/
下载是否成功测试:node --version
安装pnpm:
官网:https://www.pnpm.cn/
安装:npm install -g pnpm@next-7
相比npm更快更稳定,所有使用npm安装包的命令都能替换成pnpm;pnpm dev等价于npm run dev
创建前端项目:
新建文件夹,在文件夹路径进入cmd,执行指令:pnpm create vite@latest web(web为项目名称)
指令执行完毕会返回一些打印,让你选择项目模板,按上下键选择,我们选择Vue然后回车
再次选择TypeScript
然后会返回一些打印,提示你运行的步骤
用VSCODE打开项目:
文件-->打开文件夹-->选择刚才创建的文件
node_modules:模块包(通过install安装的包都在这个目录里面)
public:公共资源(json,images)
src:项目目录
assets:静态资源
components:组件
App.vue:根组件
main.ts:根函数入口,全局配置生效的地方
package.json:项目配置文件,项目的标题,版本,模块的版本等信息
如果没有node_modules文件,只需要在终端执行pnpm install即可
vite.config.ts文件可以配置ip和端口,以及启动后是否默认打开浏览器
server:{
host:"127.0.0.1",
port:3001,
open:true
}
server:{open: true} //自动打开浏览器
安装sass
官网介绍:https://www.sass.hk/guide/
命令:pnpm install sass
目的:支持嵌套css编写;支持定义变量
安装路由(多界面相互之间跳转):
介绍:https://router.vuejs.org/zh/introduction.html
指令:pnpm install vue-router@4
测试:
src下新建router文件夹下新建index.ts文件
import { createRouter, createWebHistory } from 'vue-router'
src下新建views文件夹下新建HomePage.vue与TestPage.vue文件,格式如下,内容不同
HomePage.vue:
<template>
<div>0000</div>
</template>
TestPage.vue:
<template>
<div>1111</div>
</template>
在main.ts中配置路由,增加下面两行
import router from './router/index'
create(App).use(router).mount('#app')
router文件夹下index.ts配置
import {createRouter,createWebHistory} from 'vue-router'
var router=createRouter({
history:createWebHistory(),
routes:[
{name:"home", path:"/", component: () => import("../views/HomePage.vue")}, //name:路由名字;path:路由路径;import:路由组件
{name:"test", path:"/test", component: () => import("../views/TestPage.vue")}
]
})
export default router
解决./App.vue或者其他.vue文件报错,在vite-env.d.ts增加以下代码解决(ts需要固定格式,这里跳过格式检查)
declare module '*.vue' {
import { App, defineComponent } from 'vue'
const component: ReturnType<typeof defineComponent> & {
install(app: App): void
}
export default component
}
在App.vue文件中添加<router-view></router-view>,此标签就是表示把路由页面呈现在哪个地方
------------------------------------------------------集成element-plus--------------------------------------------------
Element Plus
官网介绍:https://element-plus.gitee.io/zh-CN/
安装命令:pnpm install element-plus
在element-plus官网中快速入门
按需引入包:
pnpm install -D unplugin-vue-components unplugin-auto-import
因为我使用的是Vite,所以就用教程的Vite引入,其他自己做的页面的话就用Webpack
首先在vite.config.ts文件中导入:
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
再在plugins的vue(),后面添加
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
在HomePage.vue文件里面把element的组件(随便在element官网找几个button按钮组件)写入进去,运行代码,查看是否是element的UI
按需导入后,组件可以使用了,但是少数部分组件会有报错,但是不影响功能,如果想要解决报错信息,需要修改tsconfig.json文件
在include节点里面加入:"**/*.d.ts"即可导入图标:
根据element官网文档
icon图标:先安装包管理器:pnpm install @element-plus/icons-vue
注册所有图标(放在main.ts文件中):
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
最后一行改为app.use(router).mount('#app')
在HomePage.vue文件加入icon图标代码(element文档Button里面有)
例如:
<el-button :icon="Search" circle />
<el-button type="primary" :icon="Edit" circle />
还要加入下面的代码
<script lang="ts" setup>
import {
Check,
Delete,
Edit,
Message,
Search,
Star,
} from '@element-plus/icons-vue'
</script>
如果有报错就把tsconfig.json里面的配置修改为:
"moduleResolution": "Node",
"noUnusedLocals": false,
-----------------------------------------------------静态页面的实现-------------------------------------------------------
注释掉main.ts的import './style.css' //不用自带的样式
HomePage.vue增加:
<template> //页面
<div></div>
</template>
<script lang="ts" setup> //脚本
</script>
<style lang="scss" scoped> //scss:样式;scoped:只在本页面有效,不影响其他页面
</style>
以element组件表格为例,直接复制到HomePage.vue页面
然后在style标签添加:
.container{ //居中
margin: 100px auto;
width: 50%;
}
.table{ //表格间距等
margin: 10px 0;
}
增加页面的搜索,添加,删除操作:div中增加以下代码
<el-row>
<el-col :span="12">
<el-input v-model="SearchVal" placeholder="Please input" class="input-with-select" @keyup.enter="enterSearch">
<template #append>
<el-button :icon="Search" @click="enterSearch"></el-button>
</template>
</el-input>
</el-col>
<el-col :span="12">
<el-button type="primary" @click="openAdd">add</el-button>
<el-button type="danger" @click="onDel">delete</el-button>
</el-col>
</el-row>
script标签增加:
import { ref } from 'vue';
import { Search } from '@element-plus/icons-vue';
const SearchVal = ref("")
const enterSearch=()=>{
}
const onDel=()=>{
}
const openAdd=()=>{
}
增加多选框:
在table里面添加
<el-table-column type="selection" width="55"></el-table-column> //多选框
<el-table-column prop="order" label="Order" width="80" /> //排序列
在script数据里面增加字段order,例如:order: 1,在table标签最后一列增加数据后修改与删除按钮(插槽):
<el-table-column label="Operations">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.$index,scope.row)">Edit</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.$index,scope.row)">Delete</el-button>
</template>
</el-table-column>
修改与删除按钮实现方法:
const handleEdit=(index: number, row: any)=>{ //index类型指定number,row类型指定任何数据类型都可以
console.log(index,row)
}
const handleDelete=(index: number, row: any)=>{
console.log(index,row)
}
在div里面,table外增加分页代码:
<el-pagination background layout="prev,pager,next" :total="total" @current-change="currentChange"></el-pagination>
实现:
const total = ref(100) //最大100页
每页数据:
const currentChange=(val:number)=>{
console.log(val)
}
--------------------------------------------------增加add与edit组件-----------------------------------------------------
components文件夹下增加add.vue文件
使用官网的弹窗组件
<template>
<el-dialog v-model="dialogVisible" title="新增" width="30%">
<el-form>
<el-form-item label="时间" prop="date">
<el-date-picker v-model="form.date" type="date" placeholder="请选择一个时间" :disabled-date="disablesDate"></el-date-picker>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="form.address"></el-input>
</el-form-item>
<el-form-item label="排序" prop="order">
<el-input v-model.number="form.order"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeAdd()">Cancel</el-button>
<el-button type="primary" @click="save()">Confirm</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue' //导入ref
const dialogVisible = ref(false) //控制显示与隐藏,默认不显示
const form = ref()
const disablesDate = (time: any) =>{
//最大时间,从今天开始,只能选今天及以后的时间
const _maxTime = Date.now() - 24 * 60 * 60 * 1000 * 1
return time.getTime() <= _maxTime
}
const closeAdd = () =>{
}
const save = () =>{
}
</script>
------------------------------------------页面引用组件(实现点击按钮跳出弹框)-------------------------------------
在src下新建class文件夹,class文件夹下新建User.ts文件
export default class User{ //导出一个默认User类
id: string = ""
date: string = ""
name: string = ""
address: string = ""
order: number = 0
}
add.vue文件修改:
import { ref,Ref,computed } from 'vue' //导入ref与计算函数computed和Ref表单
import User from '../class/User' //导入User对象
const props = defineProps({ //定义一个对象,defineProps为内置函数,用于接收参数
isShow: Boolean, //控制弹窗的弹出与隐藏的参数
info: User //对象类型(参数)
})
const dialogVisible = computed(()=>props.isShow) //控制弹窗显示与隐藏,实现props中isShow与dialogVisible值的联动
定义一个表单:
const form: Ref<User> = ref<User>({ //声明form是User类型
id: "",
date: "",
name: "",
address: "",
order: 0
})
修改HomePage.vue页面
导入弹窗:
<addVue :isShow = "isShow"></addVue>
导入add和User:
import addVue from '../components/add.vue'; //将add.vue导入
import User from '../class/User'; //将User导入
如果addVue报错,禁用Vetur插件,使用Vue Language Features插件
设置默认不显示:
const isShow = ref(false)
数据来源:
const info = ref<User>(new User()) //数据
修改与删除控制:
const handleEdit=(index: number, row: User)=>{ //row:数据类型
console.log(index,row)
info.value = row //编辑时赋值
isShow.value = true //点击编辑时修改为显示
}
const handleDelete=(index: number, row: User)=>{
console.log(index,row)
}
const openAdd=()=>{ //添加时也显示
isShow.value = true
}
运行测试,点击add和edit就可以跳出弹窗
------------------------------------------------------侦听器Watch()-------------------------------------------------------
作用:将页面form与数据info绑定
修改HomePage.vue页面
导入弹窗:
<addVue :isShow = "isShow" :info = "info"></addVue> //增加info,将数据传入
修改add.vue,修改title:
:title="info?.name ? '修改' : '新增'" //改为根据客户点击add还是edit改变title
import { ref, Ref, computed, watch } from 'vue' //增加watch
watch(() => props.info,(newInfo) => { //监听props.info值,如果info值改变,则将改变后的值赋值给newInfo,将newInfo值重新赋值给info;其实这里有两个参数,后面还有一个oldInfo(修改前的数据),但是我们这里用不到
if(newInfo){ //如果newInfo值改变,则重新对form表单进行赋值
form.value = {
id: newInfo.id,
date: newInfo.date,
name: newInfo.name,
address: newInfo.address,
order: newInfo.order
}
}
})
-------------------------------------------------子组件触发父组件事件--------------------------------------------------
在add.vue中定义两个事件:
const emits = defineEmits(["closeAdd","success"])
修改closeAdd事件:
const closeAdd = () => {
emits("closeAdd") //执行closeAdd事件
}
修改save事件:
const save = () => {
emits("success") //执行success事件
}
在子组件触发了父组件的事件,父组件要定义对应事件;在HomePage.vue中定义closeAdd事件与success事件:
const closeAdd = () => {
isShow.value = false //关闭弹窗
info.value = new User() //数据值初始化
}
const success = (message: string) => { //加参数打印成功
isShow.value = false
info.value = new User()
ElMessage.success(message) //成功弹窗出来
}
将事件绑定到弹窗,修改addVue标签
<addVue :isShow="isShow" :info="info" @closeAdd="closeAdd" @success="success"></addVue>
修改add.vue,实现弹窗右上角关闭(x)功能:
在<el-dialog>标签中添加@close="$emit('closeAdd')"
在<el-dialog>标签中添加draggable,实现弹窗随意拖动效果
---------------------------------------------------.NET Core Minimal API----------------------------------------------
打开VS2019 --> 创建新项目 --> 选择ASP.NET Core Web API --> 下一步 --> 修改项目名称和位置 --> 下一步 --> 启用OpenAPI支持,其他不用选 --> 创建
进入Program,删除app.MapGet默认接口,改为:
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//极简API接口,MapGet:访问方式
app.MapGet("/test", () =>
{
return "ok";
});
app.Run();
运行后会到swagger UI,调试成功即可
------------------------------------------------------SqlSugar介绍与安装-----------------------------------------------
官网:
介绍:https://www.donet5.com/Home/Doc
教学:https://www.donet5.com/Doc/29
是一款老牌的.NET开源ORM框架
项目安装引用:
在解决方案资源管理器中右键依赖项 --> 管理NuGet程序包 --> 浏览 --> 输入sqlsugarcore --> 点击后安装即可
安装完成后在项目目录的依赖项下的包下就会看到sqlsugarcore的包,说明安装成功
根目录下新建Model文件夹,Model文件夹中新建User实体类
User内容:
public class User
{
[SugarColumn(IsPrimaryKey = true)] //设置为主键
public string Id { get; set; }
public string Name { get; set; }
public DateTime Date { get; set; }
public string Address { get; set; }
public int Order { get; set; }
}
波浪线无影响,如需要去掉,鼠标放在波浪线,找到无提示即可
--------------------------------------------------------实现CodeFirst-----------------------------------------------------
1.根据数据库中的字符串信息,动态创建数据库
2.通过反射读取当前程序集下的类,然后创建表
3.添加测试数据到数据库,完成初始化根目录下创建Data文件夹,Data文件夹下创建SqlSugarHelper辅助类
public class SqlSugarHelper
{
public static SqlSugarScope Db = new SqlSugarScope(new ConnectionConfig()
{
ConnectionString = "server=127.0.0.1;uid=root;pwd=admin;database=myDatabase",
DbType = DbType.MySql,//设置数据库类型(我用的MySQL,根据自己的需要修改)
IsAutoCloseConnection = true,//自动释放数据务,如果存在事务,在事务结束后释放
InitKeyType = InitKeyType.Attribute //从实体特性中读取主键自增列信息
},
db =>
{
//打印sql日志,调试阶段可以开着,方便调试
db.Aop.OnLogExecuting = (sql, pars) =>
{
Console.WriteLine(sql);
};
}
);
//初始化数据库
public static string InitDateBase()
{
try
{
//创建数据库
Db.DbMaintenance.CreateDatabase();
//初始化数据表,如果没有则创建
string nspace = "api.Model"; //过滤,表示只使用api.Model下的类(User)
//LoadFrom可以加载AppContext.BaseDirectory程序集下的api.dll文件;GetTypes获取dll文件下的所有类;使用where过滤,确保拿到的是api.Model下的文件
Type[] ass = Assembly.LoadFrom(AppContext.BaseDirectory + "api.dll").GetTypes().Where(p => p.Namespace == nspace).ToArray();
Db.CodeFirst.SetStringDefaultLength(200).InitTables(ass); //初始化table
//添加数据前清空数据
Db.Deleteable<User>().ExecuteCommand();
//添加模拟数据
List<User> list = new List<User>();
for (int i = 1; i <= 5; i++)
{
list.Add(new User()
{
Id = Guid.NewGuid().ToString(),
Name = "Tom" + i,
Date = DateTime.Now,
Address = "No. 189, Grove St, Los Angeles",
Order = i
});
}
for (int i = 6; i <= 10; i++)
{
list.Add(new User()
{
Id = Guid.NewGuid().ToString(),
Name = "Tom" + i,
Date = DateTime.Now,
Address = "No. 129, Grove St, Los Angeles",
Order = i
});
}
for (int i = 11; i <= 30; i++)
{
list.Add(new User()
{
Id = Guid.NewGuid().ToString(),
Name = "Tom" + i,
Date = DateTime.Now,
Address = "No. 87, Grove St, Los Angeles",
Order = i
});
}
//添加到表中
Db.Insertable(list).ExecuteCommand();
return "ok";
}
catch(Exception ex)
{
return ex.Message;
}
}
}
-------------------------------------------------------------写接口-----------------------------------------------------------
实体类(正常情况是要新建类,但是demo为了方便,直接写在接口下面)
public class Model
{
public string KeyWord { get; set; } //关键字查询
public int PageIndex { get; set; } //分页
public int PageSize { get; set; }
public int Total { get; set; } //分页总数
}
//对结果包装
public class Result
{
public int Total { get; set; }
public object Res { get; set; }
}
//添加的请求参数
public class AddReq
{
public string Name { get; set; }
public DateTime Date { get; set; }
public string Address { get; set; }
public int Order { get; set; }
}
写查询方法:
//分页查询
public static Result GetUsers(Model req)
{
Result result = new Result();
int total = 0;
result.Res = Db.Queryable<User>() //sql查询
.WhereIF(!string.IsNullOrEmpty(req.KeyWord), s => s.Name.Contains(req.KeyWord) || s.Address.Contains(req.KeyWord)) //判断如果前面部分成立则执行后面的条件,否则全部返回
.OrderBy(s => s.Order) //排序
.ToOffsetPage(req.PageIndex, req.PageSize, ref total); //分页
result.Total = total;
return result;
}
写添加方法:
public static bool Add(AddReq req)
{
User info = new User()
{
Id = Guid.NewGuid().ToString(),
Name = req.Name,
Date = req.Date,
Address = req.Address,
Order = req.Order
};
if (Db.Queryable<User>().Any(p => p.Name == req.Name)) //判断是否存在,存在则返回false
{
return false;
}
return Db.Insertable(info).ExecuteCommand() > 0; //不存在则插入到数据库里面,并且返回插入成功或者失败
}
写修改方法:
public static bool Edit(User req)
{
User info = Db.Queryable<User>().First(p => p.Id == req.Id); //先读一遍
if (info == null) //判断是否存在,不存在则返回false
{
return false;
}
//存在则赋值
info.Name = req.Name;
info.Date = req.Date;
info.Address = req.Address;
info.Order = req.Order;
return Db.Updateable(info).ExecuteCommand() > 0; //更新数据,并且返回更新成功或者失败
}
//删除方法
public static bool Del(string ids)
{
return Db.Ado.ExecuteCommand($"DELETE from `User` WHERE Id IN({ids})") > 0;
}
在Program文件中增加接口实现:
app.MapGet("/codefirst", () =>
{
return SqlSugarHelper.InitDateBase();
});
app.MapPost("/list", (Model req) =>
{
return SqlSugarHelper.GetUsers(req);
});
app.MapPost("/add", (AddReq req) =>
{
return SqlSugarHelper.Add(req);
});
app.MapPost("/edit", (User req) =>
{
return SqlSugarHelper.Edit(req);
});
app.MapGet("/del", (string ids) =>
{
return SqlSugarHelper.Del(ids);
});
运行代码,会自动跳转到页面
先执行/codefirst初始化数据库
再执行/list,将条件换成全查条件,查看输出是否正确
------------------------------------------------------前后端数据交互------------------------------------------------------
axios的安装与使用:
官网介绍:http://www.axios-js.com/
安装命令:
pnpm install axios
在前端src文件夹下新建一个文件夹http(所有请求都放在这个文件夹里面)
在http下新建文件index.ts
在index.ts中写:
import axios from 'axios'
import UserDto from '../class/UserDto'
//查询
export const getList = (req: UserDto) => {
return axios.post("/api/list",req)
}
//新增
export const add = (req: {}) => {
return axios.post("/api/add",req)
}
//修改
export const edit = (req: {}) => {
return axios.post("/api/edit",req)
}
//删除
export const del = (ids: string) => {
return axios.get("/api/del?ids="+ids)
}
在class文件夹下新建文件:UserDto.ts
export default class UserDto{ //列表分页参数传到后端
keyWord: string = ""
PageIndex: number = 1
PageSize: number = 10
}
修改HomePage.vue
增加:
导入UserDto:import UserDto from '../class/UserDto';
导入getList:import {getList} from '../http';
let userDto = ref<UserDto>(new UserDto())
const load = async()=>{
console.log(await getList(userDto.value))
}
实现页面加载时请求数据
import { ref } from 'vue'改为import { ref,onMounted } from 'vue' //定义关键字
增加:
onMounted(async () => { //在页面加载时执行load方法
await load()
})
运行代码,F12显示跨域问题
---------------------------------------------------------解决跨域问题------------------------------------------------------
浏览器显示CORS问题,原因是前端IP和后端IP不匹配导致,浏览器出于同源策略的安全性校验不允许直接访问资源,则会返回跨域错误
解决:通过代理解决
在vite.config.ts文件中修改server配置:
server:{
open: true, //自动打开浏览器
proxy:{
'/api':{
target:"http://localhost:5173",
changeOrigin:true,
rewrite(path){
return path.replace(/^\/api/,'')
}
}
}
}
在index.ts中修改接口:
http://localhost:5173改为/api
-----------------------------------------------------------列表页数据联调-------------------------------------------------
HomePage.vue增加:
const tableData = ref<any[]>([]) //把默认数据改为读接口数据(定义ref响应式变量,any类型数组)
修改:
const load = async () => {
let data = (await getList(userDto.value)).data //将接口数据放入变量data
tableData.value = data.res //接口数据给到table表格
total.value = data.total //total数据也给到表格
}
实现搜索功能:
修改:
const enterSearch = async () => {
userDto.value.keyWord = SearchVal.value //将搜索框中的值传入userDto.value.KeyWord作为条件
await load() //重新请求
}
实现删除功能:
el-table标签中增加ref="multipleTableRef"
定义:
const multipleTableRef = ref()
导入删除方法:
import {getList} from '../http'改为import {getList,del} from '../http'
修改删除方法:
const onDel = async() => {
let rows = multipleTableRef.value?.getSelectionRows() as Array<User> //获取到复选框选中的数据
if(rows.length > 0){ //如果选中的值大于0
console.log(rows.map(item => {return `'${item.id}'`}).join(","))
//在${item.id}外层加一个单引号是为了序列化成字符串,结果示例:'a','b','c',传到后端放在in查询里面
let res = (await del(rows.map(item => {return `'${item.id}'`}).join(","))).data
if(res){
ElMessage.success("删除成功!")
await load()
}else{
ElMessage.error("删除失败!")
}
}else{
ElMessage.waring("请选中需要删除的行!")
}
}
实现分页效果:
修改currentChange方法:
const currentChange = async (val: number) => {
userDto.value.PageIndex = val
await load()
}
编辑页表单验证:
add.vue中el-form增加:
:model="form" label-width="60px" ref="ruleFormRef" 用于获取表单
导入用于表单验证的类型:
import {FormInstance,FormRules} from 'element-plus'
获取表单对象:
const ruleFormRef = ref<FormInstance>()
验证规则:
const rules = reactive<FormRules>({
date:[{
type: 'date',
required: true,
message: '请选择一个时间',
trigger: 'change',
}],
name:[{
required: true, message: '请输入名称', trigger: 'blur'
}],
address:[{
required: true, message: '请输入地址', trigger: 'blur'
}],
order:[
{required: true, message: '请输入一个序号'},
{type: 'number', message: '该字段必须是数字'}
]
})
修改import{...}from 'vue'中增加reactive
在el-form中增加:
:rules="rules"
在Cancel与Confirm按钮中点击事件增加参数ruleFormRef(表单对象)
导入添加和修改方法:
import {add,edit} from '../http'
增加closeAdd与save方法:
const emits = defineEmits(["closeAdd","success"])
const closeAdd = async (formEl: FormInstance | undefined) => {
if(!formEl) return
formEl.resetFields() //重置表单
emits("closeAdd")
}
const save = async (formEl: FormInstance | undefined) => {
if(!formEl) return
await formEl.validate((valid, fields) => {
if(valid){ //如果表单校验成功,则执行修改或者添加操作
if(form.value.id){ //如果id存在,执行修改方法
edit(form.value).then(function (res){
if(res.data){
emits("success","修改成功!")
}
})
} else { //如果id不存在,执行添加方法
add(form.value).then(function (res){
if(res.data){
emits("success","添加成功!")
}
})
}
}else{
console.log('error submit!', fields)
}
})
}
修改HomePage.vue中success方法:
const success = async (message: string) => {
isShow.value = false
info.value = new User()
ElMessage.success(message)
await load()
}