vue3+te项目正式开发
1.样式初始化
1.1 normalize.css 对项目样式进行初始化
npm i normalize.css import 'normalize.css'(main.js)
1.2 自定义样式文件
./assets/css/index.less 定义自定义的样式
body {
padding: 0;
margin: 0;
}
html, body, #app {
width: 100%;
height: 100%;
}
本项目用到的公共组件-components
所有项目的公共组件-base-ui
2.封装公共模块
2.1 网络请求模块
解决刷新页面vuex中数据丢失的问题
在/store/index.ts 中导出一个函数setupStore,函数中执行各个模块自定义的从localStorage中加载数据的函数,判断当localStorage有值时,则保存到store中,在main.ts中导入setupStore并调用
2.2 utils公共工具模块
2.3 路由模块
2.3.1 导航守卫
通过导航守卫,每次进行路由跳转前,判断如果要跳转去的路由不是登录界面,则去localStorage中读取token,如果token没有值,则跳转去login页面
2.3.2 setupStore() 刷新页面vuex数据不丢失
/store/index.ts 导出setupStore(),从localStorage中读取数据,如果数据不为空,则赋值给store.state,main.ts导入setupStore()并执行,则可以解决页面刷新vuex中数据丢失的问题。
注意:setupStore()需要在use(store)之前执行,否则页面刷新导航会出错
原因:刷新页面时,会重新加载页面执行main.ts,从上往下执行,执行use(store),会执行store中的install()函数,该函数会读取当前url,并从路由配置对象中进行匹配,若当前url对应的路由对象是动态加载的,则动态加载路由的操作是在执行setupStore(),从缓存中读取userMenu后,执行setMenu()之后进行的,因此setupStore()必须先执行,将动态路由加载好之后,在执行use(store)
2.4 vuex模块
在页面中使用useStore()对TS的支持不好,得到的store的类型是any,无法对store中的数据做类型检测,可以在./store.index.ts中自己定义一个useStore(),规定返回值的类型是IRootState&IModuleState,在函数内部返回vuex的useState()
3.登录模块
3.1 将账号登录-获取用户信息-获取用户菜单权限封装在vuex中login模块
3.2 登录成功后的处理流程
3.2.1 用户登录成功后,后端会返回token用于验证用户身份
- 将token保存到store中
- 并且保存到localStorage(避免页面刷新store中的数据丢失)
- 在service封装的网络模块中,在interceptors.request.use的请求拦截中当存在token时,为每一个请求头添加token config.header.authorization = window.localStorage.getItem('token')
3.2.2 根据userId请求用户详细信息
将获取的用户信息保存到vuex和localStorage
3.2.3 根据用户角色信息获取用户菜单权限
将获取的用户菜单保存到vuex和localStorage
3.2.4 跳转到首页
路由根路径redirect->登录界面
4.首页 main/main.vue
4.1 结构设计
- /main/main.vue:放各个组件的首页根组件
- /main/xx/xx: 放首页的各模块组件
- components/nav-nenu/src:侧边导航的组件
- components/nav-header/src:头部的内容
- components/nav-header/index.ts:对nav-header中的组件统一导出
4.2 动态路由
4.2.1 提前写好不同角色对应的路由数组,根据用户角色加载对应的数组
缺点:新增角色或者修改角色对应的权限时,只能修改前端代码
3.2.2 根据菜单动态生成路由映射
- npm i coderwhy --coderwhy老师自己写的库
- coderwhy add3page user -d src/views/main/system/user --自动生成页面和router中的路由映射
- 在src/views/main/system/user下生成user.vue文件,并且router/main/system/user/user.ts中生成映射关系
- /utils/map-menu.ts 定义根据menu生成对应的路由配置对象的函数MenuToRouteMap()并导出
import { RouteRecordRaw } from 'vue-router'
export default function (menu: any[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
// 1.获取所有已经定义过的路由映射对象
const allRoutes: RouteRecordRaw[] = []
const routeFiles = require.context('../router/main', true, /\.ts/)
routeFiles.keys().forEach((item) => {
const route = require(`../router/main${item.split('.')[1]}`)
allRoutes.push(route.default)
})
// 2.根据菜单获取需要添加的routes
// userMenus:
// type === 1 -> children -> type === 1
// type === 2 -> url -> route
const _recurseGetRoute = (menu: any[]) => {
for (const menuItem of menu) {
if (menuItem.type === 2) {
const route = allRoutes.find((routeItem) => {
return routeItem.path === menuItem.url
})
if (route) {
routes.push(route)
}
} else {
_recurseGetRoute(menuItem.children)
}
}
}
_recurseGetRoute(menu)
return routes
}
- /store/login 的setMenus()中,调用MenuToRouteMap(state)获取该用户对应的路由映射对象,通过addRoute()添加到/main的children
setMenus(state, payload: any) {
console.log(payload)
state.userMenus = payload
const route = MenuToRouteMap(payload)
route.forEach((item) => {
router.addRoute('main', item)
})
}
4.3 侧边栏折叠
折叠按钮,icon,用户点击控制是否折叠-》main(控制宽度)->nav-nenu是否折叠()
nav-menu-index.ts 统一导出
在template中使用别名:~@
4.4 刷新页面定位到原来选中的目录
4.4.1 根据路径定位菜单
刷新页面时,定位到刷新之前的菜单
获取当前route->path,用path匹配userMenu中的url,匹配的那个menu的id赋值给default-active
同时可以获取当前页面的breadcrumb
匹配的函数封装在utils/pathMapToMenu.ts
export function pathMapToMenu(
userMenus: any[],
currentPath: string,
breadcrumb?: IBreadcrumb[]
): any {
for (const menu of userMenus) {
if (menu.type === 1) {
const findMenu = pathMapToMenu(menu.children ?? [], currentPath)
if (findMenu) {
breadcrumb?.push({ name: menu.name })
breadcrumb?.push({ name: findMenu.name })
return findMenu
}
} else if (menu.type === 2 && menu.url === currentPath) {
return menu
}
}
4.4.2 /main重定向
根路径/:redirect:/home
/home->home.vue只显示侧边栏导航和头部,内容是由路由匹配到的子组件展示的,但是当用户第一次进入页面,或者在/main刷新页面时,main的内容部分就是空的,这就需要把/main重定向到第一个侧边导航,但是Home的子路由是动态生成的,要怎么实现/main的重定向呢?
setUserMenus()中根据导航生成路由时,保存第一个有路由页面的导航firstMenu,并导出
在路由守卫中判断,如果to='home',则读取firstMenu.url,跳转到该页面
5.搜索框封装
5.1 样式
- 定义IForm,IFormItem类型
- /base-ui/form/src/form中使用props接受IForm,并设置默认值
- /base-ui/types/index.ts 声明所以类型
- /base-ui/form/index.ts 对该组件所有内容进行统一导出,导入/types/index.ts和/src/form,并导出
- 其他组件导入/base-ui/form,根据IForm定义搜索框数据
5.2 数据传递
5.2.1 form-item通过v-model直接修改父组件数据
父组件通过传递一个对象给form组件,form组件通过IFormItem.field属性绑定数据
缺点:子组件修改了父组件传递的数据,违背了单向数据流传递
5.2.2 真正的双向数据绑定
父组件通过v-model=formData传递一个const formData= ref()类型的对象
子组件let data = ref(...formData),子组件中绑定data,再监听data的变化,当变化是,emit('update:formData'),把数据传递给父组件,这种方式子组件并没有直接修改父组件的数据
5.3 form头部和底部的插槽与search-form
在封装的form组件中,提供两个插槽,分别是头部的插槽header(标题)和底部的插槽footer(可以放重置和查询按钮等)
components/page-search:封装一个新的组件,在内部使用form,并将搜索的头部和底部信息传递给form中的插槽,在组件中使用page-search,并把搜索框配置数据searchFormConfig传递给page-search
5.4 搜索与重置
搜索与重置按钮在<page-search>,<user>中使用<page-search>,<page-search>中使用<form>
3个组件中的formData需要同步,思路如下:
- <form>和<page-search>通过v-model=‘formData’双向数据绑定,<form>中定义一个数据接受formData的拷贝,在页面中绑定到formData,再监听formData的变化,当发生变化时,将最新的数据发送到<page-search>
// 如果直接把modelValue绑定到form上,则当用户输入时,会直接修改modelValue的值,违背了单向数据流
const formData = ref({ ...prop.modelValue })
// 当用户输入的数据发送变化,把最新的数据发送到page-search
watch(
formData,
(newValue) => {
emit('update:modelValue', newValue)
},
{ deep: true }
)
当用户点击重置时, <page-search>需要重置formData,并且重新查询用户列表数据
当用户点击查询时,<page-search>需要携带最新的formData数据,并且重新查询用户列表数据
7.封装table
7.1 基础table的封装
基础的table封装在base-ui,多个项目都可以用,接收的数据如下:
- 接受后端返回的原始数据listData
- 表格样式数据contentTableConfig,包括title(表格标题),propList用来控制表格每一项数据的展示,包括label(表头)、prop(展示的数据),min-width,soltName等,showIndexColumn控制序号是否展示,showSelectColumn控制select是否展示
7.2 table中的插槽
7.2.1 表格数据中展示的内容
<template v-for="item in propData" :key="item.prop">
<el-table-column v-bind="item">
<template #default="scope">
<slot :name="item.slotName" :row="scope.row">
{
{ scope.row[item.prop] }}
</slot>
</template>
</el-table-column>
</template>
7.2.2 头部的插槽
7.2.3 底部的插槽
lot,作用域插槽,序号优化,选择,操作,header,分页
7.1各个模块的数据保存到store中,获取各模块数据的方法放到actions
8.全局属性
app.config.globalProperties:定义在这上面的变量在全局任意组件中都可以访问
app.config.perpories.$filters={} :定义全局的filter
$filter.xx:页面中使用
/global/register-properties/中导出配置变量
6.封装面包屑
1.将面包屑封装成一个组件
2.使用时,根据当前path生成数据传入面包屑组件,