1.权限
1-1 功能级别权限
首先在根目录中的premission.ts文件中
在路由跳转前的钩子函数中,调用了userStore仓库中的setUserInfoAction方法
在setUserInfoAction方法中 通过封装的getInfo这个api去请求后端获取到当前用户的权限信息
把获取到的用户信息存到全局仓库中 以便各个组件模块调用
1-2 使用示例
通过自定义指令的方式 并且基于用户的权限信息 来实现权限的控制
// src/directives/permission/hasPermi.ts
import type { App } from 'vue'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const { t } = useI18n() // 国际化
export function hasPermi(app: App<Element>) {
app.directive('hasPermi', (el, binding) => {
const { wsCache } = useCache()
// value 就是指令中传过来的参数
const { value } = binding
const all_permission = '*:*:*'
const permissions = wsCache.get(CACHE_KEY.USER).permissions
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
// 判断该参数是否在用户的权限信息中存在 如果存在 则权限验证成功 如果不存在 则权限验证失败 并且删除该DOM
const hasPermissions = permissions.some((permission: string) => {
return all_permission === permission || permissionFlag.includes(permission)
})
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(t('permission.hasPermission'))
}
})
}
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:tenant:create']"
>
<Icon icon="ep:plus" class="mr-5px" />
新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
// 这里的权限参数也可以传多个 只要其中一个在用户的权限信息中存在 就可以通过权限验证
v-hasPermi="['system:tenant:export']"
>
<Icon icon="ep:download" class="mr-5px" />
导出
</el-button>
结合 v-if 指令
在某些情况下,它是不适合使用 v-hasPermi
或 v-hasRole
指令,如元素标签组件。此时,只能通过手动设置 v-if
,通过使用全局权限判断函数,用法是基本一致的。
<el-tabs v-model="activeName">
<el-tab-pane label="商品信息" name="basicInfo" v-if="checkPermi(['system:user:aaa'])">
<BasicInfoForm
ref="basicInfoRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
<el-tab-pane label="商品详情" name="description">
<DescriptionForm
ref="descriptionRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
<el-tab-pane label="其他设置" name="otherSettings">
<OtherSettingsForm
ref="otherSettingsRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
</el-tabs>
<el-tab-pane label="商品信息" name="basicInfo" v-hasPermi="['system:user:aaa']">
<BasicInfoForm
ref="basicInfoRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
<el-tab-pane label="商品详情" name="description">
<DescriptionForm
ref="descriptionRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
<el-tab-pane label="其他设置" name="otherSettings">
<OtherSettingsForm
ref="otherSettingsRef"
v-model:activeName="activeName"
:propFormData="formData"
/>
</el-tab-pane>
2. 系统组件(部分)
2.1 弹框组件(dialog)
对 Element Plus
的 Dialog
组件进行封装,支持最大化、最大高度等特性
- Dialog 组件:位于
src/components/Dialog/src/Dialog.vue
内
<script lang="ts" name="Dialog" setup>
import { propTypes } from '@/utils/propTypes'
import { isNumber } from '@/utils/is'
const slots = useSlots()
const props = defineProps({
modelValue: propTypes.bool.def(false),
title: propTypes.string.def('Dialog'),
fullscreen: propTypes.bool.def(true),
width: propTypes.oneOfType([String, Number]).def('40%'),
scroll: propTypes.bool.def(false), // 是否开启滚动条。如果是的话,按照 maxHeight 设置最大高度
maxHeight: propTypes.oneOfType([String, Number]).def('300px')
})
const getBindValue = computed(() => {
const delArr: string[] = ['fullscreen', 'title', 'maxHeight']
const attrs = useAttrs()
const obj = { ...attrs, ...props }
for (const key in obj) {
if (delArr.indexOf(key) !== -1) {
delete obj[key]
}
}
return obj
})
const isFullscreen = ref(false)
const toggleFull = () => {
isFullscreen.value = !unref(isFullscreen)
}
const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
watch(
() => isFullscreen.value,
async (val: boolean) => {
// 计算最大高度
await nextTick()
if (val) {
const windowHeight = document.documentElement.offsetHeight
dialogHeight.value = `${windowHeight - 55 - 60 - (slots.footer ? 63 : 0)}px`
} else {
dialogHeight.value = isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight
}
},
{
immediate: true
}
)
const dialogStyle = computed(() => {
return {
height: unref(dialogHeight)
}
})
</script>
<template>
<ElDialog
:close-on-click-modal="true"
:fullscreen="isFullscreen"
:width="width"
destroy-on-close
draggable
lock-scroll
v-bind="getBindValue"
>
<template #header>
<div class="flex justify-between">
<slot name="title">
{{ title }}
</slot>
<Icon
v-if="fullscreen"
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
class="mr-22px cursor-pointer is-hover mt-2px z-10"
color="var(--el-color-info)"
@click="toggleFull"
/>
</div>
</template>
使用示例:
2.2 button组件
- button 组件:位于
src/components/XButton/src/XButton.vue
内
const props = defineProps({
modelValue: propTypes.bool.def(false),
loading: propTypes.bool.def(false),
preIcon: propTypes.string.def(''),
postIcon: propTypes.string.def(''),
title: propTypes.string.def(''),
type: propTypes.oneOf(['', 'primary', 'success', 'warning', 'danger', 'info']).def(''),
link: propTypes.bool.def(false),
circle: propTypes.bool.def(false),
round: propTypes.bool.def(false),
plain: propTypes.bool.def(false),
onClick: { type: Function as PropType<(...args) => any>, default: null }
})
<el-button v-bind="getBindValue" @click="onClick">
<Icon v-if="preIcon" :icon="preIcon" class="mr-1px" />
{{ title ? title : '' }}
<Icon v-if="postIcon" :icon="postIcon" class="mr-1px" />
</el-button>
使用示例
<XButton
preIcon="fa:align-left"
class="align align-top"
@click="elementsAlign('right')"
:disabled="true"
/>
2.3 分页组件
- pagination 组件:位于
src/components/pagination/index.vue
内
<!-- 基于 ruoyi-vue3 的 Pagination 重构,核心是简化无用的属性,并使用 ts 重写 -->
<template>
<el-pagination
v-show="total > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:background="true"
:page-sizes="[10, 20, 30, 50, 100]"
:pager-count="pagerCount"
:total="total"
class="float-right mt-15px mb-15px"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<script name="Pagination" setup>
import { computed } from 'vue'
const props = defineProps({
// 总条目数
total: {
required: true,
type: Number
},
// 当前页数:pageNo
page: {
type: Number,
default: 1
},
// 每页显示条目个数:pageSize
limit: {
type: Number,
default: 20
},
// 设置最大页码按钮数。 页码按钮的数量,当总页数超过该值时会折叠
// 移动端页码按钮的数量端默认值 5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
}
})
const emit = defineEmits(['update:page', 'update:limit', 'pagination', 'pagination'])
const currentPage = computed({
get() {
return props.page
},
set(val) {
// 触发 update:page 事件,更新 limit 属性,从而更新 pageNo
emit('update:page', val)
}
})
const pageSize = computed({
get() {
return props.limit
},
set(val) {
// 触发 update:limit 事件,更新 limit 属性,从而更新 pageSize
emit('update:limit', val)
}
})
const handleSizeChange = (val) => {
// 如果修改后超过最大页面,强制跳转到第 1 页
if (currentPage.value * val > props.total) {
currentPage.value = 1
}
// 触发 pagination 事件,重新加载列表
emit('pagination', { page: currentPage.value, limit: val })
}
const handleCurrentChange = (val) => {
// 触发 pagination 事件,重新加载列表
emit('pagination', { page: val, limit: pageSize.value })
}
</script>
使用示例
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
CRUD组件
Form 表单组件 使用示例
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Form ref="formRef" v-loading="formLoading" :rules="rules" :schema="allSchemas.formSchema" />
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
import { dateFormatter } from '@/utils/formatTime'
const { t } = useI18n() // 国际化
// 表单校验
export const rules = reactive({
mail: [
{ required: true, message: t('profile.rules.mail'), trigger: 'blur' },
{
type: 'email',
message: t('profile.rules.truemail'),
trigger: ['blur', 'change']
}
],
username: [required],
password: [required],
host: [required],
port: [required],
sslEnable: [required]
})
// CrudSchema:https://doc.iocoder.cn/vue3/crud-schema/
const crudSchemas = reactive<CrudSchema[]>([
{
label: '邮箱',
field: 'mail',
isSearch: true
},
{
label: '用户名',
field: 'username',
isSearch: true
},
{
label: '密码',
field: 'password',
isTable: false
},
{
label: 'SMTP 服务器域名',
field: 'host'
},
{
label: 'SMTP 服务器端口',
field: 'port',
form: {
component: 'InputNumber',
value: 465
}
},
{
label: '是否开启 SSL',
field: 'sslEnable',
dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
dictClass: 'boolean',
form: {
component: 'Radio'
}
},
{
label: '创建时间',
field: 'createTime',
isForm: false,
formatter: dateFormatter,
detail: {
dateFormat: 'YYYY-MM-DD HH:mm:ss'
}
},
{
label: '操作',
field: 'action',
isForm: false,
isDetail: false
}
])
export const { allSchemas } = useCrudSchemas(crudSchemas)
Search 和 Table组件使用示例
<template>
<doc-alert title="邮件配置" url="https://doc.iocoder.cn/mail" />
<!-- 搜索工作栏 -->
<ContentWrap>
<Search :schema="allSchemas.searchSchema" @search="setSearchParams" @reset="setSearchParams">
<!-- 新增等操作按钮 -->
<template #actionMore>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:mail-account:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
</template>
</Search>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<Table
:columns="allSchemas.tableColumns"
:data="tableObject.tableList"
:loading="tableObject.loading"
:pagination="{
total: tableObject.total
}"
v-model:pageSize="tableObject.pageSize"
v-model:currentPage="tableObject.currentPage"
>
<template #action="{ row }">
<el-button
link
type="primary"
@click="openForm('update', row.id)"
v-hasPermi="['system:mail-account:update']"
>
编辑
</el-button>
<el-button
link
type="primary"
@click="openDetail(row.id)"
v-hasPermi="['system:mail-account:query']"
>
详情
</el-button>
<el-button
link
type="danger"
v-hasPermi="['system:mail-account:delete']"
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</Table>
</ContentWrap>
<!-- 表单弹窗:添加/修改 -->
<MailAccountForm ref="formRef" @success="getList" />
<!-- 详情弹窗 -->
<MailAccountDetail ref="detailRef" />
</template>
<script setup lang="ts" name="SystemMailAccount">
import { allSchemas } from './account.data'
import * as MailAccountApi from '@/api/system/mail/account'
import MailAccountForm from './MailAccountForm.vue'
import MailAccountDetail from './MailAccountDetail.vue'
// tableObject:表格的属性对象,可获得分页大小、条数等属性
// tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
const { tableObject, tableMethods } = useTable({
getListApi: MailAccountApi.getMailAccountPage, // 分页接口
delListApi: MailAccountApi.deleteMailAccount // 删除接口
})
// 获得表格的各种操作
const { getList, setSearchParams } = tableMethods
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 详情操作 */
const detailRef = ref()
const openDetail = (id: number) => {
detailRef.value.open(id)
}
/** 删除按钮操作 */
const handleDelete = (id: number) => {
tableMethods.delList(id, false)
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
// 过滤所有结构
export const useCrudSchemas = (
crudSchema: CrudSchema[]
): {
allSchemas: AllSchemas
} => {
// 所有结构数据
const allSchemas = reactive<AllSchemas>({
searchSchema: [],
tableColumns: [],
formSchema: [],
detailSchema: []
})
const searchSchema = filterSearchSchema(crudSchema, allSchemas)
allSchemas.searchSchema = searchSchema || []
const tableColumns = filterTableSchema(crudSchema)
allSchemas.tableColumns = tableColumns || []
const formSchema = filterFormSchema(crudSchema, allSchemas)
allSchemas.formSchema = formSchema
const detailSchema = filterDescriptionsSchema(crudSchema)
allSchemas.detailSchema = detailSchema
return {
allSchemas
}
}
detail 组件使用示例
<Dialog v-model="dialogVisible" title="详情">
<Descriptions :data="detailData" :schema="allSchemas.detailSchema" />
</Dialog>