!11 follow后端发布

* docs: readme
* fix: missing formPath
* chore: 去除锁定的esbuild版本
* perf: 去除debug组件
* perf: 参数键值 自动高度
* refactor: 代码生成配置页面重构 去除步骤条
* perf: 移除文件
* docs: 文件夹说明
* chore: 移除一些配置项
* chore: 注释优化
* refactor: 移除ele和naive目录
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* perf: request support to set how to return response (#5436)
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* refactor: 登录超时的i18n
* fix: requestClient缺失i18n内容
* refactor: 优化oss下载进度提示
* feat: 下载进度loading
* fix: antd button icon style (#5421)
* feat: oss下载进度(已下载的KB 无法作为进度显示 total返回为null)
* fix: 下载文件时(responseType === 'blob')需要判断下载失败(返回json而非二进制)的情况
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* refactor: 新增后跳转到未发布流程
* fix: same name route
* chore: 调整为部署json类型
* fix: mouse events ignored on modal loading (#5409)
* docs: update docs (#5408)
* refactor: 移除已经弃用的方法
* refactor: follow官方handleRangeTimeValue更新
* chore: 删除文件夹(前端路由需要的)
* chore: 修改本地路由写法(新版)/新增本地菜单图标
* fix: form update state error before form mounted (#5406)
* fix: demos route fixed (#5405)
* chore: 不使用基础布局(仅在顶级生效)
* feat: modal state locked on submitting (#5401)
* chore: 修改zIndex
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* refactor: fix popup component zIndex (#5397)
* style: element plus loading style fixed (#5393)
* perf: improve fieldMappingTime to support format function (#5392)
* fix: hide root route in breadcrumb
* feat: support set default props for drawer and modal (#5390)
* fix: root router config fixed (#5389)
* fix: 修改Vxe默认zIndex为995 解决右上角全屏后modal/drawer(zIndex: 1000)被遮挡
* feat: add `noBasicLayout` in route meta (#5386)
* chore: wechat image
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* chore: 改为全局参数配置 去除局部参数
* fix: spinner may stop playing animation after dismiss (#5365)
* style: popover bgColor is too close to common (#5364)
* docs: version update
* docs: changelog
* chore: 文件上传 描述
* ci: retry deploy while faild
* feat: 文件上传 进度条+提示文字
* feat: 文件上传 进度条
* feat: 上传文件格式说明
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into warmflow
* fix: useEcharts return invalid instance (#5360)
* feat: popup component support overlay blur effect (#5359)
* feat: improve `tippy` demo (#5357)
* feat: integrate new component `Tippy` with demo (#5355)
* chore: 优化表格图片显示
* perf: add nested modal demo (#5353)
* chore: 默认显示右边的滚动条 防止出现滚动条被挤压
* perf: modal and drawer api support chain calls (#5351)
* feat: allow close tab when mouse middle button click (#5347)
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into warmflow
* refactor: 重构显示total的逻辑
* chore: 调整高度自适应代码
* chore: vxe升级4.10.0版本(锁定)
* fix: 添加失效的option
* fix: 需要为数组
* fix: locale switching logic correction (#5344)
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into warmflow
* chore: 导入类型优化 解决eslint报红
* refactor: type/注释优化 去除大量any
* fix: vxeGrid init without search form (#5342)
* chore: 锁屏默认false  关闭该功能
* chore: 调整接口
* chore: update deps
* fix: primaryColor calculation (#5337)
* fix: form valid-error style in naive (#5336)
* fix: form `fieldMappingTime` improve and `modelPropName` support (#5335)
* fix: code lint
* fix: form `fieldMappingTime` is not working (#5333)
* chore: 选人组件样式
* fix: download from url triggered twice sometimes (#5319)
* chore: 优化代码
* chore: 动态类名(无效)改为style
* refactor: 字典相关功能重构 采用一个Map储存字典(之前为两个Map)
* feat: 字典支持number类型存储
* chore: 调整样式
* chore: 修改选中border为1px
* chore: 字段
* chore: 改为新窗口打开(适用于pdf/图片)而非直接下载
* chore: 更新样式
* chore: 更新字段
* chore: 改为computed
* chore: 跳转到未发布流程tab
* chore: 优化样式
* docs: readme
* fix: name重复导致的404
* Merge branch 'dev' of https://gitee.com/dapppp/ruoyi-plus-vben5 into warmflow
* chore: 使用legacy来保证copy的兼容性
* chore: 去除log 添加说明
* chore: 优化代码
* feat: 节点关联/节点独立的切换逻辑
* chore: remove logic
* chore: vxe可编辑表格demo
* chore: 不允许在按钮下添加数据
* docs: changelog
* fix: wrong code
* chore: 移除测试菜单
* chore: 优化代码
* refactor: 租户套餐菜单替换为新版
* refactor: 使用新版菜单勾选
* chore: 点行会勾选/取消全部权限  点权限不会勾选行
* chore: 全屏引导+样式优化
* chore: 调整间距
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* feat: useEcharts exports echarts instance#5294 (#5299)
* chore: update quick-start.md (#5303)
* chore: updateCheckedNumber
* refactor: 优化代码
* chore: 优化代码
* chore: 优化样式
* chore: keys依赖于menu 需要先加载menu
* chore: 菜单加载完毕再显示
* feat: 新的菜单选择组件(beta)
* chore: $t
* chore: 测试菜单页面
* chore: 优化代码
* feat: 对ossId回显的支持
* chore: 只获取一次默认密码而非每次打开modal都获取
* fix: vben select placeholder color (#5286)
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* perf: format code with better style (#5283)
* chore: 工作流演示站
* fix: sidebar preferences fixed (#5276)
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* fix: breadcrumb setting not valid for `header-sidebar-nav` layout (#5275)
* fix: header logo may not be collapsed in `header-sidebar-nav` layout (#5274)
* feat: new layout `sidebar nav with full header` (#5270)
* feat: drawer close icon placement (#5269)
* docs: update dialog and drawer docs
* feat: drawer support destroy on close
* feat: drawer support `onOpened` & `onClosed`
* feat: modal support destroy on close
* fix: wrong boolean
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* chore: 调整tab位置
* chore: 删除历史流程 改为tab切换
* fix: header-mixed layout side-menu active (#5265)
* feat: header mixed layout (#5263)
* chore: release 5.5.2
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* chore: downgrade vue-tsc version
* feat: header menu align support (#5256)
* chore: update deps
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* chore: add apiSelect remote search demo (#5246)
* chore: 审批改为description而非disabled的表单
* chore: 改为ts
* chore: 错误的conetnt
* refactor: 终止/转办/委托支持填写意见
* chore: 第一次拿到的是readonly的数据 如果需要修改 需要cloneDeep
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* fix: grid form submit button locale switch (#5205)
* chore: 调整驳回
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* fix: build error (#5199)
* fix: esbuild自动升级导致运行/打包报错
* fix: esbuild自动升级导致运行/打包报错
* chore: 流程定义 激活改为switch
* chore: 流程申请支持上传文件
* chore: title 审批通过
* fix: vxeGrid top padding (#5193)
* fix: 表格排序翻页会丢失排序参数
* chore: 去除log打印
* chore: 流程监控 待办任务
* chore: 我发起的
* chore: 去除已经移除的菜单页面
* chore: 我的已办
* chore: 页面优化
* chore: 重置tooltip
* feat: 我的抄送搜索/优化重复触发的接口
* feat: 流程定义 历史
* chore: 修改分类
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* fix: grid tools in toolbar config not working as expected (#5190)
* feat: add `resizable` and `ColPage` component (#5188)
* chore: 条件
* chore: break-all
* feat: 流程分类 搜索
* chore: 弹窗关闭后仍然显示表单浮层
* chore: 选人组件的样式
* chore: 搜索的样式
* chore: 漏掉的导入
* chore: 最大显示的头像数量 超过显示为省略号头像
* fix: 选人的一些问题
* Merge branch 'warmflow' of https://gitee.com/dapppp/ruoyi-plus-vben5 i…
* chore: 没有更多数据了
* fix: sidebar header height (#5183)
* chore: 搜索表单布局+申请人
* fix: remove the overlap caused by border-b (#5160)
* docs: fix typos (#5169)
* fix: resolve eslint errors as well as TS type errors (#5172)
* chore: enter提交表单
* chore: 修改文案
* chore: 默认全部展开
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* feat: page content class override (#5179)
* fix: sidebar style on focus (#5178)
* fix: 抄送选人 最右侧已选中删除item无效
* feat: 复制
* chore: 昵称过长的显示
* chore: 默认选中第一个
* chore: 修改relative位置
* chore: 搜索
* feat: 我的待办 - 搜索条件
* chore: 流程监控 - 待办任务页面的id不唯一 改为前端处理
* feat: 修改办理人
* chore: 流程干预 - 加签/减签
* chore: avatar大小
* chore: 抄送需要手动添加createByName
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* chore: 审批通过 抄送
* feat: 流程实例-流程预览
* chore: spell
* chore: clientid
* chore: 分类条件
* chore: 修改办理人
* chore: 更改postMessage参数
* chore: 内嵌iframe高度根据表单高度调整
* chore: 流程详情
* feat: 抄送选择
* chore: 调整分类树
* fix: user homePath no effect sometimes (#5166)
* feat: form compact mode support (#5165)
* fix: form auto submit no effect when showDefaultActions is false (#5163)
* chore: 修改width
* feat: 待办任务
* feat: 我的抄送
* chore: 流程定义 样式
* chore: 退回后重新申请
* chore: 请假申请布局
* chore: 请假申请-并行会签网关
* chore: 分类去除根目录
* chore: 详情modal(未完成)
* chore: 请假申请根据不同状态显示按钮
* chore: 流程删除/撤销
* chore: 审批完成后刷新当前页
* feat: 选人组件(未完成) 加签减签
* docs: fix docs-link and add `EllipsisText` docs (#5158)
* chore: 新窗口打开文件
* chore: 审批通过
* chore: 使用useEventListener替换原生
* chore: 字段错误
* chore: iframe通信 加载完毕后才显示表单 解决卡顿问题
* chore: 审批终止/驳回
* chore: 附件图标
* chore: process_running显示按钮
* chore: label错误
* chore: 保存的事件
* chore: 需要加上clientId
* Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
* feat: form `colon` support (#5156)
* chore: 完善请假申请
* feat: improve code login demo (#5154)
* chore: 客户端管理 行高自适应
* chore: 内嵌表单的路径
* chore: 修改avatar背景色
* chore: 注释
* chore: activePath
* chore: leave表单
* chore: 修改请假demo路径
* chore: categoryId
* chore: 我的已办
* chore: 我发起的
* chore: loading
* chore: 历史版本
* chore: 完善task api
* chore: 隐藏'菜单加载中'
* chore: missing import
* feat: add demo for modify menu badge data
* chore: 流程实例
* chore: 审批附件
* chore: 我的待办 提取公共组件
* chore: 流程部署
* chore: 新增/编辑/导出xml
* chore: 流程定义(除历史版本)
* feat: `autoActivateChild` support more layout mode (#5148)
* feat: auto activate subMenu on select root menu (#5147)
* fix: `disabledOnChangeListener` not work in form (#5146)
* fix: login expired modal z-index (#5145)
* feat: user-dropdown support `hover` trigger (#5143)
* fix: pinInput value synchronous update (#5142)
* fix: vxeGrid default sort data no effect in first query (#5141)
* fix: vscode debug profile (#5140)
* fix: form component events bind (#5137)
* chore: 在线用户样式 开启虚拟滚动
* chore: 去掉个人中心 在线设备的分页
* chore: 去掉在线用户的分页
* chore: changelog
* refactor: 获取字典的方法 提取公共函数 减少冗余代码
* fix: element plus validate failed style (#5130)
* chore: 使用私有桶的提示
* feat: tabbar support mouse wheel vertical (#5129)
* fix: form support `disabledOnInputListener` (#5127)
* fix: form submission not appropriate (#5126)
* Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev
* chore: release 5.5.1
* feat: table search form visible control (#5121)
* chore: 需要隐藏菜单
* chore: 我的待办 & 请假
* chore: 流程定义(未完成)
* chore: 流程定义(开发中)
* Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev
* Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev
* Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev
* chore: version
* chore: 锁定vxe-table版本 4.9.8版本存在样式问题
* chore: 暂时锁定cspell版本
* refactor: 由于不能输入 需要使用watch监听
* chore: https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IB7ANL
* chore: 移除冗余代码
* chore: 组件卸载时移除emitter
* fix: the route path did not synchronize with the page (#4947)
* style: typo (#4948)
* chore: 替换为commonDownloadExcel
* fix: 左边部门树错误emit导致会调用两次列表api
* chore: label样式
* chore: 改为Textarea
* chore: 滚动条宽度
* chore: 审批样式
* chore: 部门及以下或本人数据权限
* Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev
* chore: 个人中心强退设备接口路径
This commit is contained in:
玲娜贝er
2025-01-20 03:43:19 +00:00
parent 8ab0bd2212
commit 29f9f3de31
729 changed files with 12839 additions and 9132 deletions

View File

@@ -1,4 +1,3 @@
export { default as CropperImage } from './src/cropper.vue';
export { default as CropperAvatar } from './src/cropper-avatar.vue';
export { default as CropperImage } from './src/cropper.vue';
export type { Cropper } from './src/typing';

View File

@@ -1,18 +1,14 @@
<script lang="ts" setup>
import {
computed,
type CSSProperties,
type PropType,
ref,
unref,
watch,
watchEffect,
} from 'vue';
import type { ButtonProps } from 'ant-design-vue';
import type { CSSProperties, PropType } from 'vue';
import { computed, ref, unref, watch, watchEffect } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t as t } from '@vben/locales';
import { type ButtonProps, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import cropperModal from './cropper-modal.vue';

View File

@@ -1,7 +1,9 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import type { CropendResult, Cropper } from './typing';
import { type PropType, ref } from 'vue';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t as t } from '@vben/locales';

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { CSSProperties, PropType } from 'vue';
import { computed, onMounted, onUnmounted, ref, unref, useAttrs } from 'vue';
import { useDebounceFn } from '@vueuse/core';

View File

@@ -2,19 +2,11 @@
import type { CardSize } from 'ant-design-vue/es/card/Card';
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
import type { CSSProperties, PropType, Slots } from 'vue';
import type { DescInstance, DescItem, DescriptionProps } from './typing';
import {
computed,
type CSSProperties,
defineComponent,
type PropType,
ref,
type Slots,
toRefs,
unref,
useAttrs,
} from 'vue';
import { computed, defineComponent, ref, toRefs, unref, useAttrs } from 'vue';
import { Card, Descriptions } from 'ant-design-vue';
import { get, isFunction } from 'lodash-es';

View File

@@ -1,9 +1,10 @@
import type { Recordable } from '@vben/types';
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
import type { JSX } from 'vue/jsx-runtime';
import type { CSSProperties, VNode } from 'vue';
import type { Recordable } from '@vben/types';
export interface DescItem {
labelMinWidth?: number;
contentMinWidth?: number;

View File

@@ -1,4 +1,4 @@
import { type VNode } from 'vue';
import type { VNode } from 'vue';
import { Tag } from 'ant-design-vue';

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable eqeqeq -->
<script setup lang="ts">
import type { DictData } from '#/api/system/dict/dict-data-model';
@@ -17,7 +18,6 @@ const props = withDefaults(defineProps<Props>(), {
});
const color = computed<string>(() => {
// eslint-disable-next-line eqeqeq
const current = props.dicts.find((item) => item.dictValue == props.value);
const listClass = current?.listClass ?? '';
// 是否为默认的颜色
@@ -31,13 +31,11 @@ const color = computed<string>(() => {
});
const cssClass = computed<string>(() => {
// eslint-disable-next-line eqeqeq
const current = props.dicts.find((item) => item.dictValue == props.value);
return current?.cssClass ?? '';
});
const label = computed<number | string>(() => {
// eslint-disable-next-line eqeqeq
const current = props.dicts.find((item) => item.dictValue == props.value);
return current?.dictLabel ?? 'unknown';
});

View File

@@ -1 +1,2 @@
export { default as OptionsTag } from './src/options-tag.vue';
export { default as TableSwitch } from './src/table-switch.vue';

View File

@@ -0,0 +1,21 @@
<script setup lang="tsx">
import { computed } from 'vue';
import { Tag } from 'ant-design-vue';
defineOptions({ name: 'OptionsTag' });
const props = defineProps<{
options: { color?: string; label: string; value: number | string }[];
value: number | string;
}>();
const found = computed(() =>
props.options.find((item) => item.value === props.value),
);
</script>
<template>
<Tag v-if="found" :color="found.color">{{ found.label }}</Tag>
<span v-else>未知</span>
</template>

View File

@@ -2,6 +2,10 @@
import type { IPropTypes } from '@tinymce/tinymce-vue/lib/cjs/main/ts/components/EditorPropTypes';
import type { Editor as EditorType } from 'tinymce/tinymce';
import type { PropType } from 'vue';
import type { UploadResult } from '#/api/core/upload';
import {
computed,
nextTick,
@@ -9,7 +13,6 @@ import {
onBeforeUnmount,
onDeactivated,
onMounted,
type PropType,
ref,
unref,
useAttrs,
@@ -22,7 +25,7 @@ import { buildShortUUID } from '@vben/utils';
import Editor from '@tinymce/tinymce-vue';
import { isNumber } from 'lodash-es';
import { uploadApi, type UploadResult } from '#/api/core/upload';
import { uploadApi } from '#/api/core/upload';
import { bindHandlers } from './helper';
import ImgUpload from './img-upload.vue';

View File

@@ -4,9 +4,9 @@ const validEvents = new Set([
'onBeforeAddUndo',
'onBeforeExecCommand',
'onBeforeGetContent',
'onBeforePaste',
'onBeforeRenderUI',
'onBeforeSetContent',
'onBeforePaste',
'onBlur',
'onChange',
'onClearUndos',
@@ -42,8 +42,8 @@ const validEvents = new Set([
'onMouseOver',
'onMouseUp',
'onNodeChange',
'onObjectResizeStart',
'onObjectResized',
'onObjectResizeStart',
'onObjectSelected',
'onPaste',
'onPostProcess',

View File

@@ -1 +1,2 @@
export { default as MenuSelectTable } from './src/menu-select-table.vue';
export { default as TreeSelectPanel } from './src/tree-select-panel.vue';

View File

@@ -0,0 +1,85 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { ID } from '#/api/common';
import type { MenuOption } from '#/api/system/menu/model';
import { h, markRaw } from 'vue';
import { FolderIcon, MenuIcon, OkButtonIcon, VbenIcon } from '@vben/icons';
export interface Permission {
checked: boolean;
id: ID;
label: string;
}
export interface MenuPermissionOption extends MenuOption {
permissions: Permission[];
}
const menuTypes = {
C: { icon: markRaw(MenuIcon), value: '菜单' },
F: { icon: markRaw(OkButtonIcon), value: '按钮' },
M: { icon: markRaw(FolderIcon), value: '目录' },
};
export const nodeOptions = [
{ label: '节点关联', value: true },
{ label: '节点独立', value: false },
];
export const columns: VxeGridProps['columns'] = [
{
type: 'checkbox',
title: '菜单名称',
field: 'label',
treeNode: true,
headerAlign: 'left',
align: 'left',
width: 230,
},
{
title: '图标',
field: 'icon',
width: 80,
slots: {
default: ({ row }) => {
if (row?.icon === '#') {
return '';
}
return (
<span class={'flex justify-center'}>
<VbenIcon icon={row.icon} />
</span>
);
},
},
},
{
title: '类型',
field: 'menuType',
width: 80,
slots: {
default: ({ row }) => {
const current = menuTypes[row.menuType as 'C' | 'F' | 'M'];
if (!current) {
return '未知';
}
return (
<span class="flex items-center justify-center gap-1">
{h(current.icon, { class: 'size-[18px]' })}
<span>{current.value}</span>
</span>
);
},
},
},
{
title: '权限标识',
field: 'permissions',
headerAlign: 'left',
align: 'left',
slots: {
default: 'permissions',
},
},
];

View File

@@ -0,0 +1,133 @@
import type { MenuPermissionOption } from './data';
import type { useVbenVxeGrid } from '#/adapter/vxe-table';
import type { MenuOption } from '#/api/system/menu/model';
import { eachTree, treeToList } from '@vben/utils';
import { difference, isEmpty, isUndefined } from 'lodash-es';
/**
* 权限列设置是否全选
* @param record 行记录
* @param checked 是否选中
*/
export function setPermissionsChecked(
record: MenuPermissionOption,
checked: boolean,
) {
if (record?.permissions?.length > 0) {
// 全部设置为选中
record.permissions.forEach((permission) => {
permission.checked = checked;
});
}
}
/**
* 设置当前行 & 所有子节点选中状态
* @param record 行
* @param checked 是否选中
*/
export function rowAndChildrenChecked(
record: MenuPermissionOption,
checked: boolean,
) {
// 当前行选中
setPermissionsChecked(record, checked);
// 所有子节点选中
record?.children?.forEach?.((permission) => {
rowAndChildrenChecked(permission as MenuPermissionOption, checked);
});
}
/**
* void方法 会直接修改原始数据
* 将树结构转为 tree+permissions结构
* @param menus 后台返回的menu
*/
export function menusWithPermissions(menus: MenuOption[]) {
eachTree(menus, (item: MenuPermissionOption) => {
if (item.children && item.children.length > 0) {
/**
* 所有为按钮的节点提取出来
* 需要注意 这里需要过滤目录下直接是按钮的情况item.menuType !== 'M'
* 将按钮往children添加而非加到permissions
*/
const permissions = item.children.filter(
(child: MenuOption) => child.menuType === 'F' && item.menuType !== 'M',
);
// 取差集
const diffCollection = difference(item.children, permissions);
// 更新后的children 即去除按钮
item.children = diffCollection;
// permissions作为字段添加到item
const permissionsArr = permissions.map((permission) => {
return {
id: permission.id,
label: permission.label,
checked: false,
};
});
item.permissions = permissionsArr;
}
});
}
/**
* 设置表格选中
* @param checkedKeys 选中的keys
* @param menus 菜单 转换后的菜单
* @param tableApi api
* @param association 是否节点关联
*/
export function setTableChecked(
checkedKeys: (number | string)[],
menus: MenuPermissionOption[],
tableApi: ReturnType<typeof useVbenVxeGrid>['1'],
association: boolean,
) {
// tree转list
const menuList: MenuPermissionOption[] = treeToList(menus);
// 拿到勾选的行数据
let checkedRows = menuList.filter((item) => checkedKeys.includes(item.id));
/**
* 节点独立切换到节点关联 只需要最末尾的数据 即children为空
*/
if (!association) {
checkedRows = checkedRows.filter(
(item) => isUndefined(item.children) || isEmpty(item.children),
);
}
// 设置行选中 & permissions选中
checkedRows.forEach((item) => {
tableApi.grid.setCheckboxRow(item, true);
if (item?.permissions?.length > 0) {
item.permissions.forEach((permission) => {
if (checkedKeys.includes(permission.id)) {
permission.checked = true;
}
});
}
});
/**
* 节点独立切换到节点关联
* 勾选后还需要过滤权限没有任何勾选的情况 这时候取消行的勾选
*/
if (!association) {
const emptyRows = checkedRows.filter((item) => {
if (isUndefined(item.permissions) || isEmpty(item.permissions)) {
return false;
}
return item.permissions.every(
(permission) => permission.checked === false,
);
});
// 设置为不选中
tableApi.grid.setCheckboxRow(emptyRows, false);
}
}

View File

@@ -0,0 +1,62 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { TourProps } from 'ant-design-vue';
import { defineComponent, ref } from 'vue';
import { useLocalStorage } from '@vueuse/core';
import { Tour } from 'ant-design-vue';
/**
* 全屏引导
* @returns value
*/
export function useFullScreenGuide() {
const open = ref(false);
/**
* 是否已读 只显示一次
*/
const read = useLocalStorage('menu_select_fullscreen_read', false);
function openGuide() {
if (!read.value) {
open.value = true;
}
}
function closeGuide() {
open.value = false;
read.value = true;
}
const steps: TourProps['steps'] = [
{
title: '提示',
description: '点击这里可以全屏',
target: () =>
document.querySelector(
'div#menu-select-table .vxe-tools--operate > button[title="全屏"]',
)!,
},
];
const FullScreenGuide = defineComponent({
name: 'FullScreenGuide',
inheritAttrs: false,
setup() {
return () => (
<Tour
onClose={closeGuide}
open={open.value}
steps={steps}
zIndex={9999}
/>
);
},
});
return {
FullScreenGuide,
openGuide,
closeGuide,
};
}

View File

@@ -0,0 +1,412 @@
<!--
不兼容也不会兼容一些错误用法
比如: 菜单下放目录 菜单下放菜单
比如: 按钮下放目录 按钮下放菜单 按钮下放按钮
-->
<script setup lang="tsx">
import type { RadioChangeEvent } from 'ant-design-vue';
import type { MenuPermissionOption } from './data';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { MenuOption } from '#/api/system/menu/model';
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
import { cloneDeep, findGroupParentIds } from '@vben/utils';
import { Alert, Checkbox, RadioGroup, Space } from 'ant-design-vue';
import { uniq } from 'lodash-es';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { columns, nodeOptions } from './data';
import {
menusWithPermissions,
rowAndChildrenChecked,
setPermissionsChecked,
setTableChecked,
} from './helper';
import { useFullScreenGuide } from './hook';
defineOptions({
name: 'MenuSelectTable',
inheritAttrs: false,
});
const props = withDefaults(
defineProps<{
checkedKeys: (number | string)[];
defaultExpandAll?: boolean;
menus: MenuOption[];
}>(),
{
/**
* 是否默认展开全部
*/
defaultExpandAll: true,
/**
* 注意这里不是双向绑定 需要调用getCheckedKeys实例方法来获取真正选中的节点
*/
checkedKeys: () => [],
},
);
/**
* 是否节点关联
*/
const association = defineModel('association', {
type: Boolean,
default: true,
});
const gridOptions: VxeGridProps = {
checkboxConfig: {
// checkbox显示的字段
labelField: 'label',
// 是否严格模式 即节点不关联
checkStrictly: !association.value,
},
size: 'small',
columns,
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
enabled: false,
},
toolbarConfig: {
// 自定义列
custom: false,
// 最大化
zoom: true,
// 刷新
refresh: false,
},
rowConfig: {
isHover: false,
isCurrent: false,
keyField: 'id',
},
/**
* 开启虚拟滚动
* 数据量小可以选择关闭
* 如果遇到样式问题(空白、错位 滚动等)可以选择关闭虚拟滚动
*/
scrollY: {
enabled: true,
gt: 0,
},
treeConfig: {
parentField: 'parentId',
rowField: 'id',
transform: false,
},
// 溢出换行显示
showOverflow: false,
};
/**
* 用于界面显示选中的数量
*/
const checkedNum = ref(0);
/**
* 更新选中的数量
*/
function updateCheckedNumber() {
checkedNum.value = getCheckedKeys().length;
}
const [BasicTable, tableApi] = useVbenVxeGrid({
gridOptions,
gridEvents: {
// 勾选事件
checkboxChange: (params) => {
// 选中还是取消选中
const checked = params.checked;
// 行
const record = params.row;
if (association.value) {
// 节点关联
// 设置所有子节点选中状态
rowAndChildrenChecked(record, checked);
} else {
// 节点独立
// 点行会勾选/取消全部权限 点权限不会勾选行
setPermissionsChecked(record, checked);
}
updateCheckedNumber();
},
// 全选事件
checkboxAll: (params) => {
const records = params.$grid.getData();
records.forEach((item) => {
rowAndChildrenChecked(item, params.checked);
});
updateCheckedNumber();
},
},
});
/**
* 设置表格选中
* @param menus menu
* @param keys 选中的key
* @param triggerOnchange 节点独立情况 不需要触发onChange(false)
*/
function setCheckedByKeys(
menus: MenuPermissionOption[],
keys: (number | string)[],
triggerOnchange: boolean,
) {
menus.forEach((item) => {
// 设置行选中
if (keys.includes(item.id)) {
tableApi.grid.setCheckboxRow(item, true);
}
// 设置权限columns选中
if (item.permissions && item.permissions.length > 0) {
// 遍历 设置勾选
item.permissions.forEach((permission) => {
if (keys.includes(permission.id)) {
permission.checked = true;
// 手动触发onChange来选中 节点独立情况不需要处理
triggerOnchange && handlePermissionChange(item);
}
});
}
// 设置children选中
if (item.children && item.children.length > 0) {
setCheckedByKeys(item.children as any, keys, triggerOnchange);
}
});
}
const { FullScreenGuide, openGuide } = useFullScreenGuide();
onMounted(() => {
/**
* 加载表格数据 转为指定结构
*/
watch(
() => props.menus,
async (menus) => {
const clonedMenus = cloneDeep(menus);
menusWithPermissions(clonedMenus);
// console.log(clonedMenus);
await tableApi.grid.loadData(clonedMenus);
// 展开全部 默认true
if (props.defaultExpandAll) {
await nextTick();
setExpandOrCollapse(true);
}
},
);
/**
* 节点关联变动 更新表格勾选效果
*/
watch(association, (value) => {
tableApi.setGridOptions({
checkboxConfig: {
checkStrictly: !value,
},
});
});
/**
* checkedKeys依赖menus
* 要注意加载顺序
* !!!要在外部确保menus先加载!!!
*/
watch(
() => props.checkedKeys,
(value) => {
const allCheckedKeys = uniq([...value]);
// 获取表格data 如果checkedKeys在menus的watch之前触发 这里会拿到空 导致勾选异常
const records = tableApi.grid.getData();
setCheckedByKeys(records, allCheckedKeys, association.value);
updateCheckedNumber();
// 全屏引导
setTimeout(openGuide, 1000);
},
);
});
// 缓存上次(切换节点关系前)选中的keys
const lastCheckedKeys = shallowRef<(number | string)[]>([]);
/**
* 节点关联变动 事件
*/
async function handleAssociationChange(e: RadioChangeEvent) {
lastCheckedKeys.value = getCheckedKeys();
// 清空全部permissions选中
const records = tableApi.grid.getData();
records.forEach((item) => {
rowAndChildrenChecked(item, false);
});
// 需要清空全部勾选
await tableApi.grid.clearCheckboxRow();
// 滚动到顶部
await tableApi.grid.scrollTo(0, 0);
// 节点切换 不同的选中
setTableChecked(lastCheckedKeys.value, records, tableApi, !e.target.value);
updateCheckedNumber();
}
/**
* 全部展开/折叠
* @param expand 是否展开
*/
function setExpandOrCollapse(expand: boolean) {
tableApi.grid?.setAllTreeExpand(expand);
}
/**
* 权限列表 checkbox勾选的事件
* @param row 行
*/
function handlePermissionChange(row: any) {
// 节点关联
if (association.value) {
const checkedPermissions = row.permissions.filter(
(item: any) => item.checked === true,
);
// 有一条选中 则整个行选中
if (checkedPermissions.length > 0) {
tableApi.grid.setCheckboxRow(row, true);
}
// 无任何选中 则整个行不选中
if (checkedPermissions.length === 0) {
tableApi.grid.setCheckboxRow(row, false);
}
}
// 节点独立 不处理
updateCheckedNumber();
}
/**
* 获取勾选的key
* @param records 行记录列表
* @param addCurrent 是否添加当前行的id
*/
function getKeys(records: MenuPermissionOption[], addCurrent: boolean) {
const allKeys: (number | string)[] = [];
records.forEach((item) => {
// 处理children
if (item.children && item.children.length > 0) {
const keys = getKeys(item.children as MenuPermissionOption[], addCurrent);
allKeys.push(...keys);
} else {
// 当前行的id
addCurrent && allKeys.push(item.id);
// 当前行权限id 获取已经选中的
if (item.permissions && item.permissions.length > 0) {
const ids = item.permissions
.filter((m) => m.checked === true)
.map((m) => m.id);
allKeys.push(...ids);
}
}
});
return uniq(allKeys);
}
/**
* 获取选中的key
*/
function getCheckedKeys() {
// 节点关联
if (association.value) {
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
// 子节点
const nodeKeys = getKeys(records, true);
// 所有父节点
const parentIds = findGroupParentIds(props.menus, nodeKeys as number[]);
// 拼接 去重
const realKeys = uniq([...parentIds, ...nodeKeys]);
return realKeys;
}
// 节点独立
// 勾选的行
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
// 全部数据 用于获取permissions
const allRecords = tableApi?.grid?.getData?.() ?? [];
// 表格已经选中的行ids
const checkedIds = records.map((item) => item.id);
// 所有已经勾选权限的ids
const permissionIds = getKeys(allRecords, false);
// 合并 去重
const allIds = uniq([...checkedIds, ...permissionIds]);
return allIds;
}
/**
* 暴露给外部使用 获取已选中的key
*/
defineExpose({
getCheckedKeys,
});
</script>
<template>
<div class="flex h-full flex-col" id="menu-select-table">
<BasicTable>
<template #toolbar-actions>
<RadioGroup
v-model:value="association"
:options="nodeOptions"
button-style="solid"
option-type="button"
@change="handleAssociationChange"
/>
<Alert class="mx-2" type="info">
<template #message>
<div>
已选中
<span class="text-primary mx-1 font-semibold">
{{ checkedNum }}
</span>
个节点
</div>
</template>
</Alert>
</template>
<template #toolbar-tools>
<Space>
<a-button @click="setExpandOrCollapse(false)">
{{ $t('pages.common.collapse') }}
</a-button>
<a-button @click="setExpandOrCollapse(true)">
{{ $t('pages.common.expand') }}
</a-button>
</Space>
</template>
<template #permissions="{ row }">
<div class="flex flex-wrap gap-x-3 gap-y-1">
<Checkbox
v-for="permission in row.permissions"
:key="permission.id"
v-model:checked="permission.checked"
@change="() => handlePermissionChange(row)"
>
{{ permission.label }}
</Checkbox>
</div>
</template>
</BasicTable>
<!-- 全屏引导 -->
<FullScreenGuide />
</div>
</template>
<style scoped>
:deep(.ant-alert) {
padding: 4px 8px;
}
</style>

View File

@@ -3,7 +3,9 @@ import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
import type { DataNode } from 'ant-design-vue/es/tree';
import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
import { computed, nextTick, onMounted, type PropType, ref, watch } from 'vue';
import type { PropType, SetupContext } from 'vue';
import { computed, nextTick, onMounted, ref, useSlots, watch } from 'vue';
import { findGroupParentIds, treeToList } from '@vben/utils';
@@ -108,8 +110,8 @@ const stop = watch([checkedKeys, () => props.treeData], () => {
* @param info info.halfCheckedKeys为父节点的ID
*/
type CheckedState<T = number | string> =
| { checked: T[]; halfChecked: T[] }
| T[];
| T[]
| { checked: T[]; halfChecked: T[] };
function handleChecked(checkedStateKeys: CheckedState, info: CheckInfo) {
// 数组的话为节点关联
if (Array.isArray(checkedStateKeys)) {
@@ -157,6 +159,8 @@ onMounted(async () => {
expandedKeys.value = allKeys.value;
}
});
const slots = useSlots() as SetupContext['slots'];
</script>
<template>
@@ -205,7 +209,7 @@ onMounted(async () => {
@check="handleChecked"
>
<template
v-for="slotName in Object.keys($slots)"
v-for="slotName in Object.keys(slots)"
:key="slotName"
#[slotName]="data"
>

View File

@@ -2,6 +2,10 @@
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosResponse } from '@vben/request';
import type { AxiosProgressEvent } from '#/api';
import { ref, toRefs, watch } from 'vue';
import { $t } from '@vben/locales';
@@ -26,7 +30,10 @@ const props = withDefaults(
* 需自行改造 ./helper/checkFileType方法
*/
accept?: string[];
api?: (...args: any[]) => Promise<any>;
api?: (
file: Blob | File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
disabled?: boolean;
helpText?: string;
// 最大数量的文件Infinity不限制
@@ -38,6 +45,10 @@ const props = withDefaults(
// support xxx.xxx.xx
// 返回的字段 默认url
resultField?: 'fileName' | 'ossId' | 'url' | string;
/**
* 是否显示下面的描述
*/
showDescription?: boolean;
value?: string[];
}>(),
{
@@ -50,6 +61,7 @@ const props = withDefaults(
multiple: false,
api: uploadApi,
resultField: '',
showDescription: true,
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
@@ -144,12 +156,19 @@ async function customRequest(info: UploadRequestOption<any>) {
return;
}
try {
const res = await api?.(info.file);
// 进度条事件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
/**
* 由getValue处理 传对象过去
* 直接传string(id)会被转为Number
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
*/
info.onSuccess!(res);
message.success($t('component.upload.uploadSuccess'));
// 获取
const value = getValue();
isInnerOperate.value = true;
@@ -191,6 +210,7 @@ function getValue() {
:max-count="maxNumber"
:multiple="multiple"
list-type="text"
:progress="{ showInfo: true }"
@remove="handleRemove"
>
<div v-if="fileList && fileList.length < maxNumber">
@@ -199,6 +219,13 @@ function getValue() {
{{ $t('component.upload.upload') }}
</a-button>
</div>
<div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
格式文件
</div>
</Upload>
</div>
</template>

View File

@@ -2,15 +2,20 @@
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosResponse } from '@vben/request';
import type { AxiosProgressEvent } from '#/api';
import { ref, toRefs, watch } from 'vue';
import { $t } from '@vben/locales';
import { PlusOutlined } from '@ant-design/icons-vue';
import { message, Modal, Upload } from 'ant-design-vue';
import { isArray, isFunction, isObject, isString } from 'lodash-es';
import { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es';
import { uploadApi } from '#/api';
import { ossInfo } from '#/api/system/oss';
import { checkImageFileType, defaultImageAccept } from './helper';
import { UploadResultStatus } from './typing';
@@ -24,7 +29,10 @@ const props = withDefaults(
* 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
*/
accept?: string[];
api?: (...args: any[]) => Promise<any>;
api?: (
file: Blob | File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
disabled?: boolean;
helpText?: string;
// eslint-disable-next-line no-use-before-define
@@ -37,7 +45,11 @@ const props = withDefaults(
multiple?: boolean;
// support xxx.xxx.xx
// 返回的字段 默认url
resultField?: 'fileName' | 'ossId' | 'url' | string;
resultField?: 'fileName' | 'ossId' | 'url';
/**
* 是否显示下面的描述
*/
showDescription?: boolean;
value?: string | string[];
}>(),
{
@@ -50,7 +62,8 @@ const props = withDefaults(
accept: () => defaultImageAccept,
multiple: false,
api: uploadApi,
resultField: '',
resultField: 'url',
showDescription: true,
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
@@ -74,7 +87,7 @@ const isFirstRender = ref<boolean>(true);
watch(
() => props.value,
(v) => {
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
@@ -90,19 +103,40 @@ watch(
}
// 直接赋值 可能为string | string[]
value = v;
fileList.value = _fileList.map((item, i) => {
if (item && isString(item)) {
return {
uid: `${-i}`,
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: item,
};
} else if (item && isObject(item)) {
return item;
const withUrlList: UploadProps['fileList'] = [];
for (const item of _fileList) {
// ossId情况
if (props.resultField === 'ossId') {
const resp = await ossInfo([item]);
if (item && isString(item)) {
withUrlList.push({
uid: item, // ossId作为uid 方便getValue获取
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: resp?.[0]?.url,
});
} else if (item && isObject(item)) {
withUrlList.push({
...(item as any),
uid: item,
url: resp?.[0]?.url,
});
}
} else {
// 非ossId情况
if (item && isString(item)) {
withUrlList.push({
uid: uniqueId(),
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: 'done',
url: item,
});
} else if (item && isObject(item)) {
withUrlList.push(item);
}
}
return null;
}) as UploadProps['fileList'];
}
fileList.value = withUrlList;
}
if (!isFirstRender.value) {
emit('change', value);
@@ -182,12 +216,19 @@ async function customRequest(info: UploadRequestOption<any>) {
return;
}
try {
const res = await api?.(info.file);
// 进度条事件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
/**
* 由getValue处理 传对象过去
* 直接传string(id)会被转为Number
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
*/
info.onSuccess!(res);
message.success($t('component.upload.uploadSuccess'));
// 获取
const value = getValue();
isInnerOperate.value = true;
@@ -200,12 +241,17 @@ async function customRequest(info: UploadRequestOption<any>) {
}
function getValue() {
console.log(fileList.value);
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
if (item?.response && props?.resultField) {
return item?.response?.[props.resultField];
}
// ossId兼容 uid为ossId直接返回
if (props.resultField === 'ossId' && item.uid) {
return item.uid;
}
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
if (item?.url) {
return item.url;
@@ -237,6 +283,7 @@ function getValue() {
:list-type="listType"
:max-count="maxNumber"
:multiple="multiple"
:progress="{ showInfo: true }"
@preview="handlePreview"
@remove="handleRemove"
>
@@ -245,6 +292,16 @@ function getValue() {
<div style="margin-top: 8px">{{ $t('component.upload.upload') }}</div>
</div>
</Upload>
<div
v-if="showDescription"
class="mt-2 flex flex-wrap items-center text-[14px]"
>
请上传不超过
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
格式文件
</div>
<Modal
:footer="null"
:open="previewOpen"

View File

@@ -15,7 +15,7 @@ export interface FileItem {
percent: number;
file: File;
status?: UploadResultStatus;
response?: { fileName: string; ossId: string; url: string } | Recordable<any>;
response?: Recordable<any> | { fileName: string; ossId: string; url: string };
uuid: string;
}

View File

@@ -1,6 +1,7 @@
import { computed, unref } from 'vue';
import type { Ref } from 'vue';
import { computed, unref } from 'vue';
import { $t } from '@vben/locales';
export function useUploadType({