Appearance
vue3+ts后台管理项目
第一部分
01:创建项目的过程-选择feature
创建项目:利用vue脚手架
shell
vue create vue3-ts-cms
手动选择特性
路由和状态管理手动集成
选择vue3
不选择class风格的组件(react常用,vue不常用)
使用babel来编译ts,这样可以打补丁,好一点
预处理器使用less,习惯其他的也行
代码检测使用eslint+prettier
保存代码的时候就做eslint
配置文件放到单独的配置文件
不保存预设
02:项目的代码格式editorconfig配置
在项目根目录创建.editorconfig文件,配置编码风格
在vscode中需要安装这个插件配置文件才能生效
03:项目格式化prettier配置
安装prettier
shell
npm install prettier -D
项目根目录创建.prettierrc文件用于配置代码格式化
useTabs:使用tab缩进还是空格缩进,选择false;
tabWidth:tab是空格的情况下,是几个空格,选择2个;
printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
singleQuote:使用单引号还是双引号,选择true,使用单引号;
trailingComma:在多行输入的尾逗号是否添加,设置为 none;
semi:语句末尾是否要加分号,默认值true,选择false表示不加;
根目录创建.prettierignore忽略文件
VSCode需要安装prettier的插件
测试prettier是否生效
- 测试一:在代码中保存代码;
- 测试二:配置一次性修改的命令;
在package.json中配置一个scripts:
04:项目代码检测eslint配置
在前面创建项目的时候,我们就选择了ESLint,所以Vue会默认帮助我们配置需要的ESLint环境。
VSCode需要安装ESLint插件:
解决eslint和prettier冲突的问题:
安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装)
shell
npm i eslint-plugin-prettier eslint-config-prettier -D
添加prettier插件:
05:commit代码符合eslint规范
执行 git commit 命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复
husky是一个git hook工具,可以帮助我们触发git提交的各个阶段:pre-commit、commit-msg、pre-push
shell
npx husky-init && npm install (window不用加&&)
这里会做三件事:
1.安装husky相关的依赖:
2.在项目目录下创建 .husky 文件夹:
3.在package.json中添加一个脚本:
接下来,需要去完成一个操作:在进行commit时,执行lint脚本:
这个时候我们执行git commit的时候会自动对代码进行lint校验。
06:git-commit代码提交规范和限制
代码提交风格
通常git commit会按照统一的风格来提交,这样可以快速定位每次提交的内容,方便之后对版本进行控制。
但是如果每次手动来编写这些是比较麻烦的事情,我们可以使用一个工具:Commitizen
- Commitizen 是一个帮助我们编写规范 commit message 的工具;
1.安装Commitizen
shell
npm install commitizen -D
2.安装cz-conventional-changelog,并且初始化cz-conventional-changelog:
shell
npx commitizen init cz-conventional-changelog --save-dev --save-exact
这个命令会帮助我们安装cz-conventional-changelog:
并且在package.json中进行配置:
这个时候我们提交代码需要使用 npx cz:
- 第一步是选择type,本次更新的类型
Type | 作用 |
---|---|
feat | 新增特性 (feature) |
fix | 修复 Bug(bug fix) |
docs | 修改文档 (documentation) |
style | 代码格式修改(white-space, formatting, missing semi colons, etc) |
refactor | 代码重构(refactor) |
perf | 改善性能(A code change that improves performance) |
test | 测试(when adding missing tests) |
build | 变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等) |
ci | 更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等 |
chore | 变更构建流程或辅助工具(比如更改测试环境) |
revert | 代码回退 |
- 第二步选择本次修改的范围(作用域)
- 第三步选择提交的信息
- 第四步提交详细的描述信息
- 第五步是否是一次重大的更改
- 第六步是否影响某个open issue
我们也可以在scripts中构建一个命令来执行 cz:
代码提交验证
如果我们按照cz来规范了提交风格,但是依然有同事通过 git commit 按照不规范的格式提交应该怎么办呢?
- 我们可以通过commitlint来限制提交;
1.安装 @commitlint/config-conventional 和 @commitlint/cli
shell
npm i @commitlint/config-conventional @commitlint/cli -D
2.在根目录创建commitlint.config.js文件,配置commitlint
shell
module.exports = {
extends: ['@commitlint/config-conventional']
}
3.使用husky生成commit-msg文件,验证提交信息:
shell
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
07:项目的vue.config.js文件
vue.config.js有三种配置方式:
方式一:直接通过CLI提供给我们的选项来配置:
- 比如publicPath:配置应用程序部署的子目录(默认是 /,相当于部署在 https://www.my-app.com/);
- 比如outputDir:修改输出的文件夹;
方式二:通过configureWebpack修改webpack的配置:
- 可以是一个对象,直接会被合并;
- 可以是一个函数,会接收一个config,可以通过config来修改配置;
方式三:通过chainWebpack修改webpack的配置:
- 是一个函数,会接收一个基于 webpack-chain 的config对象,可以对配置进行修改;
在项目根目录创建vue.config.js配置文件
javascript
const path = require('path')
module.exports = {
outputDir: './build',
// configureWebpack: {
// resolve: {
// alias: {
// views: '@/views'
// }
// }
// }
// configureWebpack: (config) => {
// config.resolve.alias = {
// '@': path.resolve(__dirname, 'src'),
// views: '@/views'
// }
// },
chainWebpack: (config) => {
config.resolve.alias.set('@', path.resolve(__dirname, 'src')).set('views', '@/views')
}
}
08:项目的集成-vue-router的集成
安装vue-router的最新版本:
shell
npm install vue-router@next
创建views文件夹,并创建login和main路由组件
创建router文件夹,并创建index.ts文件
在main.js文件中导入路由
app.vue中配置路由跳转
09:项目的集成-vuex的集成
安装vuex
shell
npm install vuex@next
创建store文件夹,并创建index.ts用于创建store,在main.js中安装store,在app.vue中测试
使用$store编辑器找不到变量可能会报错,在scr/shims-vue.d.ts文件中定义$store的类型就不会报错了
第二部分
01:element-plus的全局引入
安装element-plus组件库
shell
npm install element-plus
一种引入element-plus的方式是全局引入,代表的含义是所有的组件和插件都会被自动注册
javascript
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css'
import router from './router'
import store from './store'
createApp(App).use(router).use(store).use(ElementPlus).mount('#app')
02:element-plus的按需引入和封装
按需引入的方式
shell
npm install babel-plugin-import -D
在src目录下新建global文件夹,在里面新建register-element.ts文件和index.ts文件
在main.js导入
更加优雅的写法(插件方式),因为app.use(函数|对象),如果是函数就会调用这个函数并传入app,如果是对象就会调用对象的install方法
03:区分环境变量
三种区分环境变量方式
1.方式一:手动的切换不同的环境(不推荐)
javascript
const BASE_URL = "http://coderwhy.org/dev"
const BASE_NAME = "coderwhy"
const BASE_URL = 'http://coderwhy.org/prod'
const BASE_NAME = 'kobe'
const BASE_URL = 'http://coderwhy.org/test'
const BASE_NAME = 'james'
2.根据process.env.NODE_ENV区分
javascript
// 开发环境:development
// 生产环境:production
// 测试环境:test
let BASE_URL = ""
let BASE_NAME = ""
if (process.env.NODE_ENV === "development") {
BASE_URL = "http://coderwhy.org/dev"
BASE_NAME = "coderwhy"
} else if (process.env.NODE_ENV === "production") {
BASE_URL = "http://coderwhy.org/prod"
BASE_NAME = "kobe"
} else {
BASE_URL = "http://coderwhy.org/test"
BASE_NAME = "james"
}
export { BASE_URL, BASE_NAME }
3.在根目录下创建环境变量文件
打包的时候文件位置找不到可以手动修改,也可以在vue.config.js中配置项目的根路径
第三部分
01:封装axios
安装axios
shell
npm i axios
在src目录中创建service文件夹,在service中创建index.ts出口文件
typescript
/*-----------------------------service/index.ts-------------------*/
// service统一出口(CJ是本人名字的首字母缩写)
import CJRequest from "./request"
import { BASE_URL, TIME_OUT } from "./request/config"
const cjRequest = new CJRequest({
baseURL: BASE_URL,
timeout: TIME_OUT,
interceptors: {
requestInterceptor: (config) => {
// 携带token的拦截
const token = ""
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
console.log("实例单独的请求成功拦截")
return config
},
requestInterceptorCatch: (err) => {
console.log("实例单独的请求失败拦截")
return err
},
responseInterceptor: (res) => {
console.log("实例单独的响应成功拦截")
return res
},
responseInterceptorCatch: (err) => {
console.log("实例单独的响应失败拦截")
return err
}
}
})
export default cjRequest
在service文件夹中创建一个request文件夹,里面新建index.ts文件
typescript
/*-----------------------------service/request/index.ts-------------------*/
import axios from "axios"
import type { AxiosInstance } from "axios"
import { ElLoading } from "element-plus"
import { ILoadingInstance } from "element-plus/lib/el-loading/src/loading.type"
import { CJRequestInterceptors, CJRequestConfig } from "../types"
const DEFAULT_LOADING = true
class CJRequest {
instance: AxiosInstance
interceptors?: CJRequestInterceptors
showLoading: boolean
loading?: ILoadingInstance
constructor(config: CJRequestConfig) {
// 创建axios实例
this.instance = axios.create(config)
// 保存基本信息
this.showLoading = config.showLoading ?? DEFAULT_LOADING
this.interceptors = config.interceptors
// 1.单个实例的拦截器
this.instance.interceptors.request.use(
this.interceptors?.requestInterceptor,
this.interceptors?.requestInterceptorCatch
)
this.instance.interceptors.response.use(
this.interceptors?.responseInterceptor,
this.interceptors?.responseInterceptorCatch
)
// 2.所有的实例都有的拦截器
this.instance.interceptors.request.use(
(config) => {
console.log("所有的实例都有的拦截器:请求成功的拦截")
if (this.showLoading) {
this.loading = ElLoading.service({
lock: true,
text: "正在请求数据...",
background: "rgba(0,0,0,0.5)"
})
}
return config
},
(err) => {
console.log("所有的实例都有的拦截器:请求失败的拦截")
}
)
this.instance.interceptors.response.use(
(res) => {
console.log("所有的实例都有的拦截器:响应成功的拦截")
// 将loading移除
this.loading?.close()
const data = res.data
if (data?.returnCode === "-1001") {
console.log("请求失败~,错误信息")
} else {
return data
}
},
(err) => {
console.log("所有的实例都有的拦截器:响应失败的拦截")
// 将loading移除
this.loading?.close()
// 例子:判断不同的HttpErrorCode显示不同的错误信息
if (err.response.status === 404) {
console.log("404错误")
}
return err
}
)
}
request<T = any>(config: CJRequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
// 1.单个请求对请求config的处理
if (config.interceptors?.requestInterceptor) {
config = config.interceptors.requestInterceptor(config)
}
// 2.判断是否需要显示loading
if (config.showLoading === false) {
this.showLoading = config.showLoading
}
this.instance
.request<any, T>(config)
.then((res) => {
// 1.每个请求单独的拦截器
if (config.interceptors?.responseInterceptor) {
res = config.interceptors.responseInterceptor(res)
}
// 2.将showLoading设置为true,这样不会影响下一个请求
this.showLoading = DEFAULT_LOADING
// 3.将结果resolve返回出去
resolve(res)
})
.catch((err) => {
// 将showLoading设置true,这样不会影响下一个请求
this.showLoading = DEFAULT_LOADING
reject(err)
return err
})
})
}
get<T>(config: CJRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "GET" })
}
post<T>(config: CJRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "POST" })
}
delete<T>(config: CJRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "DELETE" })
}
patch<T>(config: CJRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "PATCH" })
}
}
export default CJRequest
在service/request文件夹中创建types.ts文件和config.ts文件
types.ts用于定义类型
config.ts文件中导出常量(分为开发环境、生产环境和测试环境的常量)
typescript
/*--------------------------service/request/config.ts-----------------------*/
import { AxiosRequestConfig, AxiosResponse } from "axios"
interface CJRequestInterceptors<T = AxiosResponse> {
requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestInterceptorCatch?: (error: any) => any
responseInterceptor?: (res: T) => T
responseInterceptorCatch?: (error: any) => any
}
interface CJRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: CJRequestInterceptors<T>
showLoading?: boolean
}
export { CJRequestInterceptors, CJRequestConfig }
typescript
/*-------------------------------service/types.ts--------------------------------*/
let BASE_URL = ""
const TIME_OUT = 10000
if (process.env.NODE_ENV === "development") {
BASE_URL = "http://123.207.32.32:8000/"
} else if (process.env.NODE_ENV === "production") {
BASE_URL = "http://123.207.32.32:8000/"
} else {
BASE_URL = "http://123.207.32.32:8000/"
}
export { BASE_URL, TIME_OUT }
第四部分
01:项目的tsconfig文件解析
02:login页面搭建
初始化样式操作
安装css初始化样式的库
shell
npm i normalize.css
在main.js引入normalize
在src/assets中创建css文件夹,用于放置全局样式文件,在里面新建base.less文件,编写自己的重置css代码,再新建一个index.less文件,导入base.less文件,然后在main.js中引入index.less
编写src/views/login/login.vue页面
页面搭建
login-panel.vue
在views/login/cpns中创建login-panel.vue(登录面板)组件,在login页面中导入
typescript
/*---------------------------views/login/cpns/login-panel.vue-----------------------*/
<template>
<div class="login-panel">
<h1 class="title">后台管理系统</h1>
<el-tabs type="border-card" stretch v-model="currentTabs">
<el-tab-pane name="account">
<template #label>
<span><i class="el-icon-user-solid"></i> 账号登录</span>
</template>
<login-account ref="accountRef" />
</el-tab-pane>
<el-tab-pane name="phone">
<template #label>
<span><i class="el-icon-mobile-phone"></i> 手机登录</span>
</template>
<login-phone ref="phoneRef" />
</el-tab-pane>
</el-tabs>
<div class="account-control">
<el-checkbox v-model="isKeepPassword">记住密码</el-checkbox>
<el-link type="primary">忘记密码</el-link>
</div>
<el-button type="primary" class="login-btn" @click="handleLoginClick"
>立即登录</el-button
>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue"
import LoginAccount from "./login-account.vue"
import LoginPhone from "./login-phone.vue"
export default defineComponent({
setup() {
// 1.定义属性
const isKeepPassword = ref(true)
const accountRef = ref<InstanceType<typeof LoginAccount>>()
const phoneRef = ref<InstanceType<typeof LoginPhone>>()
const currentTabs = ref("account")
// 2.定义方法
const handleLoginClick = () => {
if (currentTabs.value === "account") {
accountRef.value?.loginAction(isKeepPassword.value)
} else {
console.log("phoneRef调用loginAction")
}
}
return {
isKeepPassword,
handleLoginClick,
accountRef,
currentTabs,
phoneRef
}
},
components: {
LoginAccount,
LoginPhone
}
})
</script>
<style scoped lang="less">
.login-panel {
margin-top: -150px;
width: 320px;
.title {
text-align: center;
}
.account-control {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.login-btn {
width: 100%;
margin-top: 10px;
}
}
</style>
login-account.vue
在views/login/cpns中创建login-account.vue和login-phone.vue文件,在login-panel.vue中导入它们
typescript
/*--------------------------views/login/cpns/login-account.vue-----------------------*/
<template>
<div class="login-account">
<el-form label-width="60px" :rules="rules" :model="account" ref="formRef">
<el-form-item label="账号" prop="name">
<el-input v-model="account.name" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="account.password" show-password />
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from "vue"
import { useStore } from "vuex"
import { ElForm } from "element-plus"
import localCache from "@/utils/cache"
import { rules } from "../config/account-config"
export default defineComponent({
setup() {
const store = useStore()
const account = reactive({
name: localCache.getCache("name") ?? "",
password: localCache.getCache("password") ?? ""
})
const formRef = ref<InstanceType<typeof ElForm>>()
const loginAction = (isKeepPassword: boolean) => {
formRef.value?.validate((valid) => {
if (valid) {
// 1.判断是否需要记住密码
if (isKeepPassword) {
// 本地缓存
localCache.setCache("name", account.name)
localCache.setCache("password", account.password)
} else {
localCache.deleteCache("name")
localCache.deleteCache("password")
}
// 2.开始进行登录逻辑
store.dispatch("login/accountLoginAction", { ...account })
}
})
}
return {
account,
rules,
loginAction,
formRef
}
}
})
</script>
<style scoped></style>
login-phone.vue
typescript
/*--------------------------views/login/cpns/login-phone.vue-----------------------*/
<template>
<div class="login-phone">
<el-form label-width="60px">
<el-form-item label="手机号" prop="num">
<el-input v-model="phone.num" />
</el-form-item>
<el-form-item label="验证码" prop="code">
<div class="get-code">
<el-input v-model="phone.code" />
<el-button type="primary" class="get-btn">获取验证码</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue"
export default defineComponent({
setup() {
const phone = reactive({
num: "",
code: ""
})
return {
phone
}
}
})
</script>
<style scoped lang="less">
.get-code {
display: flex;
.get-btn {
margin-left: 8px;
}
}
</style>
login
typescript
/*------------------------views/login/login.vue----------------------*/
<template>
<div class="login">
<login-panel />
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import LoginPanel from "./cpns/login-panel.vue"
export default defineComponent({
setup() {
return {}
},
components: {
LoginPanel
}
})
</script>
<style scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: url(../../assets/img/login-bg.svg);
}
</style>
element表单规则校验
在login中创建config文件夹,在config文件夹中创建account-config.ts文件
typescript
// 编写好规则
const rules = {
name: [
{
required: true,
message: "用户名是必传内容~",
trigger: "blur"
},
{
pattern: /^[a-z0-9]{5,10}$/,
message: "用户名必须是5~10位字母或者数字~",
trigger: "blur"
}
],
password: [
{
required: true,
message: "密码是必传内容~",
trigger: "blur"
},
{
pattern: /^[a-z0-9]{3,}$/,
message: "密码必须是3位以上的字母或者数字~",
trigger: "blur"
}
]
}
export { rules }
vuex配置
在src/store文件夹中创建login文件夹,存放login页面相关的vuex操作
typescript
/*------------------------store/login/types.ts-------------------------*/
export interface ILoginState {
token: string
userInfo: any
userMenus: any
}
typescript
/*------------------------store/login/index.ts-------------------------*/
import { Module } from "vuex"
import {
accountLoginReuest,
requestUserInfoById,
requestUserMenusByRoleId
} from "@/service/login/login"
import localCache from "@/utils/cache"
import router from "@/router"
import { mapMenusToRoutes } from "@/utils/map-menus"
import { ILoginState } from "./types"
import { IRootState } from "../types"
import { IAccount } from "@/service/login/type"
const loginModule: Module<ILoginState, IRootState> = {
namespaced: true,
state() {
return {
token: "1",
userInfo: {},
userMenus: []
}
},
mutations: {
changeToken(state, token: string) {
state.token = token
},
changeUserInfo(state, userInfo: any) {
state.userInfo = userInfo
},
changeUserMenus(state, userMenus: any) {
state.userMenus = userMenus
// userMenus => routes
const routes = mapMenusToRoutes(userMenus)
// 将routes => router.main.children
routes.forEach((route) => {
router.addRoute("main", route)
})
}
},
getters: {},
actions: {
async accountLoginAction({ commit }, payload: IAccount) {
// 1.实现登录逻辑
const loginResult = await accountLoginReuest(payload)
const { id, token } = loginResult.data
commit("changeToken", token)
localCache.setCache("token", token)
// 2.请求用户信息
const userInfoResult = await requestUserInfoById(id)
const userInfo = userInfoResult.data
commit("changeUserInfo", userInfo)
localCache.setCache("userInfo", userInfo)
// 3.请求用户菜单
const userMenusResult = await requestUserMenusByRoleId(userInfo.role.id)
const userMenus = userMenusResult.data
commit("changeUserMenus", userMenus)
localCache.setCache("userMenus", userMenus)
// 4.跳转到首页
router.push("/main")
},
loadLocalLogin({ commit }) {
const token = localCache.getCache("token")
if (token) {
commit("changeToken", token)
}
const userInfo = localCache.getCache("userInfo")
if (userInfo) {
commit("changeUserInfo", userInfo)
}
const userMenus = localCache.getCache("userMenus")
if (userMenus) {
commit("changeUserMenus", userMenus)
}
}
}
}
export default loginModule
在store/index.ts中进行使用
typescript
/*------------------------store/types.ts--------------------*/
import { ILoginState } from "./login/types"
import { ISystemState } from "./main/system/types"
export interface IRootState {
name: string
age: number
}
export interface IRootAndModule {
login: ILoginState
system: ISystemState
}
export type IStoreType = IRootState & IRootAndModule
typescript
/*------------------------store/index.ts--------------------*/
import { createStore, Store, useStore as useVuexStore } from "vuex"
import login from "./login/login"
import system from "./main/system/system"
import { IRootState, IStoreType } from "./types"
const store = createStore<IRootState>({
state: () => {
return {
name: "zhanchujin",
age: 18
}
},
mutations: {},
getters: {},
actions: {},
modules: {
login,
system
}
})
export function setupStore() {
store.dispatch("login/loadLocalLogin")
}
export function useStore(): Store<IStoreType> {
return useVuexStore()
}
export default store
当页面刷新时,如果是登录状态把数据重新保存到vuex中,因为vuex的数据是保存在内存中的,刷新后需要重新获取保存
service配置
typescript
/*--------------------service/login/type.ts-----------*/
export interface IAccount {
name: string
password: string
}
export interface ILoginResult {
id: number
name: string
token: string
}
typescript
/*--------------------service/login/login.ts-----------*/
import cjRequest from "../index"
import { IAccount, ILoginResult } from "./type"
import { IDataType } from "../types"
enum LoginAPI {
AccountLogin = "/login",
LoginUserInfo = "/users/", // 用法:user/1
UserMenus = "/role/" // 用法:role/1/menu
}
export function accountLoginReuest(account: IAccount) {
return cjRequest.post<IDataType<ILoginResult>>({
url: LoginAPI.AccountLogin,
data: account
})
}
export function requestUserInfoById(id: number) {
return cjRequest.get<IDataType>({
url: LoginAPI.LoginUserInfo + id,
showLoading: false
})
}
export function requestUserMenusByRoleId(id: number) {
return cjRequest.get<IDataType>({
url: LoginAPI.UserMenus + id + "/menu",
showLoading: false
})
}
typescript
/*------------------------src/service/index.ts------------------------*/
// service统一出口(CJ是本人名字的首字母缩写)
import CJRequest from "./request"
import { BASE_URL, TIME_OUT } from "./request/config"
import localCache from "@/utils/cache"
// axios实例
const cjRequest = new CJRequest({
baseURL: BASE_URL,
timeout: TIME_OUT,
interceptors: {
requestInterceptor: (config) => {
// 携带token的拦截
const token = localCache.getCache("token")
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
requestInterceptorCatch: (config) => {
return config
},
responseInterceptor: (response) => {
return response
},
responseInterceptorCatch: (response) => {
return response
}
}
})
export default cjRequest
utils
封装操作操作本地存储(localStorage)的方法
typescript
/*--------------------------------src/utils/cache.ts------------------------------*/
class LocalCache {
setCache(key: string, value: any) {
window.localStorage.setItem(key, JSON.stringify(value))
}
getCache(key: string) {
// obj => string => obj
const value = window.localStorage.getItem(key)
if (value) {
return JSON.parse(value)
}
}
deleteCache(key: string) {
window.localStorage.removeItem(key)
}
clearCache() {
window.localStorage.clear()
}
}
export default new LocalCache()
跨域配置
路由守卫配置
第五部分
01:main页面框架搭建
编写main页面,包括左侧导航菜单和右侧顶部和内容
typescript
/*--------------src/view/main/main.vue-----------*/
<template>
<div class="main">
<el-container class="main-content">
<el-aside :width="isCollapse ? '60px' : '210px'">
<nav-menu :collapse="isCollapse" />
</el-aside>
<el-container class="page">
<el-header class="page-header">
<nav-header @foldChange="handleFoldChange" />
</el-header>
<el-main class="page-content">Main</el-main>
</el-container>
</el-container>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import NavMenu from '@/components/nav-menu'
import NavHeader from '@/components/nav-header'
export default defineComponent({
components: {
NavMenu,
NavHeader
},
setup() {
const isCollapse = ref(false)
const handleFoldChange = (isFold: boolean) => {
isCollapse.value = isFold
}
return {
isCollapse,
handleFoldChange
}
}
})
</script>
<style scoped lang="less">
.main {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.main-content,
.page {
height: 100%;
}
.page-content {
height: calc(100% - 48px);
}
.el-header,
.el-footer {
display: flex;
color: #333;
text-align: center;
align-items: center;
}
.el-header {
height: 48px !important;
}
.el-aside {
overflow-x: hidden;
overflow-y: auto;
line-height: 200px;
text-align: left;
cursor: pointer;
background-color: #001529;
transition: width 0.3s linear;
scrollbar-width: none; /* firefox */
-ms-overflow-style: none; /* IE 10+ */
&::-webkit-scrollbar {
display: none;
}
}
.el-main {
color: #333;
text-align: center;
background-color: #f0f2f5;
}
</style>
把nav-menu和nav-header放到公用组件更合理
typescript
/*---------src/components/nav-header/index.ts------------*/
import NavHeader from './src/nav-header.vue'
export default NavHeader
typescript
/*---------src/components/nav-header/src/nav-header.vue------------*/
<template>
<div class="nav-header">
<i
class="fold-menu"
:class="isFold ? 'el-icon-s-fold' : 'el-icon-s-unfold'"
@click="handleFoldClick"
></i>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
emits: ['foldChange'],
setup(props, { emit }) {
const isFold = ref(false)
const handleFoldClick = () => {
isFold.value = !isFold.value
emit('foldChange', isFold.value)
}
return {
isFold,
handleFoldClick
}
}
})
</script>
<style scoped lang="less">
.nav-header {
.fold-menu {
font-size: 30px;
cursor: pointer;
}
}
</style>
typescript
/*--------------------------src/components/nav-menu/index.ts------------------------------*/
import NavMenu from './src/nav-menu.vue'
export default NavMenu
typescript
/*----------------------src/components/nav-menu/src/nav-menu.vue---------------------*/
<template>
<div class="nav-menu">
<div class="logo">
<img class="img" src="~@/assets/img/logo.svg" alt="logo" />
<span v-if="!collapse" class="title">Vue3+TS</span>
</div>
<el-menu
default-active="2"
class="el-menu-vertical"
:collapse="collapse"
background-color="#0c2135"
text-color="#b7bdc3"
active-text-color="#0a60bd"
>
<template v-for="item in userMenus" :key="item.id">
<!-- 二级菜单 -->
<template v-if="item.type === 1">
<!-- 二级菜单的可以展开的标题 -->
<el-submenu :index="item.id + ''">
<template #title>
<i v-if="item.icon" :class="item.icon"></i>
<span>{{ item.name }}</span>
</template>
<!-- 遍历里面的item -->
<template v-for="subitem in item.children" :key="subitem.id">
<el-menu-item :index="subitem.id + ''">
<i v-if="subitem.icon" :class="subitem.icon"></i>
<span>{{ subitem.name }}</span>
</el-menu-item>
</template>
</el-submenu>
</template>
<!-- 一级菜单 -->
<template v-else-if="item.type === 2">
<el-menu-item :index="item.id + ''">
<i v-if="item.icon" :class="item.icon"></i>
<span>{{ item.name }}</span>
</el-menu-item>
</template>
</template>
</el-menu>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'
// vuex - typescript => pinia
export default defineComponent({
props: {
collapse: {
type: Boolean,
default: false
}
},
setup() {
const store = useStore()
const userMenus = computed(() => store.state.login.userMenus)
return {
userMenus
}
}
})
</script>
<style scoped lang="less">
.nav-menu {
height: 100%;
background-color: #001529;
.logo {
display: flex;
height: 28px;
padding: 12px 10px 8px 10px;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.img {
height: 100%;
margin: 0 10px;
}
.title {
font-size: 16px;
font-weight: 700;
color: white;
}
}
.el-menu {
border-right: none;
}
// 目录
.el-submenu {
background-color: #001529 !important;
// 二级菜单 ( 默认背景 )
.el-menu-item {
padding-left: 50px !important;
background-color: #0c2135 !important;
}
}
::v-deep .el-submenu__title {
background-color: #001529 !important;
}
// hover 高亮
.el-menu-item:hover {
color: #fff !important; // 菜单
}
.el-menu-item.is-active {
color: #fff !important;
background-color: #0a60bd !important;
}
}
.el-menu-vertical:not(.el-menu--collapse) {
width: 100%;
height: calc(100% - 48px);
}
</style>
02:动态路由相关文件创建
动态路由搭建,使用工具快速生成路由文件和页面组件文件
typescript
npm i coderwhy -g
coderwhy add3page user -d src/views/main/system/user
coderwhy add3page role -d src/views/main/system/role
coderwhy add3page department -d src/views/main/system/department
coderwhy add3page menu -d src/views/main/system/menu
coderwhy add3page categary -d src/views/main/product/category
coderwhy add3page goods -d src/views/main/product/goods
coderwhy add3page chat -d src/views/main/story/chat
coderwhy add3page list -d src/views/main/story/list
coderwhy add3page overview -d src/views/main/analysis/overview
coderwhy add3page dashboard -d src/views/main/analysis/dashboard
..........
第六部分
01:动态路由配置
封装方法map-menus
typescript
/*--------------src/utils/map-menus.ts---------------*/
import { RouteRecordRaw } from 'vue-router'
export function mapMenusToRoutes(userMenus: any[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
// 1.先去加载默认所有的routes
const allRoutes: RouteRecordRaw[] = []
const routeFiles = require.context('../router/main', true, /\.ts/)
routeFiles.keys().forEach((key) => {
const route = require('../router/main' + key.split('.')[1])
allRoutes.push(route.default)
})
// 2.根据菜单获取需要添加的routes
// userMenus:
// type === 1 -> children -> type === 1
// type === 2 -> url -> route
const _recurseGetRoute = (menus: any[]) => {
for (const menu of menus) {
if (menu.type === 2) {
const route = allRoutes.find((route) => route.path === menu.url)
if (route) routes.push(route)
} else {
_recurseGetRoute(menu.children)
}
}
}
_recurseGetRoute(userMenus)
return routes
}
添加路由
typescript
/*------------------src/store/login/login.ts-------------------*/
import { Module } from 'vuex'
import {
accountLoginRequest,
requestUserInfoById,
requestUserMenusByRoleId
} from '@/service/login/login'
import localCache from '@/utils/cache'
import { mapMenusToRoutes } from '@/utils/map-menus'
import router from '@/router'
import { IAccount } from '@/service/login/type'
import { ILoginState } from './types'
import { IRootState } from '../types'
const loginModule: Module<ILoginState, IRootState> = {
namespaced: true,
state() {
return {
token: '',
userInfo: {},
userMenus: []
}
},
getters: {},
mutations: {
changeToken(state, token: string) {
state.token = token
},
changeUserInfo(state, userInfo: any) {
state.userInfo = userInfo
},
changeUserMenus(state, userMenus: any) {
state.userMenus = userMenus
console.log('注册动态路由')
// userMenus => routes
const routes = mapMenusToRoutes(userMenus)
// 将routes => router.main.children
routes.forEach((route) => {
router.addRoute('main', route)
})
}
},
actions: {
async accountLoginAction({ commit }, payload: IAccount) {
// 1.实现登录逻辑
const loginResult = await accountLoginRequest(payload)
const { id, token } = loginResult.data
commit('changeToken', token)
localCache.setCache('token', token)
// 2.请求用户信息
const userInfoResult = await requestUserInfoById(id)
const userInfo = userInfoResult.data
commit('changeUserInfo', userInfo)
localCache.setCache('userInfo', userInfo)
// 3.请求用户菜单
const userMenusResult = await requestUserMenusByRoleId(userInfo.role.id)
const userMenus = userMenusResult.data
commit('changeUserMenus', userMenus)
localCache.setCache('userMenus', userMenus)
// 4.跳到首页
router.push('/main')
},
loadLocalLogin({ commit }) {
const token = localCache.getCache('token')
if (token) {
commit('changeToken', token)
}
const userInfo = localCache.getCache('userInfo')
if (userInfo) {
commit('changeUserInfo', userInfo)
}
const userMenus = localCache.getCache('userMenus')
if (userMenus) {
commit('changeUserMenus', userMenus)
}
}
}
}
export default loginModule
菜单添加路由跳转
typescript
/*--------------------src/components/nav-menu/src/nav-menu.vue----------------*/
<template>
<div class="nav-menu">
<div class="logo">
<img class="img" src="~@/assets/img/logo.svg" alt="logo" />
<span v-if="!collapse" class="title">Vue3+TS</span>
</div>
<el-menu
default-active="2"
class="el-menu-vertical"
:collapse="collapse"
background-color="#0c2135"
text-color="#b7bdc3"
active-text-color="#0a60bd"
>
<template v-for="item in userMenus" :key="item.id">
<!-- 二级菜单 -->
<template v-if="item.type === 1">
<!-- 二级菜单的可以展开的标题 -->
<el-submenu :index="item.id + ''">
<template #title>
<i v-if="item.icon" :class="item.icon"></i>
<span>{{ item.name }}</span>
</template>
<!-- 遍历里面的item -->
<template v-for="subitem in item.children" :key="subitem.id">
<el-menu-item
:index="subitem.id + ''"
@click="handleMenuItemClick(subitem)"
>
<i v-if="subitem.icon" :class="subitem.icon"></i>
<span>{{ subitem.name }}</span>
</el-menu-item>
</template>
</el-submenu>
</template>
<!-- 一级菜单 -->
<template v-else-if="item.type === 2">
<el-menu-item :index="item.id + ''">
<i v-if="item.icon" :class="item.icon"></i>
<span>{{ item.name }}</span>
</el-menu-item>
</template>
</template>
</el-menu>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'
import { useRouter } from 'vue-router'
// vuex - typescript => pinia
export default defineComponent({
props: {
collapse: {
type: Boolean,
default: false
}
},
setup() {
const store = useStore()
const userMenus = computed(() => store.state.login.userMenus)
const router = useRouter()
const handleMenuItemClick = (item: any) => {
console.log('--------')
router.push({
path: item.url ?? '/not-found'
})
}
return {
userMenus,
handleMenuItemClick
}
}
})
</script>
<style scoped lang="less">
.nav-menu {
height: 100%;
background-color: #001529;
.logo {
display: flex;
height: 28px;
padding: 12px 10px 8px 10px;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.img {
height: 100%;
margin: 0 10px;
}
.title {
font-size: 16px;
font-weight: 700;
color: white;
}
}
.el-menu {
border-right: none;
}
// 目录
.el-submenu {
background-color: #001529 !important;
// 二级菜单 ( 默认背景 )
.el-menu-item {
padding-left: 50px !important;
background-color: #0c2135 !important;
}
}
::v-deep .el-submenu__title {
background-color: #001529 !important;
}
// hover 高亮
.el-menu-item:hover {
color: #fff !important; // 菜单
}
.el-menu-item.is-active {
color: #fff !important;
background-color: #0a60bd !important;
}
}
.el-menu-vertical:not(.el-menu--collapse) {
width: 100%;
height: calc(100% - 48px);
}
</style>
02:主页内容from表单组件封装
表单组件放在base-ui文件夹下,其他项目可以使用
typescript
/*------------------src/base-ui/from/index.ts------------------*/
import HyForm from './src/form.vue'
export * from './types'
export default HyForm
typescript
/*-----------------------src/base-ui/from/types/index.ts-----------------------------*/
type IFormType = 'input' | 'password' | 'select' | 'datepicker'
export interface IFormItem {
type: IFormType
label: string
rules?: any[]
placeholder?: any
// 针对select
options?: any[]
// 针对特殊的属性
otherOptions?: any
}
export interface IForm {
formItems: IFormItem[]
labelWidth?: string
colLayout: any
itemLayout: any
}
typescript
/*----------------------src/base-ui/from/src/from.vue-----------------------------*/
<template>
<div class="hy-form">
<el-form :label-width="labelWidth">
<el-row>
<template v-for="item in formItems" :key="item.label">
<el-col v-bind="colLayout">
<el-form-item
:label="item.label"
:rules="item.rules"
:style="itemStyle"
>
<template
v-if="item.type === 'input' || item.type === 'password'"
>
<el-input
:placeholder="item.placeholder"
v-bind="item.otherOptions"
:show-password="item.type === 'password'"
/>
</template>
<template v-else-if="item.type === 'select'">
<el-select
:placeholder="item.placeholder"
v-bind="item.otherOptions"
style="width: 100%"
>
<el-option
v-for="option in item.options"
:key="option.value"
:value="option.value"
>{{ option.title }}</el-option
>
</el-select>
</template>
<template v-else-if="item.type === 'datepicker'">
<el-date-picker
style="width: 100%"
v-bind="item.otherOptions"
></el-date-picker>
</template>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { IFormItem } from '../types'
export default defineComponent({
props: {
formItems: {
type: Array as PropType<IFormItem[]>,
default: () => []
},
labelWidth: {
type: String,
default: '100px'
},
itemStyle: {
type: Object,
default: () => ({ padding: '10px 40px' })
},
colLayout: {
type: Object,
default: () => ({
xl: 6, // >1920px 4个
lg: 8,
md: 12,
sm: 24,
xs: 24
})
}
},
setup() {
return {}
}
})
</script>
<style scoped lang="less">
.hy-form {
padding-top: 22px;
}
</style>
在user路由组件中导入from组件
typescript
/*------------src/view/main/system/user/config/search.config.ts---------*/
import { IForm } from '@/base-ui/form'
export const searchFormConfig: IForm = {
labelWidth: '120px',
itemLayout: {
padding: '10px 40px'
},
colLayout: {
span: 8
},
formItems: [
{
type: 'input',
label: 'id',
placeholder: '请输入id'
},
{
type: 'input',
label: '用户名',
placeholder: '请输入用户名'
},
{
type: 'password',
label: '密码',
placeholder: '请输入密码'
},
{
type: 'select',
label: '喜欢的运动',
placeholder: '请选择喜欢的运动',
options: [
{ title: '篮球', value: 'basketball' },
{ title: '足球', value: 'football' }
]
},
{
type: 'datepicker',
label: '创建时间',
otherOptions: {
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
type: 'daterange'
}
}
]
}
typescript
/*---------------------src/view/main/system/user/user.vue--------------------*/
<template>
<div class="user">
<hy-form v-bind="searchFormConfig" />
<div class="content"></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import HyForm from '@/base-ui/form'
import { searchFormConfig } from './config/search.config'
export default defineComponent({
name: 'user',
components: {
HyForm
},
setup() {
return {
searchFormConfig
}
}
})
</script>
<style scoped></style>
第七部分
01:刷新页面时当前路径对应的菜单高亮
1.拿到路径
2.根据路径去匹配menu
3.拿到menu.id作为defaultValue
4.到访问main时,跳转到第一个菜单
typescript
/*-------------src/components/nav-menu/src/nav-menu.vue---------------------/*/
<template>
<div class="nav-menu">
<div class="logo">
<img class="img" src="~@/assets/img/logo.svg" alt="logo" />
<span v-if="!collapse" class="title">Vue3+TS</span>
</div>
<el-menu
:default-active="defaultValue"
class="el-menu-vertical"
:collapse="collapse"
background-color="#0c2135"
text-color="#b7bdc3"
active-text-color="#0a60bd"
>
<template v-for="item in userMenus" :key="item.id">
<!-- 二级菜单 -->
<template v-if="item.type === 1">
<!-- 二级菜单的可以展开的标题 -->
<el-submenu :index="item.id + ''">
<template #title>
<i v-if="item.icon" :class="item.icon"></i>
<span>{{ item.name }}</span>
</template>
<!-- 遍历里面的item -->
<template v-for="subitem in item.children" :key="subitem.id">
<el-menu-item
:index="subitem.id + ''"
@click="handleMenuItemClick(subitem)"
>
<i v-if="subitem.icon" :class="subitem.icon"></i>
<span>{{ subitem.name }}</span>
</el-menu-item>
</template>
</el-submenu>
</template>
<!-- 一级菜单 -->
<template v-else-if="item.type === 2">
<el-menu-item :index="item.id + ''">
<i v-if="item.icon" :class="item.icon"></i>
<span>{{ item.name }}</span>
</el-menu-item>
</template>
</template>
</el-menu>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from 'vue'
import { useStore } from '@/store'
import { useRouter, useRoute } from 'vue-router'
import { pathMapToMenu } from '@/utils/map-menus'
// vuex - typescript => pinia
export default defineComponent({
props: {
collapse: {
type: Boolean,
default: false
}
},
setup() {
// store
const store = useStore()
const userMenus = computed(() => store.state.login.userMenus)
// router
const router = useRouter()
const route = useRoute()
const currentPath = route.path
// data
const menu = pathMapToMenu(userMenus.value, currentPath)
const defaultValue = ref(menu.id + '')
// event handle
const handleMenuItemClick = (item: any) => {
console.log('--------')
router.push({
path: item.url ?? '/not-found'
})
}
return {
userMenus,
defaultValue,
handleMenuItemClick
}
}
})
</script>
<style scoped lang="less">
.nav-menu {
height: 100%;
background-color: #001529;
.logo {
display: flex;
height: 28px;
padding: 12px 10px 8px 10px;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.img {
height: 100%;
margin: 0 10px;
}
.title {
font-size: 16px;
font-weight: 700;
color: white;
}
}
.el-menu {
border-right: none;
}
// 目录
.el-submenu {
background-color: #001529 !important;
// 二级菜单 ( 默认背景 )
.el-menu-item {
padding-left: 50px !important;
background-color: #0c2135 !important;
}
}
::v-deep .el-submenu__title {
background-color: #001529 !important;
}
// hover 高亮
.el-menu-item:hover {
color: #fff !important; // 菜单
}
.el-menu-item.is-active {
color: #fff !important;
background-color: #0a60bd !important;
}
}
.el-menu-vertical:not(.el-menu--collapse) {
width: 100%;
height: calc(100% - 48px);
}
</style>
typescript
/*----------------src/utils/map-menus.ts----------------------*/
import { IBreadcrumb } from '@/base-ui/breadcrumb'
import { RouteRecordRaw } from 'vue-router'
let firstMenu: any = null
export function mapMenusToRoutes(userMenus: any[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
// 1.先去加载默认所有的routes
const allRoutes: RouteRecordRaw[] = []
const routeFiles = require.context('../router/main', true, /\.ts/)
routeFiles.keys().forEach((key) => {
const route = require('../router/main' + key.split('.')[1])
allRoutes.push(route.default)
})
// 2.根据菜单获取需要添加的routes
// userMenus:
// type === 1 -> children -> type === 1
// type === 2 -> url -> route
const _recurseGetRoute = (menus: any[]) => {
for (const menu of menus) {
if (menu.type === 2) {
const route = allRoutes.find((route) => route.path === menu.url)
if (route) routes.push(route)
if (!firstMenu) {
firstMenu = menu
}
} else {
_recurseGetRoute(menu.children)
}
}
}
_recurseGetRoute(userMenus)
return routes
}
export function pathMapBreadcrumbs(userMenus: any[], currentPath: string) {
const breadcrumbs: IBreadcrumb[] = []
pathMapToMenu(userMenus, currentPath, breadcrumbs)
return breadcrumbs
}
// /main/system/role -> type === 2 对应menu
export function pathMapToMenu(userMenus: any[], currentPath: string): any {
for (const menu of userMenus) {
if (menu.type === 1) {
const findMenu = pathMapToMenu(menu.children ?? [], currentPath)
if (findMenu) {
return findMenu
}
} else if (menu.type === 2 && menu.url === currentPath) {
return menu
}
}
}
export { firstMenu }
typescript
/*---------------------utils/src/route/index.ts---------------------*/
import { createRouter, createWebHashHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import localCache from '@/utils/cache'
import { firstMenu } from '@/utils/map-menus'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/main'
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/login.vue')
},
{
path: '/main',
name: 'main',
component: () => import('@/views/main/main.vue')
// children: [] -> 根据userMenus来决定 -> children
},
{
path: '/:pathMatch(.*)*',
name: 'notFound',
component: () => import('@/views/not-found/not-found.vue')
}
]
const router = createRouter({
routes,
history: createWebHashHistory()
})
// 导航守卫
router.beforeEach((to) => {
if (to.path !== '/login') {
const token = localCache.getCache('token')
if (!token) {
return '/login'
}
}
// console.log(router.getRoutes())
// console.log(to) // route对象
if (to.path === '/main') {
return firstMenu.url
}
})
export default router
02:面包屑封装
typescript
/*------------src/base-ui/breadcrumb/index.ts--------------*/
import CjBreadcrumb from "./src/breadcrumb.vue"
export * from "./types"
export default CjBreadcrumb
typescript
/*------------src/base-ui/breadcrumb/types/index.ts--------------*/
export interface IBreadcrumb {
name: string
path?: string
}
typescript
/*------------src/base-ui/breadcrumb/src/breadcrumb.vue--------------*/
<template>
<div class="cj-breadcrumb">
<el-breadcrumb separator="/">
<template v-for="item in breadcrubms" :key="item.name">
<el-breadcrumb-item :to="{ path: item.path }">{{
item.name
}}</el-breadcrumb-item>
</template>
</el-breadcrumb>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue"
import { IBreadcrumb } from "../types"
export default defineComponent({
props: {
breadcrubms: {
type: Array as PropType<IBreadcrumb[]>,
default: () => []
}
},
setup() {
return {}
}
})
</script>
<style scoped></style>
面包屑配置
typescript
/*------------src/utils/map-menus.ts--------------*/
import { IBreadcrumb } from '@/base-ui/breadcrumb'
import { RouteRecordRaw } from 'vue-router'
let firstMenu: any = null
export function mapMenusToRoutes(userMenus: any[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
// 1.先去加载默认所有的routes
const allRoutes: RouteRecordRaw[] = []
const routeFiles = require.context('../router/main', true, /\.ts/)
routeFiles.keys().forEach((key) => {
const route = require('../router/main' + key.split('.')[1])
allRoutes.push(route.default)
})
// 2.根据菜单获取需要添加的routes
// userMenus:
// type === 1 -> children -> type === 1
// type === 2 -> url -> route
const _recurseGetRoute = (menus: any[]) => {
for (const menu of menus) {
if (menu.type === 2) {
const route = allRoutes.find((route) => route.path === menu.url)
if (route) routes.push(route)
if (!firstMenu) {
firstMenu = menu
}
} else {
_recurseGetRoute(menu.children)
}
}
}
_recurseGetRoute(userMenus)
return routes
}
export function pathMapBreadcrumbs(userMenus: any[], currentPath: string) {
const breadcrumbs: IBreadcrumb[] = []
pathMapToMenu(userMenus, currentPath, breadcrumbs)
return breadcrumbs
}
// /main/system/role -> type === 2 对应menu
export function pathMapToMenu(
userMenus: any[],
currentPath: string,
breadcrumbs?: IBreadcrumb[]
): any {
for (const menu of userMenus) {
if (menu.type === 1) {
const findMenu = pathMapToMenu(menu.children ?? [], currentPath)
if (findMenu) {
breadcrumbs?.push({ name: menu.name })
breadcrumbs?.push({ name: findMenu.name })
return findMenu
}
} else if (menu.type === 2 && menu.url === currentPath) {
return menu
}
}
}
// export function pathMapBreadcrumbs(userMenus: any[], currentPath: string) {
// const breadcrumbs: IBreadcrumb[] = []
// for (const menu of userMenus) {
// if (menu.type === 1) {
// const findMenu = pathMapToMenu(menu.children ?? [], currentPath)
// if (findMenu) {
// breadcrumbs.push({ name: menu.name, path: menu.url })
// breadcrumbs.push({ name: findMenu.name, path: findMenu.url })
// return findMenu
// }
// } else if (menu.type === 2 && menu.url === currentPath) {
// return menu
// }
// }
// return breadcrumbs
// }
// // /main/system/role -> type === 2 对应menu
// export function pathMapToMenu(userMenus: any[], currentPath: string): any {
// for (const menu of userMenus) {
// if (menu.type === 1) {
// const findMenu = pathMapToMenu(menu.children ?? [], currentPath)
// if (findMenu) {
// return findMenu
// }
// } else if (menu.type === 2 && menu.url === currentPath) {
// return menu
// }
// }
// }
export { firstMenu }
页面中导入
typescript
/*----------------src/components/nav-header/src/nav-header.vue---------------*/
<template>
<div class="nav-header">
<i
class="fold-menu"
:class="isFold ? 'el-icon-s-fold' : 'el-icon-s-unfold'"
@click="handleFoldClick"
></i>
<div class="content">
<hy-breadcrumb :breadcrumbs="breadcrumbs" />
<user-info />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
import UserInfo from './user-info.vue'
import HyBreadcrumb, { IBreadcrumb } from '@/base-ui/breadcrumb'
import { useStore } from '@/store'
import { useRoute } from 'vue-router'
import { pathMapBreadcrumbs } from '@/utils/map-menus'
export default defineComponent({
components: {
UserInfo,
HyBreadcrumb
},
emits: ['foldChange'],
setup(props, { emit }) {
const isFold = ref(false)
const handleFoldClick = () => {
isFold.value = !isFold.value
emit('foldChange', isFold.value)
}
// 面包屑的数据: [{name: , path: }]
const store = useStore()
const breadcrumbs = computed(() => {
const userMenus = store.state.login.userMenus
const route = useRoute()
const currentPath = route.path
return pathMapBreadcrumbs(userMenus, currentPath)
})
return {
isFold,
handleFoldClick,
breadcrumbs
}
}
})
</script>
<style scoped lang="less">
.nav-header {
display: flex;
width: 100%;
.fold-menu {
font-size: 30px;
cursor: pointer;
}
.content {
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
padding: 0 20px;
}
}
</style>
03:form表单双向数据绑定,插槽search封装
添加field字段
typescript
/*----------src/base-ui/form/types/index.ts-----------------*/
type IFormType = 'input' | 'password' | 'select' | 'datepicker'
export interface IFormItem {
field: string
type: IFormType
label: string
rules?: any[]
placeholder?: any
// 针对select
options?: any[]
// 针对特殊的属性
otherOptions?: any
}
export interface IForm {
formItems: IFormItem[]
labelWidth?: string
colLayout: any
itemLayout: any
}
typescript
/*----------src/views/main/system/user/config/search-config.ts-----------------*/
import { IForm } from '@/base-ui/form'
export const searchFormConfig: IForm = {
labelWidth: '120px',
itemLayout: {
padding: '10px 40px'
},
colLayout: {
span: 8
},
formItems: [
{
field: 'id',
type: 'input',
label: 'id',
placeholder: '请输入id'
},
{
field: 'name',
type: 'input',
label: '用户名',
placeholder: '请输入用户名'
},
{
field: 'password',
type: 'password',
label: '密码',
placeholder: '请输入密码'
},
{
field: 'sport',
type: 'select',
label: '喜欢的运动',
placeholder: '请选择喜欢的运动',
options: [
{ title: '篮球', value: 'basketball' },
{ title: '足球', value: 'football' }
]
},
{
field: 'createTime',
type: 'datepicker',
label: '创建时间',
otherOptions: {
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
type: 'daterange'
}
}
]
}
typescript
page-search封装
typescript
/*--------src/components/page-search/index.ts-----------*/
import PageSearch from './src/page-search.vue'
export default PageSearch
typescript
/*--------src/components/page-search/src/page-search.vue-----------*/
<template>
<div class="page-search">
<cj-form v-bind="searchFormConfig" v-model="formData">
<template #header>
<h1 class="header">高级检索</h1>
</template>
<template #footer>
<div class="handle-btns">
<el-button icon="el-icon-refresh">重置</el-button>
<el-button type="primary" icon="el-icon-search">搜索</el-button>
</div>
</template>
</cj-form>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import HyForm from '@/base-ui/form'
export default defineComponent({
props: {
searchFormConfig: {
type: Object,
reuqired: true
}
},
components: {
HyForm
},
setup() {
const formData = ref({
id: '',
name: '',
password: '',
sport: '',
createTime: ''
})
return {
formData
}
}
})
</script>
<style scoped>
.header {
color: red;
}
.handle-btns {
text-align: right;
padding: 0 50px 20px 0;
}
</style>
定义插槽,实现双向数据绑定
typescript
<template>
<div class="cj-form">
<div class="header">
<slot name="header"></slot>
</div>
<el-form :label-width="labelWidth">
<el-row>
<template v-for="item in formItems" :key="item.label">
<el-col v-bind="colLayout">
<el-form-item
:label="item.label"
:rules="item.rules"
:style="itemStyle"
>
<template
v-if="item.type === 'input' || item.type === 'password'"
>
<el-input
:placeholder="item.placeholder"
v-bind="item.otherOptions"
:show-password="item.type === 'password'"
v-model="formData[`${item.field}`]"
/>
</template>
<template v-else-if="item.type === 'select'">
<el-select
:placeholder="item.placeholder"
v-bind="item.otherOptions"
style="width: 100%"
v-model="formData[`${item.field}`]"
>
<el-option
v-for="option in item.options"
:key="option.value"
:value="option.value"
>{{ option.title }}</el-option
>
</el-select>
</template>
<template v-else-if="item.type === 'datepicker'">
<el-date-picker
style="width: 100%"
v-bind="item.otherOptions"
v-model="formData[`${item.field}`]"
></el-date-picker>
</template>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from 'vue'
import { IFormItem } from '../types'
export default defineComponent({
props: {
modelValue: {
type: Object,
required: true
},
formItems: {
type: Array as PropType<IFormItem[]>,
default: () => []
},
labelWidth: {
type: String,
default: '100px'
},
itemStyle: {
type: Object,
default: () => ({ padding: '10px 40px' })
},
colLayout: {
type: Object,
default: () => ({
xl: 6, // >1920px 4个
lg: 8,
md: 12,
sm: 24,
xs: 24
})
}
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const formData = ref({ ...props.modelValue })
watch(
formData,
(newValue) => {
console.log(newValue)
emit('update:modelValue', newValue)
},
{
deep: true
}
)
return {
formData
}
}
})
</script>
<style scoped lang="less">
.cj-form {
padding-top: 22px;
}
</style>
user组件页面中使用page-search组件
typescript
/*--------src/views/main/system/user/src/user.vue-------*/
<template>
<div class="user">
<page-search :searchFormConfig="searchFormConfig" />
<div class="content">
<hy-table :listData="userList" :propList="propList">
<template #status="scope">
<el-button>{{ scope.row.enable ? '启用' : '禁用' }}</el-button>
</template>
<template #createAt="scope">
<strong>{{ scope.row.createAt }}</strong>
</template>
</hy-table>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'
import PageSearch from '@/components/page-search'
import HyTable from '@/base-ui/table'
import { searchFormConfig } from './config/search.config'
export default defineComponent({
name: 'user',
components: {
PageSearch,
HyTable
},
setup() {
const store = useStore()
store.dispatch('system/getPageListAction', {
pageUrl: '/users/list',
queryInfo: {
offset: 0,
size: 10
}
})
const userList = computed(() => store.state.system.userList)
const userCount = computed(() => store.state.system.userCount)
const propList = [
{ prop: 'name', label: '用户名', minWidth: '100' },
{ prop: 'realname', label: '真实姓名', minWidth: '100' },
{ prop: 'cellphone', label: '手机号码', minWidth: '100' },
{ prop: 'enable', label: '状态', minWidth: '100', slotName: 'status' },
{
prop: 'createAt',
label: '创建时间',
minWidth: '250',
slotName: 'createAt'
},
{
prop: 'updateAt',
label: '更新时间',
minWidth: '250',
slotName: 'updateAt'
}
]
return {
searchFormConfig,
userList,
propList
}
}
})
</script>
<style scoped>
.content {
padding: 20px;
border-top: 20px solid #f5f5f5;
}
</style>
04:获取用户列表数据
typescript
/*---------src/store/main/system/types.ts----------*/
export interface ISystemState {
userList: any[]
userCount: number
}
typescript
/*----------src/store/main/system/system.ts---------*/
import { Module } from 'vuex'
import { IRootState } from '@/store/types'
import { ISystemState } from './types'
import { getPageListData } from '@/service/main/system/system'
const systemModule: Module<ISystemState, IRootState> = {
namespaced: true,
state() {
return {
userList: [],
userCount: 0
}
},
mutations: {
changeUserList(state, userList: any[]) {
state.userList = userList
},
changeUserCount(state, userCount: number) {
state.userCount = userCount
}
},
actions: {
async getPageListAction({ commit }, payload: any) {
console.log(payload.pageUrl)
console.log(payload.queryInfo)
// 1.对页面发送请求
const pageResult = await getPageListData(
payload.pageUrl,
payload.queryInfo
)
const { list, totalCount } = pageResult.data
commit('changeUserList', list)
commit('changeUserCount', totalCount)
}
}
}
export default systemModule
在store/index.ts中导入system模块
typescript
/*-----------src/store/types.ts-----------------*/
import { ILoginState } from './login/types'
import { ISystemState } from './main/system/types'
export interface IRootState {
name: string
age: number
}
export interface IRootWithModule {
login: ILoginState
system: ISystemState
}
export type IStoreType = IRootState & IRootWithModule
typescript
/*-------------------------src/store/index.ts---------------------------*/
import { createStore, Store, useStore as useVuexStore } from 'vuex'
import login from './login/login'
import system from './main/system/system'
import { IRootState, IStoreType } from './types'
const store = createStore<IRootState>({
state() {
return {
name: 'coderwhy',
age: 18
}
},
mutations: {},
getters: {},
actions: {},
modules: {
login,
system
}
})
export function setupStore() {
store.dispatch('login/loadLocalLogin')
}
export function useStore(): Store<IStoreType> {
return useVuexStore()
}
export default store
service
typescript
export interface IDataType<T = any> {
code: number
data: T
}
typescript
/*----------------src/service/main/system/system.ts------------*/
import cjRequest from '../../index'
import { IDataType } from '../../types'
export function getPageListData(url: string, queryInfo: any) {
return cjRequest.post<IDataType>({
url: url,
data: queryInfo
})
}
在user页面组件中请求数据 代码参考上方
...
table封装
typescript
/*----------------------src/base-ui/tabel/index.ts---------------*/
import HyTable from './src/table.vue'
export default HyTable
typescript
/*----------------------src/base-ui/tabel/src/tabel.vue---------------*/
<template>
<div class="cj-table">
<el-table :data="listData" border style="width: 100%">
<template v-for="propItem in propList" :key="propItem.prop">
<el-table-column v-bind="propItem" align="center">
<template #default="scope">
<slot :name="propItem.slotName" :row="scope.row">
{{ scope.row[propItem.prop] }}
</slot>
</template>
</el-table-column>
</template>
</cj-table>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
listData: {
type: Array,
required: true
},
propList: {
type: Array,
required: true
}
},
setup() {
return {}
}
})
</script>
<style scoped></style>
第八部分
01:时间格式化工具封装
可以格式化时间的方法封装到全局app上
typescript
/*---------src/main.ts--------------*/
import { createApp } from "vue"
import router from "./router"
import "normalize.css"
import "./assets/css/index.less"
import store from "./store"
import { setupStore } from "./store"
import App from "./App.vue"
import { globalRegister } from "./global"
const app = createApp(App)
setupStore()
app.use(globalRegister)
app.use(router)
app.use(store)
app.mount("#app")
typescript
/*---------src/global/index.ts--------------*/
import { App } from 'vue'
import registerElement from './register-element'
import registerProperties from './register-properties'
export function globalRegister(app: App): void {
app.use(registerElement)
app.use(registerProperties)
}
typescript
/*---------src/global/register-properties.ts-------------*/
import { App } from 'vue'
import { formatUtcString } from '@/utils/date-format'
export default function registerProperties(app: App) {
app.config.globalProperties.$filters = {
foo() {
console.log('foo')
},
formatTime(value: string) {
return formatUtcString(value)
}
}
}
typescript
/*------------------utils/date-format.ts----------------*/
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
export function formatUtcString(
utcString: string,
format: string = DATE_TIME_FORMAT
) {
return dayjs.utc(utcString).format(format)
}
export function formatTimestamp(
timestamp: number,
format: string = DATE_TIME_FORMAT
) {
return ''
}
02:tabel完善三个插槽和page-content封装
typescript
/*-------------------src/base-ui/table/src/table.vue-------------------*/
<template>
<div class="cj-table">
<div class="header">
<slot name="header">
<div class="title">{{ title }}</div>
<div class="handler">
<slot name="headerHandler"></slot>
</div>
</slot>
</div>
<el-table
:data="listData"
border
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column
v-if="showSelectColumn"
type="selection"
align="center"
width="60"
></el-table-column>
<el-table-column
v-if="showIndexColumn"
type="index"
label="序号"
align="center"
width="80"
></el-table-column>
<template v-for="propItem in propList" :key="propItem.prop">
<el-table-column v-bind="propItem" align="center">
<template #default="scope">
<slot :name="propItem.slotName" :row="scope.row">
{{ scope.row[propItem.prop] }}
</slot>
</template>
</el-table-column>
</template>
</el-table>
<div class="footer">
<slot name="footer">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage4"
:page-sizes="[100, 200, 300, 400]"
:page-size="100"
layout="total, sizes, prev, pager, next, jumper"
:total="400"
>
</el-pagination>
</slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
title: {
type: String,
default: ''
},
listData: {
type: Array,
required: true
},
propList: {
type: Array,
required: true
},
showIndexColumn: {
type: Boolean,
default: false
},
showSelectColumn: {
type: Boolean,
default: false
}
},
emits: ['selectionChange'],
setup(props, { emit }) {
const handleSelectionChange = (value: any) => {
emit('selectionChange', value)
}
return {
handleSelectionChange
}
}
})
</script>
<style scoped lang="less">
.header {
display: flex;
height: 45px;
padding: 0 5px;
justify-content: space-between;
align-items: center;
.title {
font-size: 20px;
font-weight: 700;
}
.handler {
align-items: center;
}
}
.footer {
margin-top: 15px;
.el-pagination {
text-align: right;
}
}
</style>
typescript
/*---------------------src/views/main/system/user/user.vue---------------------*/
<template>
<div class="user">
<page-search :searchFormConfig="searchFormConfig" />
<page-content
:contentTableConfig="contentTableConfig"
pageName="users"
></page-content>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import PageSearch from '@/components/page-search'
import PageContent from '@/components/page-content'
import { searchFormConfig } from './config/search.config'
import { contentTableConfig } from './config/content.config'
export default defineComponent({
name: 'users',
components: {
PageSearch,
PageContent
},
setup() {
return {
searchFormConfig,
contentTableConfig
}
}
})
</script>
<style scoped></style>
typescript
/*-----------src/views/main/system/user/config/content.config.ts----------------*/
export const contentTableConfig = {
title: '用户列表',
propList: [
{ prop: 'name', label: '用户名', minWidth: '100' },
{ prop: 'realname', label: '真实姓名', minWidth: '100' },
{ prop: 'cellphone', label: '手机号码', minWidth: '100' },
{ prop: 'enable', label: '状态', minWidth: '100', slotName: 'status' },
{
prop: 'createAt',
label: '创建时间',
minWidth: '250',
slotName: 'createAt'
},
{
prop: 'updateAt',
label: '更新时间',
minWidth: '250',
slotName: 'updateAt'
},
{ label: '操作', minWidth: '120', slotName: 'handler' }
],
showIndexColumn: true,
showSelectColumn: true
}
typescript
/*-----------src/components/page-content/index.ts--------------------*/
import PageContent from './src/page-content.vue'
export default PageContent
typescript
/*-----------src/components/page-content/src/page-content.ts----------*/
<template>
<div class="page-content">
<hy-table :listData="dataList" v-bind="contentTableConfig">
<!-- 1.header中的插槽 -->
<template #headerHandler>
<el-button type="primary" size="medium">新建用户</el-button>
</template>
<!-- 2.列中的插槽 -->
<template #status="scope">
<el-button
plain
size="mini"
:type="scope.row.enable ? 'success' : 'danger'"
>
{{ scope.row.enable ? '启用' : '禁用' }}
</el-button>
</template>
<template #createAt="scope">
<span>{{ $filters.formatTime(scope.row.createAt) }}</span>
</template>
<template #updateAt="scope">
<span>{{ $filters.formatTime(scope.row.updateAt) }}</span>
</template>
<template #handler>
<div class="handle-btns">
<el-button icon="el-icon-edit" size="mini" type="text"
>编辑</el-button
>
<el-button icon="el-icon-delete" size="mini" type="text"
>删除</el-button
>
</div>
</template>
</hy-table>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'
import HyTable from '@/base-ui/table'
export default defineComponent({
components: {
HyTable
},
props: {
contentTableConfig: {
type: Object,
require: true
},
pageName: {
type: String,
required: true
}
},
setup(props) {
const store = useStore()
store.dispatch('system/getPageListAction', {
pageName: props.pageName,
queryInfo: {
offset: 0,
size: 10
}
})
const dataList = computed(() =>
store.getters[`system/pageListData`](props.pageName)
)
// const userCount = computed(() => store.state.system.userCount)
return {
dataList
}
}
})
</script>
<style scoped>
.page-content {
padding: 20px;
border-top: 20px solid #f5f5f5;
}
</style>
03:封装通用的请求
typescript
/*-------src/store/main/system/types.ts-----*/
export interface ISystemState {
usersList: any[]
usersCount: number
roleList: any[]
roleCount: number
}
typescript
/*-----------src/store/main/system/system.ts------------------------*/
import { Module } from 'vuex'
import { IRootState } from '@/store/types'
import { ISystemState } from './types'
import { getPageListData } from '@/service/main/system/system'
const systemModule: Module<ISystemState, IRootState> = {
namespaced: true,
state() {
return {
usersList: [],
usersCount: 0,
roleList: [],
roleCount: 0
}
},
mutations: {
changeUsersList(state, userList: any[]) {
state.usersList = userList
},
changeUsersCount(state, userCount: number) {
state.usersCount = userCount
},
changeRoleList(state, list: any[]) {
state.roleList = list
},
changeRoleCount(state, count: number) {
state.roleCount = count
}
},
getters: {
pageListData(state) {
return (pageName: string) => {
return (state as any)[`${pageName}List`]
// switch (pageName) {
// case 'users':
// return state.usersList
// case 'role':
// return state.roleList
// }
}
}
},
actions: {
async getPageListAction({ commit }, payload: any) {
// 1.获取pageUrl
const pageName = payload.pageName
const pageUrl = `/${pageName}/list`
// switch (pageName) {
// case 'users':
// pageUrl = '/users/list'
// break
// case 'role':
// pageUrl = '/role/list'
// break
// }
// 2.对页面发送请求
const pageResult = await getPageListData(pageUrl, payload.queryInfo)
// 3.将数据存储到state中
const { list, totalCount } = pageResult.data
const changePageName =
pageName.slice(0, 1).toUpperCase() + pageName.slice(1)
commit(`change${changePageName}List`, list)
commit(`change${changePageName}Count`, totalCount)
// switch (pageName) {
// case 'users':
// commit(`changeUserList`, list)
// commit(`changeUserCount`, totalCount)
// break
// case 'role':
// commit(`changeRoleList`, list)
// commit(`changeRoleCount`, totalCount)
// break
// }
}
}
}
export default systemModule
角色页面搭建
typescript
/*-------------src/views/main/system/role/config/content.config.ts-------------*/
export const contentTableConfig = {
title: '用户列表',
propList: [
{ prop: 'name', label: '角色名', minWidth: '100' },
{ prop: 'intro', label: '权限介绍', minWidth: '100' },
{
prop: 'createAt',
label: '创建时间',
minWidth: '250',
slotName: 'createAt'
},
{
prop: 'updateAt',
label: '更新时间',
minWidth: '250',
slotName: 'updateAt'
},
{ label: '操作', minWidth: '120', slotName: 'handler' }
],
showIndexColumn: true,
showSelectColumn: true
}
typescript
/*-------------src/views/main/system/role/config/search.config.ts-------------*/
import { IForm } from '@/base-ui/form'
export const searchFormConfig: IForm = {
labelWidth: '120px',
formItems: [
{
field: 'name',
type: 'input',
label: '角色名称',
placeholder: '请输入角色名称'
},
{
field: 'intro',
type: 'input',
label: '权限介绍',
placeholder: '请输入权限介绍'
},
{
field: 'createTime',
type: 'datepicker',
label: '创建时间',
otherOptions: {
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
type: 'daterange'
}
}
]
}
typescript
/*-------------src/views/main/system/role/role.vue-------------*/
<template>
<div class="role">
<page-search :searchFormConfig="searchFormConfig"></page-search>
<page-content
:contentTableConfig="contentTableConfig"
pageName="role"
></page-content>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import PageSearch from '@/components/page-search'
import PageContent from '@/components/page-content'
import { searchFormConfig } from './config/search.config'
import { contentTableConfig } from './config/content.config'
export default defineComponent({
name: 'role',
components: {
PageContent,
PageSearch
},
setup() {
return {
searchFormConfig,
contentTableConfig
}
}
})
</script>
<style scoped></style>
第九部分
01:重置和搜索功能
page-search.vue
typescript
/*------src/components/page-search/src/page-search.vue-----*/
<template>
<div class="page-search">
<hy-form v-bind="searchFormConfig" v-model="formData">
<template #header>
<h1 class="header">高级检索</h1>
</template>
<template #footer>
<div class="handle-btns">
<el-button icon="el-icon-refresh" @click="handleResetClick"
>重置</el-button
>
<el-button
type="primary"
icon="el-icon-search"
@click="handleQueryClick"
>搜索</el-button
>
</div>
</template>
</hy-form>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import HyForm from '@/base-ui/form'
export default defineComponent({
props: {
searchFormConfig: {
type: Object,
reuqired: true
}
},
components: {
HyForm
},
emits: ['resetBtnClick', 'queryBtnClick'],
setup(props, { emit }) {
// 双向绑定的属性应该是由配置文件的field来决定
// 1.优化一: formData中的属性应该动态来决定
const formItems = props.searchFormConfig?.formItems ?? []
const formOriginData: any = {}
for (const item of formItems) {
formOriginData[item.field] = ''
}
const formData = ref(formOriginData)
// 2.优化二: 当用户点击重置
const handleResetClick = () => {
// for (const key in formOriginData) {
// formData.value[`${key}`] = formOriginData[key]
// }
formData.value = formOriginData
emit('resetBtnClick')
}
// 3.优化三: 当用户点击搜索
const handleQueryClick = () => {
emit('queryBtnClick', formData.value)
}
return {
formData,
handleResetClick,
handleQueryClick
}
}
})
</script>
<style scoped>
.header {
color: red;
}
.handle-btns {
text-align: right;
padding: 0 50px 20px 0;
}
</style>
form.vue
typescript
/*-----------src/base-ui/form/src/form.vue----------*/
<template>
<div class="hy-form">
<div class="header">
<slot name="header"></slot>
</div>
<el-form :label-width="labelWidth">
<el-row>
<template v-for="item in formItems" :key="item.label">
<el-col v-bind="colLayout">
<el-form-item
:label="item.label"
:rules="item.rules"
:style="itemStyle"
>
<template
v-if="item.type === 'input' || item.type === 'password'"
>
<el-input
:placeholder="item.placeholder"
v-bind="item.otherOptions"
:show-password="item.type === 'password'"
:model-value="modelValue[`${item.field}`]"
@update:modelValue="handleValueChange($event, item.field)"
/>
</template>
<template v-else-if="item.type === 'select'">
<el-select
:placeholder="item.placeholder"
v-bind="item.otherOptions"
style="width: 100%"
:model-value="modelValue[`${item.field}`]"
@update:modelValue="handleValueChange($event, item.field)"
>
<el-option
v-for="option in item.options"
:key="option.value"
:value="option.value"
>{{ option.title }}</el-option
>
</el-select>
</template>
<template v-else-if="item.type === 'datepicker'">
<el-date-picker
style="width: 100%"
v-bind="item.otherOptions"
:model-value="modelValue[`${item.field}`]"
@update:modelValue="handleValueChange($event, item.field)"
></el-date-picker>
</template>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { IFormItem } from '../types'
export default defineComponent({
props: {
modelValue: {
type: Object,
required: true
},
formItems: {
type: Array as PropType<IFormItem[]>,
default: () => []
},
labelWidth: {
type: String,
default: '100px'
},
itemStyle: {
type: Object,
default: () => ({ padding: '10px 40px' })
},
colLayout: {
type: Object,
default: () => ({
xl: 6, // >1920px 4个
lg: 8,
md: 12,
sm: 24,
xs: 24
})
}
},
emits: ['update:modelValue'],
setup(props, { emit }) {
// const formData = ref({ ...props.modelValue })
// watch(
// formData,
// (newValue) => {
// console.log(newValue)
// emit('update:modelValue', newValue)
// },
// {
// deep: true
// }
// )
const handleValueChange = (value: any, field: string) => {
emit('update:modelValue', { ...props.modelValue, [field]: value })
}
return {
handleValueChange
}
}
})
</script>
<style scoped lang="less">
.hy-form {
padding-top: 22px;
}
</style>
user.vue
typescript
/*-----------src/views/system/user/user.vue----------*/
<template>
<div class="user">
<page-search
:searchFormConfig="searchFormConfig"
@resetBtnClick="handleResetClick"
@queryBtnClick="handleQueryClick"
/>
<page-content
ref="pageContentRef"
:contentTableConfig="contentTableConfig"
pageName="users"
></page-content>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import PageSearch from '@/components/page-search'
import PageContent from '@/components/page-content'
import { searchFormConfig } from './config/search.config'
import { contentTableConfig } from './config/content.config'
import { usePageSearch } from '@/hooks/use-page-search'
export default defineComponent({
name: 'users',
components: {
PageSearch,
PageContent
},
setup() {
const [pageContentRef, handleResetClick, handleQueryClick] = usePageSearch()
return {
searchFormConfig,
contentTableConfig,
pageContentRef,
handleResetClick,
handleQueryClick
}
}
})
</script>
<style scoped></style>
use-page-search.ts
typescript
/*-----src/hook/use-page-search.ts-----*/
import { ref } from 'vue'
import PageContent from '@/components/page-content'
export function usePageSearch() {
const pageContentRef = ref<InstanceType<typeof PageContent>>()
const handleResetClick = () => {
pageContentRef.value?.getPageData()
}
const handleQueryClick = (queryInfo: any) => {
pageContentRef.value?.getPageData(queryInfo)
}
return [pageContentRef, handleResetClick, handleQueryClick]
}
pageconent.vue
typescript
/*----------src/components/page-content/src/pageconent.vue-----*/
<template>
<div class="page-content">
<cj-table
:listData="dataList"
:listCount="dataCount"
v-bind="contentTableConfig"
v-model:page="pageInfo"
>
<!-- 1.header中的插槽 -->
<template #headerHandler>
<el-button v-if="isCreate" type="primary" size="medium"
>新建用户</el-button
>
</template>
<!-- 2.列中的插槽 -->
<template #status="scope">
<el-button
plain
size="mini"
:type="scope.row.enable ? 'success' : 'danger'"
>
{{ scope.row.enable ? '启用' : '禁用' }}
</el-button>
</template>
<template #createAt="scope">
<span>{{ $filters.formatTime(scope.row.createAt) }}</span>
</template>
<template #updateAt="scope">
<span>{{ $filters.formatTime(scope.row.updateAt) }}</span>
</template>
<template #handler>
<div class="handle-btns">
<el-button v-if="isUpdate" icon="el-icon-edit" size="mini" type="text"
>编辑</el-button
>
<el-button
v-if="isDelete"
icon="el-icon-delete"
size="mini"
type="text"
>删除</el-button
>
</div>
</template>
<!-- 在page-content中动态插入剩余的插槽 -->
<template
v-for="item in otherPropSlots"
:key="item.prop"
#[item.slotName]="scope"
>
<template v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</template>
</cj-table>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, watch } from 'vue'
import { useStore } from '@/store'
import { usePermission } from '@/hooks/use-permission'
import CjTable from '@/base-ui/table'
export default defineComponent({
components: {
CjTable
},
props: {
contentTableConfig: {
type: Object,
require: true
},
pageName: {
type: String,
required: true
}
},
setup(props) {
const store = useStore()
// 0.获取操作的权限
const isCreate = usePermission(props.pageName, 'create')
const isUpdate = usePermission(props.pageName, 'update')
const isDelete = usePermission(props.pageName, 'delete')
const isQuery = usePermission(props.pageName, 'query')
// 1.双向绑定pageInfo
const pageInfo = ref({ currentPage: 0, pageSize: 10 })
watch(pageInfo, () => getPageData())
// 2.发送网络请求
const getPageData = (queryInfo: any = {}) => {
if (!isQuery) return
store.dispatch('system/getPageListAction', {
pageName: props.pageName,
queryInfo: {
offset: pageInfo.value.currentPage * pageInfo.value.pageSize,
size: pageInfo.value.pageSize,
...queryInfo
}
})
}
getPageData()
// 3.从vuex中获取数据
const dataList = computed(() =>
store.getters[`system/pageListData`](props.pageName)
)
const dataCount = computed(() =>
store.getters[`system/pageListCount`](props.pageName)
)
// 4.获取其他的动态插槽名称
const otherPropSlots = props.contentTableConfig?.propList.filter(
(item: any) => {
if (item.slotName === 'status') return false
if (item.slotName === 'createAt') return false
if (item.slotName === 'updateAt') return false
if (item.slotName === 'handler') return false
return true
}
)
return {
dataList,
getPageData,
dataCount,
pageInfo,
otherPropSlots,
isCreate,
isUpdate,
isDelete
}
}
})
</script>
<style scoped>
.page-content {
padding: 20px;
border-top: 20px solid #f5f5f5;
}
</style>
02:分页实现
tabel.vue
typescript
/*------------src/base-ui/table/src/tabel.vue-----------*/
<template>
<div class="cj-table">
<div class="header">
<slot name="header">
<div class="title">{{ title }}</div>
<div class="handler">
<slot name="headerHandler"></slot>
</div>
</slot>
</div>
<el-table
:data="listData"
border
style="width: 100%"
@selection-change="handleSelectionChange"
v-bind="childrenProps"
>
<el-table-column
v-if="showSelectColumn"
type="selection"
align="center"
width="60"
></el-table-column>
<el-table-column
v-if="showIndexColumn"
type="index"
label="序号"
align="center"
width="80"
></el-table-column>
<template v-for="propItem in propList" :key="propItem.prop">
<el-table-column v-bind="propItem" align="center" show-overflow-tooltip>
<template #default="scope">
<slot :name="propItem.slotName" :row="scope.row">
{{ scope.row[propItem.prop] }}
</slot>
</template>
</el-table-column>
</template>
</el-table>
<div class="footer" v-if="showFooter">
<slot name="footer">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="page.currentPage"
:page-size="page.pageSize"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
:total="listCount"
>
</el-pagination>
</slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
title: {
type: String,
default: ''
},
listData: {
type: Array,
required: true
},
listCount: {
type: Number,
default: 0
},
propList: {
type: Array,
required: true
},
showIndexColumn: {
type: Boolean,
default: false
},
showSelectColumn: {
type: Boolean,
default: false
},
page: {
type: Object,
default: () => ({ currentPage: 0, pageSize: 10 })
},
childrenProps: {
type: Object,
default: () => ({})
},
showFooter: {
type: Boolean,
default: true
}
},
emits: ['selectionChange', 'update:page'],
setup(props, { emit }) {
const handleSelectionChange = (value: any) => {
emit('selectionChange', value)
}
const handleCurrentChange = (currentPage: number) => {
emit('update:page', { ...props.page, currentPage })
}
const handleSizeChange = (pageSize: number) => {
emit('update:page', { ...props.page, pageSize })
}
return {
handleSelectionChange,
handleCurrentChange,
handleSizeChange
}
}
})
</script>
<style scoped lang="less">
.header {
display: flex;
height: 45px;
padding: 0 5px;
justify-content: space-between;
align-items: center;
.title {
font-size: 20px;
font-weight: 700;
}
.handler {
align-items: center;
}
}
.footer {
margin-top: 15px;
.el-pagination {
text-align: right;
}
}
</style>
page-content.vue
typescript
/*-------------src/components/page-content/src/page-content.vue---------------------*/
<template>
<div class="page-content">
<hy-table
:listData="dataList"
:listCount="dataCount"
v-bind="contentTableConfig"
v-model:page="pageInfo"
>
<!-- 1.header中的插槽 -->
<template #headerHandler>
<el-button v-if="isCreate" type="primary" size="medium"
>新建用户</el-button
>
</template>
<!-- 2.列中的插槽 -->
<template #status="scope">
<el-button
plain
size="mini"
:type="scope.row.enable ? 'success' : 'danger'"
>
{{ scope.row.enable ? '启用' : '禁用' }}
</el-button>
</template>
<template #createAt="scope">
<span>{{ $filters.formatTime(scope.row.createAt) }}</span>
</template>
<template #updateAt="scope">
<span>{{ $filters.formatTime(scope.row.updateAt) }}</span>
</template>
<template #handler>
<div class="handle-btns">
<el-button v-if="isUpdate" icon="el-icon-edit" size="mini" type="text"
>编辑</el-button
>
<el-button
v-if="isDelete"
icon="el-icon-delete"
size="mini"
type="text"
>删除</el-button
>
</div>
</template>
<!-- 在page-content中动态插入剩余的插槽 -->
<template
v-for="item in otherPropSlots"
:key="item.prop"
#[item.slotName]="scope"
>
<template v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</template>
</hy-table>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, watch } from 'vue'
import { useStore } from '@/store'
import { usePermission } from '@/hooks/use-permission'
import HyTable from '@/base-ui/table'
export default defineComponent({
components: {
HyTable
},
props: {
contentTableConfig: {
type: Object,
require: true
},
pageName: {
type: String,
required: true
}
},
setup(props) {
const store = useStore()
// 0.获取操作的权限
const isCreate = usePermission(props.pageName, 'create')
const isUpdate = usePermission(props.pageName, 'update')
const isDelete = usePermission(props.pageName, 'delete')
const isQuery = usePermission(props.pageName, 'query')
// 1.双向绑定pageInfo
const pageInfo = ref({ currentPage: 0, pageSize: 10 })
watch(pageInfo, () => getPageData())
// 2.发送网络请求
const getPageData = (queryInfo: any = {}) => {
if (!isQuery) return
store.dispatch('system/getPageListAction', {
pageName: props.pageName,
queryInfo: {
offset: pageInfo.value.currentPage * pageInfo.value.pageSize,
size: pageInfo.value.pageSize,
...queryInfo
}
})
}
getPageData()
// 3.从vuex中获取数据
const dataList = computed(() =>
store.getters[`system/pageListData`](props.pageName)
)
const dataCount = computed(() =>
store.getters[`system/pageListCount`](props.pageName)
)
// 4.获取其他的动态插槽名称
const otherPropSlots = props.contentTableConfig?.propList.filter(
(item: any) => {
if (item.slotName === 'status') return false
if (item.slotName === 'createAt') return false
if (item.slotName === 'updateAt') return false
if (item.slotName === 'handler') return false
return true
}
)
return {
dataList,
getPageData,
dataCount,
pageInfo,
otherPropSlots,
isCreate,
isUpdate,
isDelete
}
}
})
</script>
<style scoped>
.page-content {
padding: 20px;
border-top: 20px solid #f5f5f5;
}
</style>
03:商品信息页面搭建
商品信息页面搭建 (store代码略)
typescript
/*----------src/views/main/product/goods/config/content.config.ts----------*/
export const contentTableConfig = {
title: '商品列表',
propList: [
{ prop: 'name', label: '商品名称', minWidth: '80' },
{ prop: 'oldPrice', label: '原价格', minWidth: '80', slotName: 'oldPrice' },
{ prop: 'newPrice', label: '现价格', minWidth: '80' },
{ prop: 'imgUrl', label: '商品图片', minWidth: '100', slotName: 'image' },
{ prop: 'status', label: '状态', minWidth: '100', slotName: 'status' },
{
prop: 'createAt',
label: '创建时间',
minWidth: '250',
slotName: 'createAt'
},
{
prop: 'updateAt',
label: '更新时间',
minWidth: '250',
slotName: 'updateAt'
},
{ label: '操作', minWidth: '120', slotName: 'handler' }
],
showIndexColumn: true,
showSelectColumn: true
}
typescript
/*-----------------src/views/main/product/goods/goods.vue------------*/
<template>
<div class="goods">
<page-content :contentTableConfig="contentTableConfig" pageName="goods">
<template #image="scope">
<el-image
style="width: 60px; height: 60px"
:src="scope.row.imgUrl"
:preview-src-list="[scope.row.imgUrl]"
>
</el-image>
</template>
<template #oldPrice="scope">{{ '¥' + scope.row.oldPrice }}</template>
</page-content>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import PageContent from '@/components/page-content'
import { contentTableConfig } from './config/content.config'
export default defineComponent({
name: 'goods',
components: {
PageContent
},
setup() {
return {
contentTableConfig
}
}
})
</script>
<style scoped></style>
04:菜单管理页面搭建
typescript
/*-------src/views/main/system/menu/menu.vue------------*/
<template>
<div class="menu">
<page-content
:contentTableConfig="contentTableConfig"
pageName="menu"
></page-content>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import PageContent from '@/components/page-content'
import { contentTableConfig } from './config/content.config'
export default defineComponent({
name: 'hy-menu',
components: {
PageContent
},
setup() {
return {
contentTableConfig
}
}
})
</script>
<style scoped></style>
typescript
/*-------src/views/main/system/menu/config/content.config.ts------------*/
export const contentTableConfig = {
title: '菜单列表',
propList: [
{ prop: 'name', label: '菜单名称', minWidth: '100' },
{ prop: 'type', label: '类型', minWidth: '60' },
{ prop: 'url', label: '菜单url', minWidth: '100' },
{ prop: 'icon', label: '菜单icon', minWidth: '100' },
{ prop: 'permission', label: '按钮权限', minWidth: '100' },
{
prop: 'createAt',
label: '创建时间',
minWidth: '220',
slotName: 'createAt'
},
{
prop: 'updateAt',
label: '更新时间',
minWidth: '220',
slotName: 'updateAt'
},
{ label: '操作', minWidth: '120', slotName: 'handler' }
],
showIndexColumn: false,
showSelectColumn: false,
childrenProps: {
rowKey: 'id',
treeProp: {
children: 'children'
}
},
showFooter: false
}
typescript
/*-------------src/utils/map-menu.ts----------------*/
import { IBreadcrumb } from '@/base-ui/breadcrumb'
import { RouteRecordRaw } from 'vue-router'
let firstMenu: any = null
export function mapMenusToRoutes(userMenus: any[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
// 1.先去加载默认所有的routes
const allRoutes: RouteRecordRaw[] = []
const routeFiles = require.context('../router/main', true, /\.ts/)
routeFiles.keys().forEach((key) => {
const route = require('../router/main' + key.split('.')[1])
allRoutes.push(route.default)
})
// 2.根据菜单获取需要添加的routes
// userMenus:
// type === 1 -> children -> type === 1
// type === 2 -> url -> route
const _recurseGetRoute = (menus: any[]) => {
for (const menu of menus) {
if (menu.type === 2) {
const route = allRoutes.find((route) => route.path === menu.url)
if (route) routes.push(route)
if (!firstMenu) {
firstMenu = menu
}
} else {
_recurseGetRoute(menu.children)
}
}
}
_recurseGetRoute(userMenus)
return routes
}
export function pathMapBreadcrumbs(userMenus: any[], currentPath: string) {
const breadcrumbs: IBreadcrumb[] = []
pathMapToMenu(userMenus, currentPath, breadcrumbs)
return breadcrumbs
}
// /main/system/role -> type === 2 对应menu
export function pathMapToMenu(
userMenus: any[],
currentPath: string,
breadcrumbs?: IBreadcrumb[]
): any {
for (const menu of userMenus) {
if (menu.type === 1) {
const findMenu = pathMapToMenu(menu.children ?? [], currentPath)
if (findMenu) {
breadcrumbs?.push({ name: menu.name })
breadcrumbs?.push({ name: findMenu.name })
return findMenu
}
} else if (menu.type === 2 && menu.url === currentPath) {
return menu
}
}
}
// export function pathMapBreadcrumbs(userMenus: any[], currentPath: string) {
// const breadcrumbs: IBreadcrumb[] = []
// for (const menu of userMenus) {
// if (menu.type === 1) {
// const findMenu = pathMapToMenu(menu.children ?? [], currentPath)
// if (findMenu) {
// breadcrumbs.push({ name: menu.name, path: menu.url })
// breadcrumbs.push({ name: findMenu.name, path: findMenu.url })
// return findMenu
// }
// } else if (menu.type === 2 && menu.url === currentPath) {
// return menu
// }
// }
// return breadcrumbs
// }
// // /main/system/role -> type === 2 对应menu
// export function pathMapToMenu(userMenus: any[], currentPath: string): any {
// for (const menu of userMenus) {
// if (menu.type === 1) {
// const findMenu = pathMapToMenu(menu.children ?? [], currentPath)
// if (findMenu) {
// return findMenu
// }
// } else if (menu.type === 2 && menu.url === currentPath) {
// return menu
// }
// }
// }
export function mapMenusToPermissions(userMenus: any[]) {
const permissions: string[] = []
const _recurseGetPermission = (menus: any[]) => {
for (const menu of menus) {
if (menu.type === 1 || menu.type === 2) {
_recurseGetPermission(menu.children ?? [])
} else if (menu.type === 3) {
permissions.push(menu.permission)
}
}
}
_recurseGetPermission(userMenus)
return permissions
}
export { firstMenu }
typescript
/*-----------src/hooks/use-permission.ts-----------*/
import { useStore } from '@/store'
export function usePermission(pageName: string, hanldeName: string) {
const store = useStore()
const permissions = store.state.login.permissions
const verifyPermission = `system:${pageName}:${hanldeName}`
// name = "coderwhy"
// !name -> false
// !!name -> true
return !!permissions.find((item) => item === verifyPermission)
}
第十部分
01:删除
typescript
/*--------src/components/page-content/src/page-content.vue---------*/
<template>
<div class="page-content">
<hy-table
:listData="dataList"
:listCount="dataCount"
v-bind="contentTableConfig"
v-model:page="pageInfo"
>
<!-- 1.header中的插槽 -->
<template #headerHandler>
<el-button
v-if="isCreate"
type="primary"
size="medium"
@click="handleNewClick"
>
新建用户
</el-button>
</template>
<!-- 2.列中的插槽 -->
<template #status="scope">
<el-button
plain
size="mini"
:type="scope.row.enable ? 'success' : 'danger'"
>
{{ scope.row.enable ? '启用' : '禁用' }}
</el-button>
</template>
<template #createAt="scope">
<span>{{ $filters.formatTime(scope.row.createAt) }}</span>
</template>
<template #updateAt="scope">
<span>{{ $filters.formatTime(scope.row.updateAt) }}</span>
</template>
<template #handler="scope">
<div class="handle-btns">
<el-button
v-if="isUpdate"
icon="el-icon-edit"
size="mini"
type="text"
@click="handleEditClick(scope.row)"
>
编辑
</el-button>
<el-button
v-if="isDelete"
icon="el-icon-delete"
size="mini"
type="text"
@click="handleDeleteClick(scope.row)"
>删除</el-button
>
</div>
</template>
<!-- 在page-content中动态插入剩余的插槽 -->
<template
v-for="item in otherPropSlots"
:key="item.prop"
#[item.slotName]="scope"
>
<template v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</template>
</hy-table>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, watch } from 'vue'
import { useStore } from '@/store'
import { usePermission } from '@/hooks/use-permission'
import HyTable from '@/base-ui/table'
export default defineComponent({
components: {
HyTable
},
props: {
contentTableConfig: {
type: Object,
require: true
},
pageName: {
type: String,
required: true
}
},
emits: ['newBtnClick', 'editBtnClick'],
setup(props, { emit }) {
const store = useStore()
// 0.获取操作的权限
const isCreate = usePermission(props.pageName, 'create')
const isUpdate = usePermission(props.pageName, 'update')
const isDelete = usePermission(props.pageName, 'delete')
const isQuery = usePermission(props.pageName, 'query')
// 1.双向绑定pageInfo
const pageInfo = ref({ currentPage: 1, pageSize: 10 })
watch(pageInfo, () => getPageData())
// 2.发送网络请求
const getPageData = (queryInfo: any = {}) => {
if (!isQuery) return
store.dispatch('system/getPageListAction', {
pageName: props.pageName,
queryInfo: {
offset: (pageInfo.value.currentPage - 1) * pageInfo.value.pageSize,
size: pageInfo.value.pageSize,
...queryInfo
}
})
}
getPageData()
// 3.从vuex中获取数据
const dataList = computed(() =>
store.getters[`system/pageListData`](props.pageName)
)
const dataCount = computed(() =>
store.getters[`system/pageListCount`](props.pageName)
)
// 4.获取其他的动态插槽名称
const otherPropSlots = props.contentTableConfig?.propList.filter(
(item: any) => {
if (item.slotName === 'status') return false
if (item.slotName === 'createAt') return false
if (item.slotName === 'updateAt') return false
if (item.slotName === 'handler') return false
return true
}
)
// 5.删除/编辑/新建操作
const handleDeleteClick = (item: any) => {
console.log(item)
store.dispatch('system/deletePageDataAction', {
pageName: props.pageName,
id: item.id
})
}
const handleNewClick = () => {
emit('newBtnClick')
}
const handleEditClick = (item: any) => {
emit('editBtnClick', item)
}
return {
dataList,
getPageData,
dataCount,
pageInfo,
otherPropSlots,
isCreate,
isUpdate,
isDelete,
handleDeleteClick,
handleNewClick,
handleEditClick
}
}
})
</script>
<style scoped>
.page-content {
padding: 20px;
border-top: 20px solid #f5f5f5;
}
</style>
typescript
/*-------------------src/service/main/system/system.ts-----------------*/
import hyRequest from '../../index'
import { IDataType } from '../../types'
export function getPageListData(url: string, queryInfo: any) {
return hyRequest.post<IDataType>({
url: url,
data: queryInfo
})
}
// url: /users/id
export function deletePageData(url: string) {
return hyRequest.delete<IDataType>({
url: url
})
}
export function createPageData(url: string, newData: any) {
return hyRequest.post<IDataType>({
url: url,
data: newData
})
}
export function editPageData(url: string, editData: any) {
return hyRequest.patch<IDataType>({
url: url,
data: editData
})
}
typescript
/*---------------src/store/main/system/system.ts---------------*/
import { Module } from 'vuex'
import { IRootState } from '@/store/types'
import { ISystemState } from './types'
import {
getPageListData,
deletePageData,
createPageData,
editPageData
} from '@/service/main/system/system'
const systemModule: Module<ISystemState, IRootState> = {
namespaced: true,
state() {
return {
usersList: [],
usersCount: 0,
roleList: [],
roleCount: 0,
goodsList: [],
goodsCount: 0,
menuList: [],
menuCount: 0
}
},
mutations: {
changeUsersList(state, userList: any[]) {
state.usersList = userList
},
changeUsersCount(state, userCount: number) {
state.usersCount = userCount
},
changeRoleList(state, list: any[]) {
state.roleList = list
},
changeRoleCount(state, count: number) {
state.roleCount = count
},
changeGoodsList(state, list: any[]) {
state.goodsList = list
},
changeGoodsCount(state, count: number) {
state.goodsCount = count
},
changeMenuList(state, list: any[]) {
state.menuList = list
},
changeMenuCount(state, count: number) {
state.menuCount = count
}
},
getters: {
pageListData(state) {
return (pageName: string) => {
return (state as any)[`${pageName}List`]
}
},
pageListCount(state) {
return (pageName: string) => {
return (state as any)[`${pageName}Count`]
}
}
},
actions: {
async getPageListAction({ commit }, payload: any) {
// 1.获取pageUrl
const pageName = payload.pageName
const pageUrl = `/${pageName}/list`
// 2.对页面发送请求
const pageResult = await getPageListData(pageUrl, payload.queryInfo)
// 3.将数据存储到state中
const { list, totalCount } = pageResult.data
const changePageName =
pageName.slice(0, 1).toUpperCase() + pageName.slice(1)
commit(`change${changePageName}List`, list)
commit(`change${changePageName}Count`, totalCount)
},
async deletePageDataAction({ dispatch }, payload: any) {
// 1.获取pageName和id
// pageName -> /users
// id -> /users/id
const { pageName, id } = payload
const pageUrl = `/${pageName}/${id}`
// 2.调用删除网络请求
await deletePageData(pageUrl)
// 3.重新请求最新的数据
dispatch('getPageListAction', {
pageName,
queryInfo: {
offset: 0,
size: 10
}
})
},
async createPageDataAction({ dispatch }, payload: any) {
// 1.创建数据的请求
const { pageName, newData } = payload
const pageUrl = `/${pageName}`
await createPageData(pageUrl, newData)
// 2.请求最新的数据
dispatch('getPageListAction', {
pageName,
queryInfo: {
offset: 0,
size: 10
}
})
},
async editPageDataAction({ dispatch }, payload: any) {
// 1.编辑数据的请求
const { pageName, editData, id } = payload
console.log(editData)
const pageUrl = `/${pageName}/${id}`
await editPageData(pageUrl, editData)
// 2.请求最新的数据
dispatch('getPageListAction', {
pageName,
queryInfo: {
offset: 0,
size: 10
}
})
}
}
}
export default systemModule
02:新建用户/编辑用户
typescript
/*---------------src/views/main/system/user/user.vue------------*/
<template>
<div class="user">
<page-search
:searchFormConfig="searchFormConfig"
@resetBtnClick="handleResetClick"
@queryBtnClick="handleQueryClick"
/>
<page-content
ref="pageContentRef"
:contentTableConfig="contentTableConfig"
pageName="users"
@newBtnClick="handleNewData"
@editBtnClick="handleEditData"
></page-content>
<page-modal
:defaultInfo="defaultInfo"
ref="pageModalRef"
pageName="users"
:modalConfig="modalConfigRef"
></page-modal>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useStore } from '@/store'
import PageSearch from '@/components/page-search'
import PageContent from '@/components/page-content'
import PageModal from '@/components/page-modal'
import { searchFormConfig } from './config/search.config'
import { contentTableConfig } from './config/content.config'
import { modalConfig } from './config/modal.config'
import { usePageSearch } from '@/hooks/use-page-search'
import { usePageModal } from '@/hooks/use-page-modal'
export default defineComponent({
name: 'users',
components: {
PageSearch,
PageContent,
PageModal
},
setup() {
const [pageContentRef, handleResetClick, handleQueryClick] = usePageSearch()
// pageModal相关的hook逻辑
// 1.处理密码的逻辑
const newCallback = () => {
const passwordItem = modalConfig.formItems.find(
(item) => item.field === 'password'
)
passwordItem!.isHidden = false
}
const editCallback = () => {
const passwordItem = modalConfig.formItems.find(
(item) => item.field === 'password'
)
passwordItem!.isHidden = true
}
// 2.动态添加部门和角色列表
const store = useStore()
const modalConfigRef = computed(() => {
const departmentItem = modalConfig.formItems.find(
(item) => item.field === 'departmentId'
)
departmentItem!.options = store.state.entireDepartment.map((item) => {
return { title: item.name, value: item.id }
})
const roleItem = modalConfig.formItems.find(
(item) => item.field === 'roleId'
)
roleItem!.options = store.state.entireRole.map((item) => {
return { title: item.name, value: item.id }
})
return modalConfig
})
// 3.调用hook获取公共变量和函数
const [pageModalRef, defaultInfo, handleNewData, handleEditData] =
usePageModal(newCallback, editCallback)
return {
searchFormConfig,
contentTableConfig,
pageContentRef,
handleResetClick,
handleQueryClick,
modalConfigRef,
handleNewData,
handleEditData,
pageModalRef,
defaultInfo
}
}
})
</script>
<style scoped></style>
typescript
/*-------------src/views/main/system/user/config/modal.config.ts------------*/
import { IForm } from '@/base-ui/form'
export const modalConfig: IForm = {
formItems: [
{
field: 'name',
type: 'input',
label: '用户名',
placeholder: '请输入用户名'
},
{
field: 'realname',
type: 'input',
label: '真实姓名',
placeholder: '请输入真实姓名'
},
{
field: 'password',
type: 'password',
label: '用户密码',
placeholder: '请输入密码',
isHidden: false
},
{
field: 'cellphone',
type: 'input',
label: '电话号码',
placeholder: '请输入电话号码'
},
{
field: 'departmentId',
type: 'select',
label: '选择部门',
placeholder: '请选择部门',
options: []
},
{
field: 'roleId',
type: 'select',
label: '选择角色',
placeholder: '请选择角色',
options: []
}
],
colLayout: { span: 24 },
itemStyle: {}
}
typescript
/*-----------src/components/page-modal-------------*/
import PageModal from './src/page-modal.vue'
export default PageModal
/*------------src/components/page-modal/src/page-modal.vue-----------*/
<template>
<div class="page-modal">
<el-dialog
title="新建用户"
v-model="dialogVisible"
width="30%"
center
destroy-on-close
>
<hy-form v-bind="modalConfig" v-model="formData"></hy-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="handleConfirmClick">
确 定
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
import { useStore } from 'vuex'
import HyForm from '@/base-ui/form'
export default defineComponent({
components: {
HyForm
},
props: {
modalConfig: {
type: Object,
required: true
},
defaultInfo: {
type: Object,
default: () => ({})
},
pageName: {
type: String,
require: true
}
},
setup(props) {
const dialogVisible = ref(false)
const formData = ref<any>({})
watch(
() => props.defaultInfo,
(newValue) => {
for (const item of props.modalConfig.formItems) {
formData.value[`${item.field}`] = newValue[`${item.field}`]
}
}
)
// 点击确定按钮的逻辑
const store = useStore()
const handleConfirmClick = () => {
dialogVisible.value = false
if (Object.keys(props.defaultInfo).length) {
// 编辑
console.log('编辑用户')
store.dispatch('system/editPageDataAction', {
pageName: props.pageName,
editData: { ...formData.value },
id: props.defaultInfo.id
})
} else {
// 新建
console.log('新建用户')
store.dispatch('system/createPageDataAction', {
pageName: props.pageName,
newData: { ...formData.value }
})
}
}
return {
dialogVisible,
formData,
handleConfirmClick
}
}
})
</script>
<style scoped></style>
typescript
/*------------src/components/page-content/src/page-content.vue----------*/
<template>
<div class="page-content">
<hy-table
:listData="dataList"
:listCount="dataCount"
v-bind="contentTableConfig"
v-model:page="pageInfo"
>
<!-- 1.header中的插槽 -->
<template #headerHandler>
<el-button
v-if="isCreate"
type="primary"
size="medium"
@click="handleNewClick"
>
新建用户
</el-button>
</template>
<!-- 2.列中的插槽 -->
<template #status="scope">
<el-button
plain
size="mini"
:type="scope.row.enable ? 'success' : 'danger'"
>
{{ scope.row.enable ? '启用' : '禁用' }}
</el-button>
</template>
<template #createAt="scope">
<span>{{ $filters.formatTime(scope.row.createAt) }}</span>
</template>
<template #updateAt="scope">
<span>{{ $filters.formatTime(scope.row.updateAt) }}</span>
</template>
<template #handler="scope">
<div class="handle-btns">
<el-button
v-if="isUpdate"
icon="el-icon-edit"
size="mini"
type="text"
@click="handleEditClick(scope.row)"
>
编辑
</el-button>
<el-button
v-if="isDelete"
icon="el-icon-delete"
size="mini"
type="text"
@click="handleDeleteClick(scope.row)"
>删除</el-button
>
</div>
</template>
<!-- 在page-content中动态插入剩余的插槽 -->
<template
v-for="item in otherPropSlots"
:key="item.prop"
#[item.slotName]="scope"
>
<template v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</template>
</hy-table>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, watch } from 'vue'
import { useStore } from '@/store'
import { usePermission } from '@/hooks/use-permission'
import HyTable from '@/base-ui/table'
export default defineComponent({
components: {
HyTable
},
props: {
contentTableConfig: {
type: Object,
require: true
},
pageName: {
type: String,
required: true
}
},
emits: ['newBtnClick', 'editBtnClick'],
setup(props, { emit }) {
const store = useStore()
// 0.获取操作的权限
const isCreate = usePermission(props.pageName, 'create')
const isUpdate = usePermission(props.pageName, 'update')
const isDelete = usePermission(props.pageName, 'delete')
const isQuery = usePermission(props.pageName, 'query')
// 1.双向绑定pageInfo
const pageInfo = ref({ currentPage: 1, pageSize: 10 })
watch(pageInfo, () => getPageData())
// 2.发送网络请求
const getPageData = (queryInfo: any = {}) => {
if (!isQuery) return
store.dispatch('system/getPageListAction', {
pageName: props.pageName,
queryInfo: {
offset: (pageInfo.value.currentPage - 1) * pageInfo.value.pageSize,
size: pageInfo.value.pageSize,
...queryInfo
}
})
}
getPageData()
// 3.从vuex中获取数据
const dataList = computed(() =>
store.getters[`system/pageListData`](props.pageName)
)
const dataCount = computed(() =>
store.getters[`system/pageListCount`](props.pageName)
)
// 4.获取其他的动态插槽名称
const otherPropSlots = props.contentTableConfig?.propList.filter(
(item: any) => {
if (item.slotName === 'status') return false
if (item.slotName === 'createAt') return false
if (item.slotName === 'updateAt') return false
if (item.slotName === 'handler') return false
return true
}
)
// 5.删除/编辑/新建操作
const handleDeleteClick = (item: any) => {
console.log(item)
store.dispatch('system/deletePageDataAction', {
pageName: props.pageName,
id: item.id
})
}
const handleNewClick = () => {
emit('newBtnClick')
}
const handleEditClick = (item: any) => {
emit('editBtnClick', item)
}
return {
dataList,
getPageData,
dataCount,
pageInfo,
otherPropSlots,
isCreate,
isUpdate,
isDelete,
handleDeleteClick,
handleNewClick,
handleEditClick
}
}
})
</script>
<style scoped>
.page-content {
padding: 20px;
border-top: 20px solid #f5f5f5;
}
</style>
typescript
/*-----------------src/hooks/use-page-modal.ts-------------*/
import { ref } from 'vue'
import PageModal from '@/components/page-modal'
type CallbackFn = () => void
export function usePageModal(newCb?: CallbackFn, editCb?: CallbackFn) {
const pageModalRef = ref<InstanceType<typeof PageModal>>()
const defaultInfo = ref({})
const handleNewData = () => {
defaultInfo.value = {}
if (pageModalRef.value) {
pageModalRef.value.dialogVisible = true
}
newCb && newCb()
}
const handleEditData = (item: any) => {
defaultInfo.value = { ...item }
if (pageModalRef.value) {
pageModalRef.value.dialogVisible = true
}
editCb && editCb()
}
return [pageModalRef, defaultInfo, handleNewData, handleEditData]
}
typescript
/*-------------src/base-ui/form/src/form.vue----------*/
<template>
<div class="hy-form">
<div class="header">
<slot name="header"></slot>
</div>
<el-form :label-width="labelWidth">
<el-row>
<template v-for="item in formItems" :key="item.label">
<el-col v-bind="colLayout">
<el-form-item
v-if="!item.isHidden"
:label="item.label"
:rules="item.rules"
:style="itemStyle"
>
<template
v-if="item.type === 'input' || item.type === 'password'"
>
<el-input
:placeholder="item.placeholder"
v-bind="item.otherOptions"
:show-password="item.type === 'password'"
:model-value="modelValue[`${item.field}`]"
@update:modelValue="handleValueChange($event, item.field)"
/>
</template>
<template v-else-if="item.type === 'select'">
<el-select
:placeholder="item.placeholder"
v-bind="item.otherOptions"
style="width: 100%"
:model-value="modelValue[`${item.field}`]"
@update:modelValue="handleValueChange($event, item.field)"
>
<el-option
v-for="option in item.options"
:key="option.value"
:value="option.value"
>{{ option.title }}</el-option
>
</el-select>
</template>
<template v-else-if="item.type === 'datepicker'">
<el-date-picker
style="width: 100%"
v-bind="item.otherOptions"
:model-value="modelValue[`${item.field}`]"
@update:modelValue="handleValueChange($event, item.field)"
></el-date-picker>
</template>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { IFormItem } from '../types'
export default defineComponent({
props: {
modelValue: {
type: Object,
required: true
},
formItems: {
type: Array as PropType<IFormItem[]>,
default: () => []
},
labelWidth: {
type: String,
default: '100px'
},
itemStyle: {
type: Object,
default: () => ({ padding: '10px 40px' })
},
colLayout: {
type: Object,
default: () => ({
xl: 6, // >1920px 4个
lg: 8,
md: 12,
sm: 24,
xs: 24
})
}
},
emits: ['update:modelValue'],
setup(props, { emit }) {
// const formData = ref({ ...props.modelValue })
// watch(
// formData,
// (newValue) => {
// console.log(newValue)
// emit('update:modelValue', newValue)
// },
// {
// deep: true
// }
// )
const handleValueChange = (value: any, field: string) => {
emit('update:modelValue', { ...props.modelValue, [field]: value })
}
return {
handleValueChange
}
}
})
</script>
<style scoped lang="less">
.hy-form {
padding-top: 22px;
}
</style>
typescript
/*----------------src/store/index.ts---------------------*/
import { createStore, Store, useStore as useVuexStore } from 'vuex'
import login from './login/login'
import system from './main/system/system'
import { getPageListData } from '@/service/main/system/system'
import { IRootState, IStoreType } from './types'
const store = createStore<IRootState>({
state() {
return {
name: 'coderwhy',
age: 18,
entireDepartment: [],
entireRole: []
}
},
mutations: {
changeEntireDepartment(state, list) {
state.entireDepartment = list
},
changeEntireRole(state, list) {
state.entireRole = list
}
},
getters: {},
actions: {
async getInitialDataAction({ commit }) {
// 1.请求部门和角色数据
const departmentResult = await getPageListData('/department/list', {
offset: 0,
size: 1000
})
const { list: departmentList } = departmentResult.data
const roleResult = await getPageListData('/role/list', {
offset: 0,
size: 1000
})
const { list: roleList } = roleResult.data
// 2.保存数据
commit('changeEntireDepartment', departmentList)
commit('changeEntireRole', roleList)
}
},
modules: {
login,
system
}
})
export function setupStore() {
store.dispatch('login/loadLocalLogin')
store.dispatch('getInitialDataAction')
}
export function useStore(): Store<IStoreType> {
return useVuexStore()
}
export default store
typescript
/*----------------src/store/main/system/system.ts---------------*/
import { Module } from 'vuex'
import { IRootState } from '@/store/types'
import { ISystemState } from './types'
import {
getPageListData,
deletePageData,
createPageData,
editPageData
} from '@/service/main/system/system'
const systemModule: Module<ISystemState, IRootState> = {
namespaced: true,
state() {
return {
usersList: [],
usersCount: 0,
roleList: [],
roleCount: 0,
goodsList: [],
goodsCount: 0,
menuList: [],
menuCount: 0
}
},
mutations: {
changeUsersList(state, userList: any[]) {
state.usersList = userList
},
changeUsersCount(state, userCount: number) {
state.usersCount = userCount
},
changeRoleList(state, list: any[]) {
state.roleList = list
},
changeRoleCount(state, count: number) {
state.roleCount = count
},
changeGoodsList(state, list: any[]) {
state.goodsList = list
},
changeGoodsCount(state, count: number) {
state.goodsCount = count
},
changeMenuList(state, list: any[]) {
state.menuList = list
},
changeMenuCount(state, count: number) {
state.menuCount = count
}
},
getters: {
pageListData(state) {
return (pageName: string) => {
return (state as any)[`${pageName}List`]
}
},
pageListCount(state) {
return (pageName: string) => {
return (state as any)[`${pageName}Count`]
}
}
},
actions: {
async getPageListAction({ commit }, payload: any) {
// 1.获取pageUrl
const pageName = payload.pageName
const pageUrl = `/${pageName}/list`
// 2.对页面发送请求
const pageResult = await getPageListData(pageUrl, payload.queryInfo)
// 3.将数据存储到state中
const { list, totalCount } = pageResult.data
const changePageName =
pageName.slice(0, 1).toUpperCase() + pageName.slice(1)
commit(`change${changePageName}List`, list)
commit(`change${changePageName}Count`, totalCount)
},
async deletePageDataAction({ dispatch }, payload: any) {
// 1.获取pageName和id
// pageName -> /users
// id -> /users/id
const { pageName, id } = payload
const pageUrl = `/${pageName}/${id}`
// 2.调用删除网络请求
await deletePageData(pageUrl)
// 3.重新请求最新的数据
dispatch('getPageListAction', {
pageName,
queryInfo: {
offset: 0,
size: 10
}
})
},
async createPageDataAction({ dispatch }, payload: any) {
// 1.创建数据的请求
const { pageName, newData } = payload
const pageUrl = `/${pageName}`
await createPageData(pageUrl, newData)
// 2.请求最新的数据
dispatch('getPageListAction', {
pageName,
queryInfo: {
offset: 0,
size: 10
}
})
},
async editPageDataAction({ dispatch }, payload: any) {
// 1.编辑数据的请求
const { pageName, editData, id } = payload
console.log(editData)
const pageUrl = `/${pageName}/${id}`
await editPageData(pageUrl, editData)
// 2.请求最新的数据
dispatch('getPageListAction', {
pageName,
queryInfo: {
offset: 0,
size: 10
}
})
}
}
}
export default systemModule