芋道源码

2,285 阅读3分钟

1.权限

1-1 功能级别权限

首先在根目录中的premission.ts文件中
在路由跳转前的钩子函数中,调用了userStore仓库中的setUserInfoAction方法
在setUserInfoAction方法中 通过封装的getInfo这个api去请求后端获取到当前用户的权限信息
把获取到的用户信息存到全局仓库中 以便各个组件模块调用

01.jpg

02.jpg

03.jpg

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>

04.jpg

 <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>

05.jpg

2. 系统组件(部分)

2.1 弹框组件(dialog)

Element PlusDialog 组件进行封装,支持最大化、最大高度等特性

  • 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>

使用示例:

06.jpg

07.jpg

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)

08.jpg

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
  }
}

1686300286297.jpg

detail 组件使用示例

 <Dialog v-model="dialogVisible" title="详情">
    <Descriptions :data="detailData" :schema="allSchemas.detailSchema" />
  </Dialog>

3