!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,6 +1,8 @@
<script lang="ts" setup>
import type { LoginCodeParams, VbenFormSchema } from '@vben/common-ui';
import type { TenantResp } from '#/api';
import { computed, onMounted, ref, useTemplateRef } from 'vue';
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
@@ -8,13 +10,14 @@ import { $t } from '@vben/locales';
import { Alert, message } from 'ant-design-vue';
import { tenantList, type TenantResp } from '#/api';
import { tenantList } from '#/api';
import { sendSmsCode } from '#/api/core/captcha';
import { useAuthStore } from '#/store';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const CODE_LENGTH = 6;
const tenantInfo = ref<TenantResp>({
tenantEnabled: false,
@@ -98,7 +101,9 @@ const formSchema = computed((): VbenFormSchema[] => {
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
rules: z.string().length(CODE_LENGTH, {
message: $t('authentication.codeTip', [CODE_LENGTH]),
}),
},
];
});

View File

@@ -1,6 +1,9 @@
<script lang="ts" setup>
import type { LoginAndRegisterParams, VbenFormSchema } from '@vben/common-ui';
import type { TenantResp } from '#/api';
import type { CaptchaResponse } from '#/api/core/captcha';
import { computed, onMounted, ref, useTemplateRef } from 'vue';
import { AuthenticationLogin, z } from '@vben/common-ui';
@@ -8,8 +11,8 @@ import { $t } from '@vben/locales';
import { omit } from 'lodash-es';
import { tenantList, type TenantResp } from '#/api';
import { captchaImage, type CaptchaResponse } from '#/api/core/captcha';
import { tenantList } from '#/api';
import { captchaImage } from '#/api/core/captcha';
import { useAuthStore } from '#/store';
import OAuthLogin from './oauth-login.vue';

View File

@@ -1,7 +1,11 @@
<script setup lang="tsx">
import type { VxeGridProps } from '@vben/plugins/vxe-table';
import type { BindItem } from '../../oauth-common';
import { computed, ref, unref } from 'vue';
import { useVbenVxeGrid, type VxeGridProps } from '@vben/plugins/vxe-table';
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
import {
Alert,
@@ -16,7 +20,7 @@ import {
import { authUnbinding } from '#/api';
import { socialList } from '#/api/system/social';
import { accountBindList, type BindItem } from '../../oauth-common';
import { accountBindList } from '../../oauth-common';
/**
* 没有传递action事件则不支持绑定 弹出默认提示
@@ -103,7 +107,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
isCurrent: false,
keyField: 'id',
},

View File

@@ -1,16 +1,20 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Popconfirm } from 'ant-design-vue';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { forceLogout2, onlineDeviceList } from '#/api/monitor/online';
import { columns } from '#/views/monitor/online/data';
const gridOptions: VxeGridProps = {
columns,
keepSource: true,
pagerConfig: {},
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async () => {
@@ -19,7 +23,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'tokenId',
},
};

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { AuthApi } from '#/api';
import { onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
@@ -7,7 +9,7 @@ import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { type AuthApi, authCallback } from '#/api';
import { authCallback } from '#/api';
import { useAuthStore } from '#/store';
import { accountBindList } from '../oauth-common';

View File

@@ -1,18 +0,0 @@
import { defineComponent } from 'vue';
import { Fallback } from '@vben/common-ui';
export default defineComponent({
name: 'CommonSkeleton',
setup() {
return () => (
<div class="flex h-[600px] w-full items-center justify-center">
<Fallback
description="等待后端重构工作流后开发"
status="coming-soon"
title="等待开发"
/>
</div>
);
},
});

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,11 +1,8 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { onMounted, ref } from 'vue';
const chartRef = ref<EchartsUIType>();
const { renderEcharts } = useEcharts(chartRef);

View File

@@ -1,19 +1,18 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenModal } from '@vben/common-ui';
import { getPopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
tableCheckboxEvent,
useVbenVxeGrid,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { downloadExcel } from '#/utils/file/download';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import { commonDownloadExcel } from '#/utils/file/download';
import { demoExport, demoList, demoRemove } from './api';
import { columns, querySchema } from './data';
@@ -61,10 +60,6 @@ const checked = ref(false);
const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
checkboxChange: tableCheckboxEvent(checked),
checkboxAll: tableCheckboxEvent(checked),
},
});
const [DemoModal, modalApi] = useVbenModal({
@@ -113,7 +108,7 @@ function handleMultiDelete() {
<a-button
v-access:code="['system:demo:export']"
@click="
downloadExcel(
commonDownloadExcel(
demoExport,
'测试单数据',
tableApi.formApi.form.values,
@@ -123,7 +118,7 @@ function handleMultiDelete() {
{{ $t('pages.common.export') }}
</a-button>
<a-button
:disabled="!checked"
:disabled="!vxeCheckboxChecked(tableApi)"
danger
type="primary"
v-access:code="['system:demo:remove']"

View File

@@ -1,14 +1,17 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { nextTick } from 'vue';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenModal } from '@vben/common-ui';
import { getPopupContainer, listToTree } from '@vben/utils';
import { Popconfirm, Space } from 'ant-design-vue';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { treeList, treeRemove } from './api';
import { columns, querySchema } from './data';
@@ -51,7 +54,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'id',
},

View File

@@ -1,66 +0,0 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button, Card, message, notification, Space } from 'ant-design-vue';
type NotificationType = 'error' | 'info' | 'success' | 'warning';
function info() {
message.info('How many roads must a man walk down');
}
function error() {
message.error({
content: 'Once upon a time you dressed so fine',
duration: 2500,
});
}
function warning() {
message.warning('How many roads must a man walk down');
}
function success() {
message.success('Cause you walked hand in hand With another man in my place');
}
function notify(type: NotificationType) {
notification[type]({
duration: 2500,
message: '说点啥呢',
type,
});
}
</script>
<template>
<Page
description="支持多语言,主题功能集成切换等"
title="Ant Design Vue组件使用演示"
>
<Card class="mb-5" title="按钮">
<Space>
<Button>Default</Button>
<Button type="primary"> Primary </Button>
<Button> Info </Button>
<Button danger> Error </Button>
</Space>
</Card>
<Card class="mb-5" title="Message">
<Space>
<Button @click="info"> 信息 </Button>
<Button danger @click="error"> 错误 </Button>
<Button @click="warning"> 警告 </Button>
<Button @click="success"> 成功 </Button>
</Space>
</Card>
<Card class="mb-5" title="Notification">
<Space>
<Button @click="notify('info')"> 信息 </Button>
<Button danger @click="notify('error')"> 错误 </Button>
<Button @click="notify('warning')"> 警告 </Button>
<Button @click="notify('success')"> 成功 </Button>
</Space>
</Card>
</Page>
</template>

View File

@@ -1,20 +1,18 @@
<script lang="ts">
import type { EChartsOption } from 'echarts';
import type { PropType } from 'vue';
import type { EchartsUIType } from '@vben/plugins/echarts';
import { defineComponent, onActivated, onMounted, ref, watch } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
export default defineComponent({
components: { EchartsUI },
props: {
data: {
default: () => [],
type: Array,
type: Array as PropType<{ name: string; value: string }[]>,
},
},
setup(props, { expose }) {
@@ -41,6 +39,7 @@ export default defineComponent({
*/
onActivated(() => resize(false));
type EChartsOption = Parameters<typeof renderEcharts>['0'];
function setEchartsOption(data: any[]) {
const option: EChartsOption = {
series: [

View File

@@ -1,13 +1,9 @@
<script lang="ts">
import type { EChartsOption } from 'echarts';
import type { EchartsUIType } from '@vben/plugins/echarts';
import { defineComponent, onActivated, onMounted, ref, watch } from 'vue';
import {
EchartsUI,
type EchartsUIType,
useEcharts,
} from '@vben/plugins/echarts';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
export default defineComponent({
components: { EchartsUI },
@@ -38,6 +34,13 @@ export default defineComponent({
// 从其他页面切换回来会有一个奇怪的动画效果 需要调用resize
onActivated(resize);
/**
* 获取最近的十的幂次
* 该函数用于寻找大于给定数字num的最近的10的幂次
* 主要解决的问题是确定一个数附近较大的十的幂次,这在某些算法中很有用
*
* @param num {number} 输入的数字,用于寻找最近的十的幂次
*/
function getNearestPowerOfTen(num: number) {
let power = 10;
while (power <= num) {
@@ -46,6 +49,7 @@ export default defineComponent({
return power;
}
type EChartsOption = Parameters<typeof renderEcharts>['0'];
function setEchartsOption(value: string) {
// x10
const formattedValue = Math.floor(Number.parseFloat(value));

View File

@@ -1,13 +1,12 @@
<script setup lang="ts">
import type { PropType } from 'vue';
import type { RedisInfo } from '#/api/monitor/cache';
import type { DescItem } from '#/components/description';
import { onMounted, type PropType, watch } from 'vue';
import { onMounted, watch } from 'vue';
import {
type DescItem,
Description,
useDescription,
} from '#/components/description';
import { Description, useDescription } from '#/components/description';
interface IRedisInfo extends RedisInfo {
dbSize: string;

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { RedisInfo } from '#/api/monitor/cache';
import { onMounted, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
@@ -6,13 +8,16 @@ import { CommandLineIcon, MemoryIcon, RedisIcon } from '@vben/icons';
import { Button, Card, Col, Row } from 'ant-design-vue';
import { redisCacheInfo, type RedisInfo } from '#/api/monitor/cache';
import { redisCacheInfo } from '#/api/monitor/cache';
import { CommandChart, MemoryChart, RedisDescription } from './components';
const baseSpan = { lg: 12, md: 24, sm: 24, xl: 12, xs: 24 };
const chartData = reactive<{ command: any[]; memory: string }>({
const chartData = reactive<{
command: { name: string; value: string }[];
memory: string;
}>({
command: [],
memory: '0',
});
@@ -39,6 +44,7 @@ async function loadInfo() {
chartData.memory = usedMemory;
// 命令统计
chartData.command = ret.commandStats;
console.log(chartData.command);
// redis信息
redisInfo.value = { ...ret.info, dbSize: String(ret.dbSize) };
} catch (error) {

View File

@@ -1,11 +1,11 @@
import type { VNode } from 'vue';
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { DescItem } from '#/components/description';
import type { VNode } from 'vue';
import { DictEnum } from '@vben/constants';
import { type FormSchemaGetter } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
import { renderBrowserIcon, renderDict, renderOsIcon } from '#/utils/render';
@@ -68,7 +68,9 @@ export const columns: VxeGridProps['columns'] = [
field: 'os',
slots: {
default: ({ row }) => {
// Windows 10 or Windows Server 2016 太长了 分割一下 详情依旧能看到详细的
/**
* Windows 10 or Windows Server 2016 太长了 分割一下 详情依旧能看到详细的
*/
let value = row.os;
if (value) {
const split = value.split(' or ');

View File

@@ -1,19 +1,17 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { LoginLog } from '#/api/monitor/logininfo/model';
import { ref } from 'vue';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridDefines,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
loginInfoClean,
loginInfoExport,
@@ -71,7 +69,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'infoId',
},
id: 'monitor-logininfo-index',
@@ -82,9 +79,9 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
checkboxChange: (e: VxeGridDefines.CheckboxChangeEventParams) => {
checkboxChange: (e) => {
const records = e.$grid.getCheckboxRecords();
canUnlock.value = records.length === 1 && records[0]?.status === '1';
canUnlock.value = records.length === 1 && records[0]!.status === '1';
},
},
});
@@ -93,7 +90,7 @@ const [LoginInfoModal, modalApi] = useVbenModal({
connectedComponent: loginInfoModal,
});
function handlePreview(record: Recordable<any>) {
function handlePreview(record: LoginLog) {
modalApi.setData(record);
modalApi.open();
}
@@ -107,14 +104,14 @@ function handleClear() {
});
}
async function handleDelete(row: Recordable<any>) {
await loginInfoRemove(row.infoId);
async function handleDelete(row: LoginLog) {
await loginInfoRemove([row.infoId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.infoId);
const ids = rows.map((row: LoginLog) => row.infoId);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,10 +1,10 @@
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { VNode } from 'vue';
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import dayjs from 'dayjs';
import { type FormSchemaGetter } from '#/adapter/form';
import { renderBrowserIcon, renderOsIcon } from '#/utils/render';
export const querySchema: FormSchemaGetter = () => [

View File

@@ -1,12 +1,17 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import { Page, type VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { OnlineUser } from '#/api/monitor/online/model';
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Popconfirm } from 'ant-design-vue';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { forceLogout, onlineList } from '#/api/monitor/online';
import { columns, querySchema } from './data';
@@ -22,24 +27,30 @@ const formOptions: VbenFormProps = {
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
};
const onlineCount = ref(0);
const gridOptions: VxeGridProps = {
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await onlineList({
pageNum: page.currentPage,
pageSize: page.pageSize,
query: async (_, formValues = {}) => {
const resp = await onlineList({
...formValues,
});
onlineCount.value = resp.total;
return resp;
},
},
},
scrollY: {
enabled: true,
gt: 0,
},
rowConfig: {
isHover: true,
keyField: 'tokenId',
},
id: 'monitor-online-index',
@@ -47,7 +58,7 @@ const gridOptions: VxeGridProps = {
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });
async function handleForceOffline(row: Recordable<any>) {
async function handleForceOffline(row: OnlineUser) {
await forceLogout(row.tokenId);
await tableApi.query();
}
@@ -55,7 +66,16 @@ async function handleForceOffline(row: Recordable<any>) {
<template>
<Page :auto-content-height="true">
<BasicTable table-title="在线用户列表">
<BasicTable>
<template #toolbar-actions>
<div class="mr-1 pl-1 text-[1rem]">
<div>
在线用户列表 (
<span class="text-primary font-bold">{{ onlineCount }}</span>
人在线)
</div>
</div>
</template>
<template #action="{ row }">
<Popconfirm
:get-popup-container="getVxePopupContainer"

View File

@@ -1,3 +1,4 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { DescItem } from '#/components/description';
@@ -5,7 +6,6 @@ import { DictEnum } from '@vben/constants';
import { Tag } from 'ant-design-vue';
import { type FormSchemaGetter } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
import {
renderDict,

View File

@@ -1,18 +1,19 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { PageQuery } from '#/api/common';
import type { OperationLog } from '#/api/monitor/operlog/model';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { Modal, Space } from 'ant-design-vue';
import {
addSortParams,
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
vxeSortEvent,
} from '#/adapter/vxe-table';
import {
operLogClean,
@@ -60,18 +61,19 @@ const gridOptions: VxeGridProps<OperationLog> = {
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
const params: any = {
query: async ({ page, sorts }, formValues = {}) => {
const params: PageQuery = {
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
};
// 添加排序参数
addSortParams(params, sorts);
return await operLogList(params);
},
},
},
rowConfig: {
isHover: true,
keyField: 'operId',
},
sortConfig: {
@@ -87,7 +89,8 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams),
// 排序 重新请求接口
sortChange: () => tableApi.query(),
},
});
@@ -99,7 +102,7 @@ const [OperationPreviewDrawer, drawerApi] = useVbenDrawer({
* 预览
* @param record 操作日志记录
*/
function handlePreview(record: Recordable<any>) {
function handlePreview(record: OperationLog) {
drawerApi.setData({ record });
drawerApi.open();
}
@@ -120,7 +123,7 @@ function handleClear() {
*/
async function handleDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.operId);
const ids = rows.map((row: OperationLog) => row.operId);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { OperationLog } from '#/api/monitor/operlog/model';
import { useVbenDrawer } from '@vben/common-ui';
@@ -20,7 +20,7 @@ function handleOpenChange(open: boolean) {
if (!open) {
return null;
}
const { record } = drawerApi.getData() as { record: Recordable<any> };
const { record } = drawerApi.getData() as { record: OperationLog };
setDescProps({ data: record }, true);
}
</script>

View File

@@ -1,10 +1,10 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { type FormSchemaGetter } from '#/adapter/form';
import { getDict, getDictOptions } from '#/utils/dict';
import { getDictOptions } from '#/utils/dict';
import { renderDict, renderDictTags } from '#/utils/render';
export const querySchema: FormSchemaGetter = () => [
@@ -53,7 +53,9 @@ export const columns: VxeGridProps['columns'] = [
}
return renderDictTags(
row.grantTypeList,
getDict(DictEnum.SYS_GRANT_TYPE),
getDictOptions(DictEnum.SYS_GRANT_TYPE),
true,
4,
);
},
},

View File

@@ -1,17 +1,16 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { Client } from '#/api/system/client/model';
import { useAccess } from '@vben/access';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
clientChangeStatus,
clientExport,
@@ -43,7 +42,7 @@ const gridOptions: VxeGridProps = {
reserve: true,
// 点击行选中
// trigger: 'row',
checkMethod: (row: any) => row?.id !== 1,
checkMethod: ({ row }) => (row as Client)?.id !== 1,
},
columns,
height: 'auto',
@@ -61,11 +60,10 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'id',
height: 90,
},
id: 'system-client-index',
showOverflow: false,
};
const [BasicTable, tableApi] = useVbenVxeGrid({
@@ -82,19 +80,19 @@ function handleAdd() {
drawerApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: Client) {
drawerApi.setData({ id: record.id });
drawerApi.open();
}
async function handleDelete(row: Recordable<any>) {
await clientRemove(row.id);
async function handleDelete(row: Client) {
await clientRemove([row.id]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.id);
const ids = rows.map((row: Client) => row.id);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,9 +1,9 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { type FormSchemaGetter } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
import { renderDict } from '#/utils/render';
@@ -102,6 +102,9 @@ export const modalSchema: FormSchemaGetter = () => [
formItemClass: 'items-baseline',
fieldName: 'configValue',
label: '参数键值',
componentProps: {
autosize: true,
},
rules: 'required',
},
{

View File

@@ -1,16 +1,15 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { SysConfig } from '#/api/system/config/model';
import { Page, useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
configExport,
configList,
@@ -64,7 +63,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'configId',
},
id: 'system-config-index',
@@ -83,19 +81,19 @@ function handleAdd() {
modalApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: SysConfig) {
modalApi.setData({ id: record.configId });
modalApi.open();
}
async function handleDelete(row: Recordable<any>) {
await configRemove(row.configId);
async function handleDelete(row: SysConfig) {
await configRemove([row.configId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.configId);
const ids = rows.map((row: SysConfig) => row.configId);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,9 +1,10 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { type FormSchemaGetter, z } from '#/adapter/form';
import { z } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
import { renderDict } from '#/utils/render';

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { Dept } from '#/api/system/dept/model';
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
@@ -43,7 +45,7 @@ const [BasicForm, formApi] = useVbenForm({
});
async function getDeptTree(deptId?: number | string, exclude = false) {
let ret: any[] = [];
let ret: Dept[] = [];
ret = await (!deptId || exclude ? deptList({}) : deptNodeList(deptId));
const treeData = listToTree(ret, { id: 'deptId', pid: 'parentId' });
// 添加部门名称 如 xx-xx-xx

View File

@@ -1,14 +1,17 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { Dept } from '#/api/system/dept/model';
import { nextTick } from 'vue';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { eachTree, getVxePopupContainer } from '@vben/utils';
import { Popconfirm, Space } from 'ant-design-vue';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { deptList, deptRemove } from '#/api/system/dept';
import { columns, querySchema } from './data';
@@ -59,7 +62,6 @@ const gridOptions: VxeGridProps = {
gt: 0,
},
rowConfig: {
isHover: true,
keyField: 'deptId',
},
treeConfig: {
@@ -74,7 +76,7 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
cellDblclick: (e: any) => {
cellDblclick: (e) => {
const { row = {} } = e;
if (!row?.children) {
return;
@@ -84,7 +86,7 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
row.expand = !isExpanded;
},
// 需要监听使用箭头展开的情况 否则展开/折叠的数据不一致
toggleTreeExpand: (e: any) => {
toggleTreeExpand: (e) => {
const { row = {}, expanded } = e;
row.expand = expanded;
},
@@ -99,18 +101,18 @@ function handleAdd() {
drawerApi.open();
}
function handleSubAdd(row: Recordable<any>) {
function handleSubAdd(row: Dept) {
const { deptId } = row;
drawerApi.setData({ id: deptId, update: false });
drawerApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: Dept) {
drawerApi.setData({ id: record.deptId, update: true });
drawerApi.open();
}
async function handleDelete(row: Recordable<any>) {
async function handleDelete(row: Dept) {
await deptRemove(row.deptId);
await tableApi.query();
}

View File

@@ -1,6 +1,7 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { DictData } from '#/api/system/dict/dict-data-model';
import { type FormSchemaGetter } from '#/adapter/form';
import { renderDictTag } from '#/utils/render';
export const querySchema: FormSchemaGetter = () => [
@@ -18,8 +19,8 @@ export const columns: VxeGridProps['columns'] = [
field: 'cssClass',
slots: {
default: ({ row }) => {
const { dictValue } = row;
return renderDictTag(dictValue, [row as any]);
const { dictValue } = row as DictData;
return renderDictTag(dictValue, [row]);
},
},
},

View File

@@ -1,18 +1,18 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { PageQuery } from '#/api/common';
import type { DictData } from '#/api/system/dict/dict-data-model';
import { ref } from 'vue';
import { useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
dictDataExport,
dictDataList,
@@ -53,7 +53,7 @@ const gridOptions: VxeGridProps = {
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
const params: any = {
const params: PageQuery = {
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
@@ -67,7 +67,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'dictCode',
},
id: 'system-dict-data-index',
@@ -87,7 +86,7 @@ function handleAdd() {
drawerApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: DictData) {
drawerApi.setData({
dictType: dictType.value,
dictCode: record.dictCode,
@@ -95,14 +94,14 @@ async function handleEdit(record: Recordable<any>) {
drawerApi.open();
}
async function handleDelete(row: Recordable<any>) {
await dictDataRemove(row.dictCode);
async function handleDelete(row: DictData) {
await dictDataRemove([row.dictCode]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.dictCode);
const ids = rows.map((row: DictData) => row.dictCode);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,12 +1,11 @@
<script setup lang="ts">
import { computed, type PropType } from 'vue';
import type { RadioChangeEvent } from 'ant-design-vue';
import {
Input,
type RadioChangeEvent,
RadioGroup,
Select,
} from 'ant-design-vue';
import type { PropType } from 'vue';
import { computed } from 'vue';
import { Input, RadioGroup, Select } from 'ant-design-vue';
import { tagSelectOptions } from '#/components/dict';

View File

@@ -1,6 +1,7 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { type FormSchemaGetter, z } from '#/adapter/form';
import { z } from '#/adapter/form';
export const querySchema: FormSchemaGetter = () => [
{

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { DictType } from '#/api/system/dict/dict-type-model';
import { ref } from 'vue';
import { useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
dictTypeExport,
dictTypeList,
@@ -61,7 +60,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'dictId',
},
id: 'system-dict-type-index',
@@ -73,7 +71,7 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
cellClick: (e: any) => {
cellClick: (e) => {
const { row } = e;
if (lastDictType.value === row.dictType) {
return;
@@ -92,19 +90,19 @@ function handleAdd() {
modalApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: DictType) {
modalApi.setData({ id: record.dictId });
modalApi.open();
}
async function handleDelete(row: Recordable<any>) {
await dictTypeRemove(row.dictId);
async function handleDelete(row: DictType) {
await dictTypeRemove([row.dictId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.dictId);
const ids = rows.map((row: DictType) => row.dictId);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,3 +1,4 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { h } from 'vue';
@@ -7,7 +8,7 @@ import { FolderIcon, MenuIcon, OkButtonIcon, VbenIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { getPopupContainer } from '@vben/utils';
import { type FormSchemaGetter, z } from '#/adapter/form';
import { z } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
import { renderDict } from '#/utils/render';

View File

@@ -1,16 +1,18 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { Menu } from '#/api/system/menu/model';
import { computed } from 'vue';
import { useAccess } from '@vben/access';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { Fallback } from '@vben/common-ui';
import { Fallback, Page, useVbenDrawer } from '@vben/common-ui';
import { eachTree, getVxePopupContainer } from '@vben/utils';
import { Popconfirm, Space } from 'ant-design-vue';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { menuList, menuRemove } from '#/api/system/menu';
import { columns, querySchema } from './data';
@@ -49,7 +51,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'menuId',
},
/**
@@ -74,7 +75,7 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
cellDblclick: (e: any) => {
cellDblclick: (e) => {
const { row = {} } = e;
if (!row?.children) {
return;
@@ -84,7 +85,7 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
row.expand = !isExpanded;
},
// 需要监听使用箭头展开的情况 否则展开/折叠的数据不一致
toggleTreeExpand: (e: any) => {
toggleTreeExpand: (e) => {
const { row = {}, expanded } = e;
row.expand = expanded;
},
@@ -99,19 +100,19 @@ function handleAdd() {
drawerApi.open();
}
function handleSubAdd(row: Recordable<any>) {
function handleSubAdd(row: Menu) {
const { menuId } = row;
drawerApi.setData({ id: menuId, update: false });
drawerApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: Menu) {
drawerApi.setData({ id: record.menuId, update: true });
drawerApi.open();
}
async function handleDelete(row: Recordable<any>) {
await menuRemove(row.menuId);
async function handleDelete(row: Menu) {
await menuRemove([row.menuId]);
await tableApi.query();
}

View File

@@ -48,7 +48,12 @@ async function setupMenuSelect() {
item.menuName = $t(item.menuName);
});
// const folderArray = menuArray.filter((item) => item.menuType === 'M');
const menuTree = listToTree(menuArray, { id: 'menuId', pid: 'parentId' });
/**
* 这里需要过滤掉按钮类型
* 不允许在按钮下添加数据
*/
const filteredList = menuArray.filter((item) => item.menuType !== 'F');
const menuTree = listToTree(filteredList, { id: 'menuId', pid: 'parentId' });
const fullMenuTree = [
{
menuId: 0,

View File

@@ -1,9 +1,9 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { type FormSchemaGetter } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
import { renderDict } from '#/utils/render';

View File

@@ -1,16 +1,15 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { Notice } from '#/api/system/notice/model';
import { Page, useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import { noticeList, noticeRemove } from '#/api/system/notice';
import { columns, querySchema } from './data';
@@ -52,7 +51,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'noticeId',
},
id: 'system-notice-index',
@@ -72,19 +70,19 @@ function handleAdd() {
modalApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: Notice) {
modalApi.setData({ id: record.noticeId });
modalApi.open();
}
async function handleDelete(row: Recordable<any>) {
await noticeRemove(row.noticeId);
async function handleDelete(row: Notice) {
await noticeRemove([row.noticeId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.noticeId);
const ids = rows.map((row: Notice) => row.noticeId);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,10 +1,11 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { Tag } from 'ant-design-vue';
import { type FormSchemaGetter, z } from '#/adapter/form';
import { z } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
const accessPolicyOptions = [
@@ -128,6 +129,12 @@ export const drawerSchema: FormSchemaGetter = () => [
fieldName: 'domain',
label: '自定义域名',
},
{
component: 'Input',
fieldName: 'tip',
label: '占位作为提示使用',
hideLabel: true,
},
{
component: 'Divider',
componentProps: {

View File

@@ -1,17 +1,16 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { OssConfig } from '#/api/system/oss-config/model';
import { useAccess } from '@vben/access';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
ossConfigChangeStatus,
ossConfigList,
@@ -58,7 +57,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'ossConfigId',
},
id: 'system-oss-config-index',
@@ -78,19 +76,19 @@ function handleAdd() {
drawerApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: OssConfig) {
drawerApi.setData({ id: record.ossConfigId });
drawerApi.open();
}
async function handleDelete(row: Recordable<any>) {
await ossConfigRemove(row.ossConfigId);
async function handleDelete(row: OssConfig) {
await ossConfigRemove([row.ossConfigId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.ossConfigId);
const ids = rows.map((row: OssConfig) => row.ossConfigId);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -5,6 +5,8 @@ import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { Alert } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
ossConfigAdd,
@@ -79,7 +81,17 @@ async function handleCancel() {
<template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[650px]">
<BasicForm />
<BasicForm>
<template #tip>
<div class="ml-7 w-full">
<Alert
message="私有桶使用自定义域名无法预览, 但可以正常上传/下载"
show-icon
type="warning"
/>
</div>
</template>
</BasicForm>
</BasicDrawer>
</template>

View File

@@ -1,7 +1,6 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { type FormSchemaGetter } from '#/adapter/form';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
@@ -73,3 +72,9 @@ export const columns: VxeGridProps['columns'] = [
width: 180,
},
];
/**
* 图片加载失败的fallback
*/
export const fallbackImageBase64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg==';

View File

@@ -45,6 +45,7 @@ const message = computed(() => {
v-model:value="fileList"
:accept="accept"
:max-number="maxNumber"
:max-size="5"
/>
</div>
</BasicModal>

View File

@@ -1,10 +1,14 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { PageQuery } from '#/api/common';
import type { OssFile } from '#/api/system/oss/model';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { getVxePopupContainer } from '@vben/utils';
@@ -14,21 +18,22 @@ import {
Modal,
Popconfirm,
Space,
Spin,
Switch,
Tooltip,
} from 'ant-design-vue';
import {
addSortParams,
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
vxeSortEvent,
} from '#/adapter/vxe-table';
import { configInfoByKey } from '#/api/system/config';
import { ossDownload, ossList, ossRemove } from '#/api/system/oss';
import { calculateFileSize } from '#/utils/file';
import { downloadByData } from '#/utils/file/download';
import { columns, querySchema } from './data';
import { columns, fallbackImageBase64, querySchema } from './data';
import fileUploadModal from './file-upload-modal.vue';
import imageUploadModal from './image-upload-modal.vue';
@@ -66,18 +71,19 @@ const gridOptions: VxeGridProps = {
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
const params: any = {
query: async ({ page, sorts }, formValues = {}) => {
const params: PageQuery = {
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
};
// 添加排序参数
addSortParams(params, sorts);
return await ossList(params);
},
},
},
rowConfig: {
isHover: true,
keyField: 'ossId',
height: 65,
},
@@ -94,28 +100,42 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams),
// 排序 重新请求接口
sortChange: () => tableApi.query(),
},
});
async function handleDownload(row: Recordable<any>) {
const hideLoading = message.loading($t('pages.common.downloadLoading'), 0);
async function handleDownload(row: OssFile) {
const downloadSize = ref($t('pages.common.downloadLoading'));
const hideLoading = message.loading({
content: () => downloadSize.value,
duration: 0,
});
try {
const data = await ossDownload(row.ossId);
const data = await ossDownload(row.ossId, (e) => {
// 计算下载进度
const percent = Math.floor((e.loaded / e.total!) * 100);
// 已经下载
const current = calculateFileSize(e.loaded);
// 总大小
const total = calculateFileSize(e.total!);
downloadSize.value = `已下载: ${current}/${total} (${percent}%)`;
});
downloadByData(data, row.originalName);
message.success('下载完成');
} finally {
hideLoading();
}
}
async function handleDelete(row: Recordable<any>) {
await ossRemove(row.ossId);
async function handleDelete(row: OssFile) {
await ossRemove([row.ossId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.ossId);
const ids = rows.map((row: OssFile) => row.ossId);
Modal.confirm({
title: '提示',
okType: 'danger',
@@ -134,8 +154,8 @@ function handleToSettings() {
const preview = ref(false);
onMounted(async () => {
const resp = await configInfoByKey('sys.oss.previewListResource');
preview.value = Boolean(resp);
const previewStr = await configInfoByKey('sys.oss.previewListResource');
preview.value = previewStr === 'true';
});
function isImageFile(ext: string) {
@@ -189,11 +209,22 @@ const [FileUploadModal, fileUploadApi] = useVbenModal({
</Space>
</template>
<template #url="{ row }">
<!-- placeholder为图片未加载时显示的占位图 -->
<!-- fallback为图片加载失败时显示 -->
<!-- 需要设置key属性 否则切换翻页会有延迟 -->
<Image
:key="row.ossId"
v-if="preview && isImageFile(row.url)"
:src="row.url"
height="50px"
/>
:fallback="fallbackImageBase64"
>
<template #placeholder>
<div class="flex size-full items-center justify-center">
<Spin />
</div>
</template>
</Image>
<span v-else>{{ row.url }}</span>
</template>
<template #action="{ row }">

View File

@@ -1,9 +1,9 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { type FormSchemaGetter } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
import { renderDict } from '#/utils/render';

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { Post } from '#/api/system/post/model';
import { ref } from 'vue';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import { postExport, postList, postRemove } from '#/api/system/post';
import { commonDownloadExcel } from '#/utils/file/download';
import DeptTree from '#/views/system/user/dept-tree.vue';
@@ -73,7 +72,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'postId',
},
id: 'system-post-index',
@@ -93,19 +91,19 @@ function handleAdd() {
drawerApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: Post) {
drawerApi.setData({ id: record.postId });
drawerApi.open();
}
async function handleDelete(row: Recordable<any>) {
await postRemove(row.postId);
async function handleDelete(row: Post) {
await postRemove([row.postId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.postId);
const ids = rows.map((row: Post) => row.postId);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,7 +1,6 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { type FormSchemaGetter } from '#/adapter/form';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { User } from '#/api/system/user/model';
import { useRoute } from 'vue-router';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
roleAllocatedList,
roleAuthCancel,
@@ -62,7 +61,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'userId',
},
id: 'system-role-assign-index',
@@ -85,7 +83,7 @@ function handleAdd() {
/**
* 取消授权 一条记录
*/
async function handleAuthCancel(record: Recordable<any>) {
async function handleAuthCancel(record: User) {
await roleAuthCancel({ userId: record.userId, roleId });
await tableApi.query();
}
@@ -95,7 +93,7 @@ async function handleAuthCancel(record: Recordable<any>) {
*/
function handleMultipleAuthCancel() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.userId);
const ids = rows.map((row: User) => row.userId);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,9 +1,13 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { useRoute } from 'vue-router';
import { useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { useVbenDrawer } from '@vben/common-ui';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { roleSelectAll, roleUnallocatedList } from '#/api/system/role';
import { columns, querySchema } from './data';
@@ -52,7 +56,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'userId',
},
};

View File

@@ -1,3 +1,4 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
@@ -5,7 +6,6 @@ import { getPopupContainer } from '@vben/utils';
import { Tag } from 'ant-design-vue';
import { type FormSchemaGetter } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
/**
@@ -17,6 +17,7 @@ export const authScopeOptions = [
{ color: 'orange', label: '本部门数据权限', value: '3' },
{ color: 'cyan', label: '本部门及以下数据权限', value: '4' },
{ color: 'error', label: '仅本人数据权限', value: '5' },
{ color: 'default', label: '部门及以下或本人数据权限', value: '6' },
];
export const querySchema: FormSchemaGetter = () => [

View File

@@ -1,16 +1,14 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { Role } from '#/api/system/role/model';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useAccess } from '@vben/access';
import {
Page,
useVbenDrawer,
useVbenModal,
type VbenFormProps,
} from '@vben/common-ui';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import {
@@ -22,11 +20,7 @@ import {
Space,
} from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
roleChangeStatus,
roleExport,
@@ -85,7 +79,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'roleId',
},
id: 'system-role-index',
@@ -104,19 +97,19 @@ function handleAdd() {
drawerApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: Role) {
drawerApi.setData({ id: record.roleId });
drawerApi.open();
}
async function handleDelete(row: Recordable<any>) {
await roleRemove(row.roleId);
async function handleDelete(row: Role) {
await roleRemove([row.roleId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.roleId);
const ids = rows.map((row: Role) => row.roleId);
Modal.confirm({
title: '提示',
okType: 'danger',
@@ -142,13 +135,13 @@ const [RoleAuthModal, authModalApi] = useVbenModal({
connectedComponent: roleAuthModal,
});
function handleAuthEdit(record: Recordable<any>) {
function handleAuthEdit(record: Role) {
authModalApi.setData({ id: record.roleId });
authModalApi.open();
}
const router = useRouter();
function handleAssignRole(record: Recordable<any>) {
function handleAssignRole(record: Role) {
router.push(`/system/role-assign/${record.roleId}`);
}
</script>

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { DeptOption } from '#/api/system/role/model';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -23,7 +25,7 @@ const [BasicForm, formApi] = useVbenForm({
showDefaultActions: false,
});
const deptTree = ref<any[]>([]);
const deptTree = ref<DeptOption[]>([]);
async function setupDeptTree(id: number | string) {
const resp = await roleDeptTree(id);
formApi.setFieldValue('deptIds', resp.checkedKeys);

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { MenuOption } from '#/api/system/menu/model';
import { computed, nextTick, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
@@ -8,7 +10,7 @@ import { cloneDeep, eachTree } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import { menuTreeSelect, roleMenuTreeSelect } from '#/api/system/menu';
import { roleAdd, roleInfo, roleUpdate } from '#/api/system/role';
import { TreeSelectPanel } from '#/components/tree';
import { MenuSelectTable } from '#/components/tree';
import { drawerSchema } from './data';
@@ -32,11 +34,10 @@ const [BasicForm, formApi] = useVbenForm({
wrapperClass: 'grid-cols-2 gap-x-4',
});
const menuTree = ref<any[]>([]);
const menuTree = ref<MenuOption[]>([]);
async function setupMenuTree(id?: number | string) {
if (id) {
const resp = await roleMenuTreeSelect(id);
formApi.setFieldValue('menuIds', resp.checkedKeys);
const menus = resp.menus;
// i18n处理
eachTree(menus, (node) => {
@@ -44,15 +45,20 @@ async function setupMenuTree(id?: number | string) {
});
// 设置菜单信息
menuTree.value = resp.menus;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', resp.checkedKeys);
} else {
const resp = await menuTreeSelect();
formApi.setFieldValue('menuIds', []);
// i18n处理
eachTree(resp, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = resp;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', []);
}
}
@@ -78,11 +84,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
},
});
/**
* 这里拿到的是一个数组ref
*/
const menuSelectRef = ref();
const menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>();
async function handleConfirm() {
try {
drawerApi.drawerLoading(true);
@@ -91,7 +93,7 @@ async function handleConfirm() {
return;
}
// 这个用于提交
const menuIds = menuSelectRef.value?.[0]?.getCheckedKeys() ?? [];
const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];
// formApi.getValues拿到的是一个readonly对象不能直接修改需要cloneDeep
const data = cloneDeep(await formApi.getValues());
data.menuIds = menuIds;
@@ -120,17 +122,19 @@ function handleMenuCheckStrictlyChange(value: boolean) {
</script>
<template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[800px]">
<BasicForm>
<template #menuIds="slotProps">
<!-- check-strictly为readonly 不能通过v-model绑定 -->
<TreeSelectPanel
ref="menuSelectRef"
v-bind="slotProps"
:check-strictly="formApi.form.values.menuCheckStrictly"
:tree-data="menuTree"
@check-strictly-change="handleMenuCheckStrictlyChange"
/>
<div class="h-[600px] w-full">
<!-- association为readonly 不能通过v-model绑定 -->
<MenuSelectTable
ref="menuSelectRef"
:checked-keys="slotProps.value"
:association="formApi.form.values.menuCheckStrictly"
:menus="menuTree"
@update:association="handleMenuCheckStrictlyChange"
/>
</div>
</template>
</BasicForm>
</BasicDrawer>

View File

@@ -1,10 +1,11 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getPopupContainer } from '@vben/utils';
import dayjs from 'dayjs';
import { type FormSchemaGetter, z } from '#/adapter/form';
import { z } from '#/adapter/form';
export const querySchema: FormSchemaGetter = () => [
{

View File

@@ -1,20 +1,18 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { Tenant } from '#/api/system/tenant/model';
import { computed } from 'vue';
import { useAccess } from '@vben/access';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { Fallback } from '@vben/common-ui';
import { Fallback, Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
dictSyncTenant,
tenantExport,
@@ -67,7 +65,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'id',
},
id: 'system-tenant-index',
@@ -87,20 +84,20 @@ function handleAdd() {
drawerApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: Tenant) {
drawerApi.setData({ id: record.id });
drawerApi.open();
}
async function handleSync(record: Recordable<any>) {
async function handleSync(record: Tenant) {
const { tenantId, packageId } = record;
await tenantSyncPackage(tenantId, packageId);
await tableApi.query();
}
const tenantStore = useTenantStore();
async function handleDelete(row: Recordable<any>) {
await tenantRemove(row.id);
async function handleDelete(row: Tenant) {
await tenantRemove([row.id]);
await tableApi.query();
// 重新加载租户信息
tenantStore.initTenant();
@@ -108,7 +105,7 @@ async function handleDelete(row: Recordable<any>) {
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.id);
const ids = rows.map((row: Tenant) => row.id);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,7 +1,6 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { type FormSchemaGetter } from '#/adapter/form';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',

View File

@@ -1,20 +1,18 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { TenantPackage } from '#/api/system/tenant-package/model';
import { computed } from 'vue';
import { useAccess } from '@vben/access';
import { Page, useVbenDrawer, type VbenFormProps } from '@vben/common-ui';
import { Fallback } from '@vben/common-ui';
import { Fallback, Page, useVbenDrawer } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
packageChangeStatus,
packageExport,
@@ -63,7 +61,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'packageId',
},
id: 'system-tenant-package-index',
@@ -83,19 +80,19 @@ function handleAdd() {
drawerApi.open();
}
async function handleEdit(record: Recordable<any>) {
async function handleEdit(record: TenantPackage) {
drawerApi.setData({ id: record.packageId });
drawerApi.open();
}
async function handleDelete(row: Recordable<any>) {
await packageRemove(row.packageId);
async function handleDelete(row: TenantPackage) {
await packageRemove([row.packageId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.packageId);
const ids = rows.map((row: TenantPackage) => row.packageId);
Modal.confirm({
title: '提示',
okType: 'danger',

View File

@@ -1,23 +1,24 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { MenuOption } from '#/api/system/menu/model';
import { computed, nextTick, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep, eachTree, listToTree } from '@vben/utils';
import { cloneDeep, eachTree } from '@vben/utils';
import { omit } from 'lodash-es';
import { useVbenForm } from '#/adapter/form';
import { menuList, tenantPackageMenuTreeSelect } from '#/api/system/menu';
import { menuTreeSelect, tenantPackageMenuTreeSelect } from '#/api/system/menu';
import {
packageAdd,
packageInfo,
packageUpdate,
} from '#/api/system/tenant-package';
import { TreeSelectPanel } from '#/components/tree';
import { MenuSelectTable } from '#/components/tree';
import { drawerSchema } from './data';
import TreeItem from './tree-item';
const emit = defineEmits<{ reload: [] }>();
@@ -36,25 +37,34 @@ const [BasicForm, formApi] = useVbenForm({
wrapperClass: 'grid-cols-2',
});
async function setupMenuTreeSelect(id?: number | string) {
const menuTree = ref<MenuOption[]>([]);
async function setupMenuTree(id?: number | string) {
if (id) {
const resp = await tenantPackageMenuTreeSelect(id);
const menus = resp.menus;
// i18n处理
eachTree(menus, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = resp.menus;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', resp.checkedKeys);
} else {
const resp = await menuTreeSelect();
// i18n处理
eachTree(resp, (node) => {
node.label = $t(node.label);
});
// 设置菜单信息
menuTree.value = resp;
// keys依赖于menu 需要先加载menu
await nextTick();
await formApi.setFieldValue('menuIds', []);
}
}
const menuTree = ref<any[]>([]);
async function setupMenuTree() {
const resp = await menuList();
const treeData = listToTree(resp, { id: 'menuId' });
// i18n处理
eachTree(treeData, (node) => {
node.menuName = $t(node.menuName);
});
// 设置菜单信息
menuTree.value = treeData;
}
const [BasicDrawer, drawerApi] = useVbenDrawer({
onCancel: handleCancel,
onConfirm: handleConfirm,
@@ -72,20 +82,14 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
// 通过setupMenuTreeSelect设置
await formApi.setValues(omit(record, ['menuIds']));
}
/**
* 加载菜单树和已勾选菜单
*/
await Promise.all([setupMenuTree(), setupMenuTreeSelect(id)]);
// init菜单 注意顺序要放在赋值record之后 内部watch会依赖record
await setupMenuTree(id);
drawerApi.drawerLoading(false);
},
});
/**
* 这里拿到的是一个数组ref
*/
const menuSelectRef = ref();
const menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>();
async function handleConfirm() {
try {
drawerApi.drawerLoading(true);
@@ -94,7 +98,7 @@ async function handleConfirm() {
return;
}
// 这个用于提交
const menuIds = menuSelectRef.value?.[0]?.getCheckedKeys() ?? [];
const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];
// formApi.getValues拿到的是一个readonly对象不能直接修改需要cloneDeep
const data = cloneDeep(await formApi.getValues());
data.menuIds = menuIds;
@@ -123,22 +127,19 @@ function handleMenuCheckStrictlyChange(value: boolean) {
</script>
<template>
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[800px]">
<BasicForm>
<template #menuIds="slotProps">
<TreeSelectPanel
ref="menuSelectRef"
v-bind="slotProps"
:check-strictly="formApi.form.values.menuCheckStrictly"
:expand-all-on-init="false"
:field-names="{ title: 'menuName', key: 'menuId' }"
:tree-data="menuTree"
@check-strictly-change="handleMenuCheckStrictlyChange"
>
<template #title="data">
<TreeItem :data="data" />
</template>
</TreeSelectPanel>
<div class="h-[600px] w-full">
<!-- association为readonly 不能通过v-model绑定 -->
<MenuSelectTable
ref="menuSelectRef"
:checked-keys="slotProps.value"
:association="formApi.form.values.menuCheckStrictly"
:menus="menuTree"
@update:association="handleMenuCheckStrictlyChange"
/>
</div>
</template>
</BasicForm>
</BasicDrawer>

View File

@@ -1,6 +1,8 @@
import type { PropType } from 'vue';
import type { Menu } from '#/api/system/menu/model';
import { computed, defineComponent, type PropType } from 'vue';
import { computed, defineComponent } from 'vue';
import { Tag } from 'ant-design-vue';

View File

@@ -1,9 +1,10 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { type FormSchemaGetter, z } from '#/adapter/form';
import { z } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
export const querySchema: FormSchemaGetter = () => [

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
import type { PropType } from 'vue';
import type { DeptTree } from '#/api/system/user/model';
import { onMounted, type PropType, ref } from 'vue';
import { onMounted, ref } from 'vue';
import { SyncOutlined } from '@ant-design/icons-vue';
import { Empty, InputSearch, Skeleton, Tree } from 'ant-design-vue';
@@ -10,6 +12,8 @@ import { getDeptTree } from '#/api/system/user';
defineOptions({ inheritAttrs: false });
withDefaults(defineProps<{ showSearch?: boolean }>(), { showSearch: true });
const emit = defineEmits<{
/**
* 点击刷新按钮的事件
@@ -68,7 +72,10 @@ onMounted(loadTree);
class="bg-background flex h-full flex-col overflow-y-auto rounded-lg"
>
<!-- 固定在顶部 必须加上bg-background背景色 否则会产生'穿透'效果 -->
<div class="bg-background z-100 sticky left-0 top-0 p-[8px]">
<div
v-if="showSearch"
class="bg-background z-100 sticky left-0 top-0 p-[8px]"
>
<InputSearch
v-model:value="searchValue"
:placeholder="$t('pages.common.search')"

View File

@@ -1,15 +1,13 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { User } from '#/api/system/user/model';
import { ref } from 'vue';
import { useAccess } from '@vben/access';
import {
Page,
useVbenDrawer,
useVbenModal,
type VbenFormProps,
} from '@vben/common-ui';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { preferences } from '@vben/preferences';
import { getVxePopupContainer } from '@vben/utils';
@@ -24,8 +22,7 @@ import {
Space,
} from 'ant-design-vue';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { vxeCheckboxChecked } from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
userExport,
userList,
@@ -117,7 +114,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'userId',
height: 48,
},
@@ -137,19 +133,19 @@ function handleAdd() {
userDrawerApi.open();
}
function handleEdit(row: Recordable<any>) {
function handleEdit(row: User) {
userDrawerApi.setData({ id: row.userId });
userDrawerApi.open();
}
async function handleDelete(row: Recordable<any>) {
await userRemove(row.userId);
async function handleDelete(row: User) {
await userRemove([row.userId]);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.userId);
const ids = rows.map((row: User) => row.userId);
Modal.confirm({
title: '提示',
okType: 'danger',
@@ -170,7 +166,7 @@ function handleDownloadExcel() {
const [UserInfoModal, userInfoModalApi] = useVbenModal({
connectedComponent: userInfoModal,
});
function handleUserInfo(row: Recordable<any>) {
function handleUserInfo(row: User) {
userInfoModalApi.setData({ userId: row.userId });
userInfoModalApi.open();
}
@@ -179,7 +175,7 @@ const [UserResetPwdModal, userResetPwdModalApi] = useVbenModal({
connectedComponent: userResetPwdModal,
});
function handleResetPwd(record: Recordable<any>) {
function handleResetPwd(record: User) {
userResetPwdModalApi.setData({ record });
userResetPwdModalApi.open();
}

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Role } from '#/api/system/user/model';
import { computed, h, ref } from 'vue';
import { computed, h, onMounted, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
@@ -117,13 +117,20 @@ async function setupDeptSelect() {
]);
}
const defaultPassword = ref('');
onMounted(async () => {
const password = await configInfoByKey('sys.user.initPassword');
if (password) {
defaultPassword.value = password;
}
});
/**
* 新增时候 从参数设置获取默认密码
*/
async function loadDefaultPassword(update: boolean) {
if (!update) {
const defaultPassword = await configInfoByKey('sys.user.initPassword');
defaultPassword && formApi.setFieldValue('password', defaultPassword);
if (!update && defaultPassword.value) {
formApi.setFieldValue('password', defaultPassword.value);
}
}

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { User } from '#/api/system/user/model';
import { useVbenModal } from '@vben/common-ui';
import { findUserInfo } from '#/api/system/user';
@@ -39,8 +41,12 @@ async function handleOpenChange(open: boolean) {
.filter((item) => roleIds.includes(item.roleId))
.map((item) => item.roleName);
(user as any).postNames = postNames;
(user as any).roleNames = roleNames;
interface UserWithNames extends User {
postNames: string[];
roleNames: string[];
}
(user as UserWithNames).postNames = postNames;
(user as UserWithNames).roleNames = roleNames;
// 赋值
setDescProps({ data: user });

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { ResetPwdParam, User } from '#/api/system/user/model';
import { useVbenModal, z } from '@vben/common-ui';
@@ -68,7 +68,7 @@ async function handleOpenChange(open: boolean) {
if (!open) {
return null;
}
const { record } = modalApi.getData() as { record: Recordable<any> };
const { record } = modalApi.getData() as { record: User };
setDescProps({ data: record }, true);
await formApi.setValues({ userId: record.userId });
}
@@ -81,7 +81,7 @@ async function handleSubmit() {
return;
}
const data = await formApi.getValues();
await userResetPassword(data as any);
await userResetPassword(data as ResetPwdParam);
emit('reload');
handleCancel();
} catch (error) {

View File

@@ -1,14 +1,14 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import type { Key } from 'ant-design-vue/es/vc-tree/interface';
import { type Component, markRaw, ref } from 'vue';
import type { Component } from 'vue';
import {
CodeMirror,
type LanguageSupport,
useVbenModal,
} from '@vben/common-ui';
import type { LanguageSupport } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { markRaw, ref } from 'vue';
import { CodeMirror, useVbenModal } from '@vben/common-ui';
import {
DefaultFileIcon,
FolderIcon,

View File

@@ -1,7 +1,6 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { type FormSchemaGetter } from '#/adapter/form';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Select',

View File

@@ -1,23 +1,20 @@
<script setup lang="ts">
import type { GenInfo } from '#/api/tool/gen/model';
import { onMounted, provide, ref } from 'vue';
import { useRoute } from 'vue-router';
import { onMounted, provide, ref, unref, useTemplateRef } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useTabs } from '@vben/hooks';
import { safeParseNumber } from '@vben/utils';
import { cloneDeep, safeParseNumber } from '@vben/utils';
import { Skeleton, Step, Steps } from 'ant-design-vue';
import { Card, Skeleton, TabPane, Tabs } from 'ant-design-vue';
import { genInfo } from '#/api/tool/gen';
import { editSave, genInfo } from '#/api/tool/gen';
import { BasicSetting, GenConfig, GenSuccess } from './edit-steps';
import { emitter } from './mitt';
import { BasicSetting, GenConfig } from './edit-steps';
const current = ref(0);
const { setTabTitle } = useTabs();
const { setTabTitle, closeCurrentTab } = useTabs();
const routes = useRoute();
// 获取路由参数
const tableId = routes.params.tableId as string;
@@ -34,29 +31,89 @@ onMounted(async () => {
setTabTitle(`生成配置: ${resp.info.tableName}`);
});
/**
* 事件总线 监听切换步骤
*/
emitter.on('to', (step: number) => {
current.value = step;
});
const currentTab = ref<'fields' | 'setting'>('setting');
const basicSettingRef = useTemplateRef('basicSettingRef');
const genConfigRef = useTemplateRef('genConfigRef');
const router = useRouter();
async function handleSave() {
try {
// 校验tab1
const settingValidate = await basicSettingRef.value?.validateForm();
if (!settingValidate) {
currentTab.value = 'setting';
return;
}
// 校验tab2
const genConfigValidate = await genConfigRef.value?.validateTable();
if (!genConfigValidate) {
currentTab.value = 'fields';
return;
}
const requestData = cloneDeep(unref(genInfoData)!);
// 获取表单数据
const formValues = await basicSettingRef.value?.getFormValues();
// 合并
Object.assign(requestData, formValues);
// 从表格获取最新的
requestData.columns = genConfigRef.value?.getTableRecords() ?? [];
// 树表需要添加这个参数
if (requestData && requestData.tplCategory === 'tree') {
const { treeCode, treeName, treeParentCode } = requestData;
requestData.params = {
treeCode,
treeName,
treeParentCode,
};
}
// 需要进行参数转化
if (requestData) {
const transform = (ret: boolean) => (ret ? '1' : '0');
requestData.columns.forEach((column) => {
const { edit, insert, query, required, list } = column;
column.isInsert = transform(insert);
column.isEdit = transform(edit);
column.isList = transform(list);
column.isQuery = transform(query);
column.isRequired = transform(required);
});
// 需要手动添加父级菜单 弹窗类型
requestData.params = {
...requestData.params,
parentMenuId: requestData.parentMenuId,
popupComponent: requestData.popupComponent,
};
}
// 保存
await editSave(requestData);
// 关闭 & 跳转
await closeCurrentTab();
router.push({ path: '/tool/gen', replace: true });
} catch (error) {
console.error(error);
}
}
</script>
<template>
<Page content-class="bg-background p-5 rounded-lg">
<div class="flex items-center justify-center">
<Steps :current="current" class="w-fit">
<Step title="生成信息" />
<Step disabled title="字段信息" />
<Step disabled title="完成" />
</Steps>
</div>
<!-- content -->
<div v-if="genInfoData">
<BasicSetting v-if="current === 0" />
<GenConfig v-if="current === 1" />
<GenSuccess v-if="current === 2" />
</div>
<Page :auto-content-height="true">
<Card
class="h-full"
v-if="genInfoData"
:body-style="{ padding: '0 16px 16px' }"
>
<Tabs v-model:active-key="currentTab" size="middle">
<template #rightExtra>
<a-button type="primary" @click="handleSave">保存配置</a-button>
</template>
<TabPane key="setting" tab="生成信息" :force-render="true">
<BasicSetting ref="basicSettingRef" />
</TabPane>
<TabPane key="fields" tab="字段信息" :force-render="true">
<GenConfig ref="genConfigRef" />
</TabPane>
</Tabs>
</Card>
<Skeleton v-else :active="true" />
</Page>
</template>

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { Column, GenInfo } from '#/api/tool/gen/model';
import { inject, onMounted, type Ref } from 'vue';
import { inject, onMounted } from 'vue';
import { useVbenForm } from '@vben/common-ui';
import { $t } from '@vben/locales';
@@ -11,7 +13,6 @@ import { Col, Row } from 'ant-design-vue';
import { menuList } from '#/api/system/menu';
import { toCurrentStep } from '../mitt';
import { formSchema } from './basic';
/**
@@ -116,28 +117,34 @@ onMounted(async () => {
await Promise.all([initTreeSelect(info.columns), initMenuSelect()]);
});
async function handleNext() {
try {
const { valid } = await formApi.validate();
if (!valid) {
return null;
}
const data = await formApi.getValues();
Object.assign(genInfoData.value, data);
toCurrentStep(1);
} catch (error) {
console.error(error);
/**
* 校验表单
*/
async function validateForm() {
const { valid } = await formApi.validate();
if (!valid) {
return false;
}
return true;
}
/**
* 获取表单值
*/
async function getFormValues() {
return await formApi.getValues();
}
defineExpose({
validateForm,
getFormValues,
});
</script>
<template>
<Row justify="center">
<Col v-bind="{ xs: 24, sm: 24, md: 20, lg: 16, xl: 16 }">
<BasicForm />
<div class="flex justify-center">
<a-button type="primary" @click="handleNext">下一步</a-button>
</div>
</Col>
</Row>
</template>

View File

@@ -1,6 +1,8 @@
import type { FormSchemaGetter } from '#/adapter/form';
import { getPopupContainer } from '@vben/utils';
import { type FormSchemaGetter, z } from '#/adapter/form';
import { z } from '#/adapter/form';
export const formSchema: FormSchemaGetter = () => [
{

View File

@@ -1,15 +1,13 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { GenInfo } from '#/api/tool/gen/model';
import { inject, type Ref, unref } from 'vue';
import { inject } from 'vue';
import { message, Space } from 'ant-design-vue';
import { cloneDeep } from 'lodash-es';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { editSave } from '#/api/tool/gen';
import { toCurrentStep } from '../mitt';
import { validRules, vxeTableColumns } from './gen-data';
/**
@@ -23,7 +21,6 @@ const gridOptions: VxeGridProps = {
editConfig: { trigger: 'click', mode: 'cell', showStatus: true },
editRules: validRules,
rowConfig: {
isHover: true,
keyField: 'id',
isCurrent: true, // 高亮当前行
},
@@ -34,9 +31,7 @@ const gridOptions: VxeGridProps = {
enabled: true,
},
toolbarConfig: {
refresh: false,
zoom: false,
custom: false,
enabled: false,
},
height: 'auto',
pagerConfig: {
@@ -47,62 +42,31 @@ const gridOptions: VxeGridProps = {
const [BasicTable, tableApi] = useVbenVxeGrid({ gridOptions });
async function handleSubmit() {
try {
const hasError = await tableApi.grid.validate();
if (hasError) {
message.error('校验未通过');
return;
}
const requestData = cloneDeep(unref(genInfoData));
// 从表格获取最新的
requestData.columns = tableApi.grid.getData();
// 树表需要添加这个参数
if (requestData && requestData.tplCategory === 'tree') {
const { treeCode, treeName, treeParentCode } = requestData;
requestData.params = {
treeCode,
treeName,
treeParentCode,
};
}
// 需要进行参数转化
if (requestData) {
const transform = (ret: boolean) => (ret ? '1' : '0');
requestData.columns.forEach((column) => {
const { edit, insert, query, required, list } = column;
column.isInsert = transform(insert);
column.isEdit = transform(edit);
column.isList = transform(list);
column.isQuery = transform(query);
column.isRequired = transform(required);
});
// 需要手动添加父级菜单 弹窗类型
requestData.params = {
...requestData.params,
parentMenuId: requestData.parentMenuId,
popupComponent: requestData.popupComponent,
};
}
await editSave(requestData);
// 跳转到成功页面
toCurrentStep(2);
} catch (error: unknown) {
console.error(error);
}
/**
* 校验表格数据
*/
async function validateTable() {
const hasError = await tableApi.grid.validate();
return !hasError;
}
/**
* 获取表格数据
*/
function getTableRecords() {
return tableApi?.grid?.getData?.() ?? [];
}
defineExpose({
validateTable,
getTableRecords,
});
</script>
<template>
<div class="flex flex-col gap-[16px] p-[12px]">
<div class="h-[calc(100vh-235px)] overflow-y-hidden">
<div class="flex flex-col gap-[16px]">
<div class="h-[calc(100vh-200px)] overflow-y-hidden">
<BasicTable />
</div>
<div class="flex justify-center">
<Space>
<a-button @click="toCurrentStep(0)">上一步</a-button>
<a-button type="primary" @click="handleSubmit">下一步</a-button>
</Space>
</div>
</div>
</template>

View File

@@ -1,25 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useTabs } from '@vben/hooks';
import { Result } from 'ant-design-vue';
const { closeCurrentTab } = useTabs();
const router = useRouter();
async function handleClose() {
await closeCurrentTab();
router.push({ path: '/tool/gen', replace: true });
}
</script>
<template>
<Result status="success" title="修改成功">
<template #extra>
<a-button type="primary" @click="handleClose"> 关闭 </a-button>
</template>
</Result>
</template>
<style scoped></style>

View File

@@ -1,3 +1,2 @@
export { default as BasicSetting } from './basic-setting.vue';
export { default as GenConfig } from './gen-config.vue';
export { default as GenSuccess } from './gen-success.vue';

View File

@@ -1,20 +1,19 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { Page, useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { message, Modal, Popconfirm, Space } from 'ant-design-vue';
import dayjs from 'dayjs';
import {
useVbenVxeGrid,
vxeCheckboxChecked,
type VxeGridProps,
} from '#/adapter/vxe-table';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
batchGenCode,
generatedList,
@@ -72,7 +71,6 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'tableId',
},
id: 'tool-gen-index',

View File

@@ -1,15 +0,0 @@
import { mitt } from '@vben/utils';
type Events = {
to: number;
};
export const emitter = mitt<Events>();
/**
* 跳转到指定步骤
* @param step 步骤
*/
export function toCurrentStep(step: number) {
emitter.emit('to', step);
}

View File

@@ -1,7 +1,11 @@
<script setup lang="ts">
import { useVbenModal, type VbenFormProps } from '@vben/common-ui';
import type { VbenFormProps } from '@vben/common-ui';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
getDataSourceNames,
importTable,
@@ -55,6 +59,7 @@ const gridOptions: VxeGridProps = {
{
title: '表描述',
field: 'tableComment',
align: 'left',
},
{
title: '创建时间',
@@ -81,13 +86,10 @@ const gridOptions: VxeGridProps = {
},
},
rowConfig: {
isHover: true,
keyField: 'tableId',
},
toolbarConfig: {
refresh: false,
zoom: false,
custom: false,
enabled: false,
},
};

View File

@@ -3,7 +3,12 @@ import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep, getPopupContainer, listToTree } from '@vben/utils';
import {
addFullName,
cloneDeep,
getPopupContainer,
listToTree,
} from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import {
@@ -40,25 +45,20 @@ const [BasicForm, formApi] = useVbenForm({
async function setupCategorySelect() {
const listData = await categoryList();
let treeData = listToTree(listData, {
id: 'id',
const treeData = listToTree(listData, {
id: 'categoryId',
pid: 'parentId',
});
treeData = [
{
categoryName: '根目录',
id: 0,
children: treeData,
},
];
addFullName(treeData, 'categoryName', ' / ');
formApi.updateSchema([
{
fieldName: 'parentId',
componentProps: {
treeData,
treeLine: { showLeafIcon: false },
fieldNames: { label: 'categoryName', value: 'id' },
fieldNames: { label: 'categoryName', value: 'categoryId' },
treeDefaultExpandAll: true,
treeNodeLabelProp: 'fullName',
getPopupContainer,
},
},
@@ -120,7 +120,11 @@ async function handleCancel() {
</script>
<template>
<BasicModal :close-on-click-modal="false" :title="title">
<BasicModal
:close-on-click-modal="false"
:title="title"
class="min-h-[500px]"
>
<BasicForm />
</BasicModal>
</template>

View File

@@ -21,11 +21,7 @@ export const columns: VxeGridProps['columns'] = [
treeNode: true,
},
{
field: 'categoryCode',
title: '分类编码',
},
{
field: 'sortNum',
field: 'orderNum',
title: '排序',
},
{
@@ -39,8 +35,8 @@ export const columns: VxeGridProps['columns'] = [
export const modalSchema: FormSchemaGetter = () => [
{
label: 'id',
fieldName: 'id',
label: 'categoryId',
fieldName: 'categoryId',
component: 'Input',
dependencies: {
show: () => false,
@@ -51,7 +47,7 @@ export const modalSchema: FormSchemaGetter = () => [
fieldName: 'parentId',
label: '父级分类',
rules: 'required',
defaultValue: 0,
defaultValue: 100,
component: 'TreeSelect',
},
{
@@ -61,15 +57,8 @@ export const modalSchema: FormSchemaGetter = () => [
rules: 'required',
},
{
fieldName: 'categoryCode',
label: '分类编码',
component: 'Input',
rules: 'required',
},
{
fieldName: 'sortNum',
fieldName: 'orderNum',
label: '排序',
component: 'InputNumber',
rules: 'required',
},
];

View File

@@ -56,11 +56,11 @@ const gridOptions: VxeGridProps = {
gt: 0,
},
rowConfig: {
keyField: 'id',
keyField: 'categoryId',
},
treeConfig: {
parentField: 'parentId',
rowField: 'id',
rowField: 'categoryId',
transform: true,
},
// 表格全局唯一表示 保存列配置需要用到
@@ -73,17 +73,17 @@ const [CategoryModal, modalApi] = useVbenModal({
});
function handleAdd(row?: Recordable<any>) {
modalApi.setData({ parentId: row?.id });
modalApi.setData({ parentId: row?.categoryId });
modalApi.open();
}
async function handleEdit(row: Recordable<any>) {
modalApi.setData({ id: row.id });
modalApi.setData({ id: row.categoryId });
modalApi.open();
}
async function handleDelete(row: Recordable<any>) {
await categoryRemove(row.id);
await categoryRemove(row.categoryId);
await tableApi.query();
}

View File

@@ -0,0 +1,135 @@
<!-- 流程发起(启动)的弹窗 -->
<script setup lang="ts">
import type { CompleteTaskReqData } from '#/api/workflow/task/model';
import { useVbenModal } from '@vben/common-ui';
import { cloneDeep } from 'lodash-es';
import { useVbenForm } from '#/adapter/form';
import { completeTask } from '#/api/workflow/task';
import { CopyComponent } from '.';
interface Emits {
/**
* 完成
*/
complete: [];
}
const emit = defineEmits<Emits>();
interface ModalProps {
taskId: string;
taskVariables: Record<string, any>;
variables?: any; // 这个干啥的
}
const [BasicModal, modalApi] = useVbenModal({
title: '流程发起',
fullscreenButton: false,
onConfirm: handleSubmit,
});
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
// 默认占满两列
formItemClass: 'col-span-2',
// 默认label宽度 px
labelWidth: 100,
// 通用配置项 会影响到所有表单项
componentProps: {
class: 'w-full',
},
},
schema: [
{
fieldName: 'messageType',
component: 'CheckboxGroup',
componentProps: {
options: [
{ label: '站内信', value: '1', disabled: true },
{ label: '邮件', value: '2' },
{ label: '短信', value: '3' },
],
},
label: '通知方式',
defaultValue: ['1'],
},
{
fieldName: 'attachment',
component: 'FileUpload',
componentProps: {
resultField: 'ossId',
maxNumber: 10,
maxSize: 20,
accept: [
'png',
'jpg',
'jpeg',
'doc',
'docx',
'xlsx',
'xls',
'ppt',
'pdf',
],
},
defaultValue: [],
label: '附件上传',
formItemClass: 'items-start',
},
{
fieldName: 'flowCopyList',
component: 'Input',
defaultValue: [],
label: '抄送人',
},
],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
async function handleSubmit() {
try {
modalApi.modalLoading(true);
const { messageType, flowCopyList, attachment } = cloneDeep(
await formApi.getValues(),
);
const { taskId, taskVariables, variables } =
modalApi.getData() as ModalProps;
// 需要转换数据 抄送人员
const flowCCList = (flowCopyList as Array<any>).map((item) => ({
userId: item.userId,
userName: item.nickName,
}));
const requestData = {
fileId: attachment.join(','),
messageType,
flowCopyList: flowCCList,
taskId,
taskVariables,
variables,
} as CompleteTaskReqData;
await completeTask(requestData);
modalApi.close();
emit('complete');
} catch (error) {
console.error(error);
} finally {
modalApi.modalLoading(false);
}
}
</script>
<template>
<BasicModal>
<BasicForm>
<template #flowCopyList="slotProps">
<CopyComponent v-model:user-list="slotProps.modelValue" />
</template>
</BasicForm>
</BasicModal>
</template>

View File

@@ -1,51 +1,83 @@
<script setup lang="ts">
import { Avatar, Descriptions, DescriptionsItem, Tag } from 'ant-design-vue';
import type { TaskInfo } from '#/api/workflow/task/model';
interface Props {
id: string;
endTime: string;
startTime: string;
title: string;
desc: string;
status: string;
import { computed } from 'vue';
import { VbenAvatar } from '@vben/common-ui';
import { DictEnum } from '@vben/constants';
import { Descriptions, DescriptionsItem, Tooltip } from 'ant-design-vue';
import { renderDict } from '#/utils/render';
import { getDiffTimeString } from './helper';
interface Props extends TaskInfo {
active: boolean;
}
const props = withDefaults(defineProps<{ info: Props }>(), {});
const props = withDefaults(defineProps<{ info: Props; rowKey?: string }>(), {
rowKey: 'id',
});
const emit = defineEmits<{ click: [string] }>();
/**
* TODO: 这里要优化 事件没有用到
*/
function handleClick() {
emit('click', props.info.id);
const idKey = props.rowKey as keyof TaskInfo;
emit('click', props.info[idKey]);
}
const diffUpdateTimeString = computed(() => {
return getDiffTimeString(props.info.updateTime);
});
</script>
<template>
<div
:class="{
'border-primary': info.active,
'border-[2px]': info.active,
}"
class="cursor-pointer rounded-lg border-[1px] border-solid p-3 transition-shadow duration-300 ease-in-out hover:shadow-lg"
@click.stop="handleClick"
>
<Descriptions :column="1" :title="info.title" size="middle">
<Descriptions :column="1" :title="info.flowName" size="middle">
<template #extra>
<Tag color="warning">审批中</Tag>
</template>
<DescriptionsItem label="描述">{{ info.desc }}</DescriptionsItem>
<DescriptionsItem label="开始时间">{{ info.startTime }}</DescriptionsItem>
<DescriptionsItem label="结束时间">{{ info.endTime }}</DescriptionsItem>
</Descriptions>
<div class="flex items-center justify-between text-[14px]">
<div class="flex items-center gap-1">
<Avatar
size="small"
src="https://plus.dapdap.top/minio-server/plus/2024/11/21/925ed278e2d441beb7f695b41e13c4dd.jpg"
<component
:is="renderDict(info.flowStatus, DictEnum.WF_BUSINESS_STATUS)"
/>
<span class="opacity-50">疯狂的牛子Li</span>
</template>
<DescriptionsItem label="当前任务">
<div class="font-bold">{{ info.nodeName }}</div>
</DescriptionsItem>
<DescriptionsItem label="提交时间">
{{ info.createTime }}
</DescriptionsItem>
<!-- <DescriptionsItem label="更新时间">
{{ info.updateTime }}
</DescriptionsItem> -->
</Descriptions>
<div class="flex w-full items-center justify-between text-[14px]">
<div class="flex items-center gap-1 overflow-hidden whitespace-nowrap">
<VbenAvatar
:alt="info.createByName"
class="bg-primary size-[24px] rounded-full text-[10px] text-white"
src=""
/>
<span class="overflow-hidden text-ellipsis opacity-50">
{{ info.createByName }}
</span>
</div>
<div class="text-nowrap opacity-50">
<Tooltip placement="top" :title="`更新时间: ${info.updateTime}`">
<div class="flex items-center gap-1">
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
<span>{{ diffUpdateTimeString }}前更新</span>
</div>
</Tooltip>
</div>
<div class="opacity-50">处理时间: 2022-01-01</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,26 @@
<!-- 审批终止 Modal弹窗的content属性专用 用于填写审批意见 -->
<script setup lang="ts">
import { Textarea } from 'ant-design-vue';
defineOptions({
name: 'ApprovalContent',
inheritAttrs: false,
});
defineProps<{ description: string; value: string }>();
defineEmits<{ 'update:value': [string] }>();
</script>
<template>
<div class="flex flex-col gap-2">
<div>{{ description }}</div>
<Textarea
:allow-clear="true"
:auto-size="true"
:value="value"
placeholder="审批意见(可选)"
@change="(e) => $emit('update:value', e.target.value!)"
/>
</div>
</template>

View File

@@ -0,0 +1,154 @@
<!-- 审批同意的弹窗 -->
<!-- 审批驳回窗口 -->
<script setup lang="ts">
import type { CompleteTaskReqData } from '#/api/workflow/task/model';
import { useVbenModal } from '@vben/common-ui';
import { cloneDeep } from '@vben/utils';
import { omit } from 'lodash-es';
import { useVbenForm } from '#/adapter/form';
import { completeTask } from '#/api/workflow/task';
import { CopyComponent } from '.';
const emit = defineEmits<{ complete: [] }>();
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
// 默认占满两列
formItemClass: 'col-span-2',
// 默认label宽度 px
labelWidth: 100,
// 通用配置项 会影响到所有表单项
componentProps: {
class: 'w-full',
},
},
schema: [
{
fieldName: 'taskId',
component: 'Input',
label: '任务ID',
dependencies: {
show: false,
triggerFields: [''],
},
},
{
fieldName: 'messageType',
component: 'CheckboxGroup',
componentProps: {
options: [
{ label: '站内信', value: '1', disabled: true },
{ label: '邮件', value: '2' },
{ label: '短信', value: '3' },
],
},
label: '通知方式',
defaultValue: ['1'],
},
{
fieldName: 'attachment',
component: 'FileUpload',
componentProps: {
resultField: 'ossId',
maxNumber: 10,
maxSize: 20,
accept: [
'png',
'jpg',
'jpeg',
'doc',
'docx',
'xlsx',
'xls',
'ppt',
'pdf',
],
},
defaultValue: [],
label: '附件上传',
formItemClass: 'items-start',
},
{
fieldName: 'flowCopyList',
component: 'Input',
defaultValue: [],
label: '抄送人',
},
{
fieldName: 'message',
component: 'Textarea',
label: '审批意见',
formItemClass: 'items-baseline',
},
],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
interface ModalProps {
taskId: string;
}
const [BasicModal, modalApi] = useVbenModal({
title: '审批通过',
fullscreenButton: false,
class: 'min-h-[365px]',
onConfirm: handleSubmit,
async onOpenChange(isOpen) {
if (!isOpen) {
await formApi.resetForm();
return null;
}
modalApi.modalLoading(true);
const { taskId } = modalApi.getData() as ModalProps;
await formApi.setFieldValue('taskId', taskId);
modalApi.modalLoading(false);
},
});
async function handleSubmit() {
try {
modalApi.modalLoading(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
const data = cloneDeep(await formApi.getValues());
// 需要转换数据 抄送人员
const flowCopyList = (data.flowCopyList as Array<any>).map((item) => ({
userId: item.userId,
userName: item.nickName,
}));
const requestData = {
...omit(data, ['attachment']),
fileId: data.attachment.join(','),
taskVariables: {},
variables: {},
flowCopyList,
} as CompleteTaskReqData;
await completeTask(requestData);
modalApi.close();
emit('complete');
} catch (error) {
console.error(error);
} finally {
modalApi.modalLoading(false);
}
}
</script>
<template>
<BasicModal>
<BasicForm>
<template #flowCopyList="slotProps">
<CopyComponent v-model:user-list="slotProps.modelValue" />
</template>
</BasicForm>
</BasicModal>
</template>

View File

@@ -0,0 +1,520 @@
<script setup lang="ts">
import type { User } from '#/api/core/user';
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
import type { TaskInfo } from '#/api/workflow/task/model';
import { computed, onUnmounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { Fallback, useVbenModal, VbenAvatar } from '@vben/common-ui';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { CopyOutlined } from '@ant-design/icons-vue';
import { useClipboard, useEventListener } from '@vueuse/core';
import {
Card,
Divider,
Dropdown,
Menu,
MenuItem,
message,
Modal,
Skeleton,
Space,
TabPane,
Tabs,
} from 'ant-design-vue';
import { isObject } from 'lodash-es';
import {
cancelProcessApply,
deleteByInstanceIds,
flowInfo,
} from '#/api/workflow/instance';
import {
getTaskByTaskId,
taskOperation,
terminationTask,
updateAssignee,
} from '#/api/workflow/task';
import { renderDict } from '#/utils/render';
import {
approvalModal,
approvalRejectionModal,
ApprovalTimeline,
flowInterfereModal,
} from '.';
import { approveWithReasonModal } from './helper';
import userSelectModal from './user-select-modal.vue';
defineOptions({
name: 'ApprovalPanel',
inheritAttrs: false,
});
// eslint-disable-next-line no-use-before-define
const props = defineProps<{ task?: TaskInfo; type: ApprovalType }>();
/**
* 下面按钮点击后会触发的事件
*/
const emit = defineEmits<{ reload: [] }>();
const currentTask = ref<TaskInfo>();
/**
* 是否显示 加签/减签操作
*/
const showMultiActions = computed(() => {
if (!currentTask.value) {
return false;
}
if (Number(currentTask.value.nodeRatio) > 0) {
return true;
}
return false;
});
/**
* myself 我发起的
* readonly 只读 只用于查看
* approve 审批
* admin 流程监控 - 待办任务使用
*/
type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
const showFooter = computed(() => {
if (props.type === 'readonly') {
return false;
}
// 我发起的 && [已完成, 已作废] 不显示
if (
props.type === 'myself' &&
['finish', 'invalid'].includes(props.task?.flowStatus ?? '')
) {
return false;
}
return true;
});
const currentFlowInfo = ref<FlowInfoResponse>();
/**
* card的loading状态
*/
const loading = ref(false);
const iframeLoaded = ref(false);
const iframeHeight = ref(300);
useEventListener('message', (event) => {
const data = event.data as { [key: string]: any; type: string };
if (!isObject(data)) return;
/**
* iframe通信 加载完毕后才显示表单 解决卡顿问题
*/
if (data.type === 'mounted') {
iframeLoaded.value = true;
}
/**
* 高度与表单高度保持一致
*/
if (data.type === 'height') {
const height = data.height;
iframeHeight.value = height;
}
});
async function handleLoadInfo(task: TaskInfo | undefined) {
try {
if (!task) return null;
loading.value = true;
iframeLoaded.value = false;
const resp = await flowInfo(task.businessId);
currentFlowInfo.value = resp;
const taskResp = await getTaskByTaskId(props.task!.id);
currentTask.value = taskResp;
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
}
watch(() => props.task, handleLoadInfo);
onUnmounted(() => (currentFlowInfo.value = undefined));
// 进行中 可以撤销
const revocable = computed(() => props.task?.flowStatus === 'waiting');
async function handleCancel() {
Modal.confirm({
title: '提示',
content: '确定要撤销该申请吗?',
centered: true,
okButtonProps: { danger: true },
onOk: async () => {
await cancelProcessApply({
businessId: props.task!.businessId,
message: '申请人撤销流程!',
});
emit('reload');
},
});
}
/**
* 是否可编辑/删除
*/
const editableAndRemoveable = computed(() => {
if (!props.task) {
return false;
}
return ['back', 'cancel', 'draft'].includes(props.task.flowStatus);
});
const router = useRouter();
function handleEdit() {
const path = props.task?.formPath;
if (path) {
router.push({ path, query: { id: props.task!.businessId } });
}
}
function handleRemove() {
Modal.confirm({
title: '提示',
content: '确定删除该申请吗?',
centered: true,
okButtonProps: { danger: true },
onOk: async () => {
await deleteByInstanceIds([props.task!.id]);
emit('reload');
},
});
}
/**
* 审批驳回
*/
const [RejectionModal, rejectionModalApi] = useVbenModal({
connectedComponent: approvalRejectionModal,
});
function handleRejection() {
rejectionModalApi.setData({
taskId: props.task?.id,
definitionId: props.task?.definitionId,
nodeCode: props.task?.nodeCode,
});
rejectionModalApi.open();
}
/**
* 审批终止
*/
function handleTermination() {
approveWithReasonModal({
title: '审批终止',
description: '确定终止当前审批流程吗?',
onOk: async (reason) => {
await terminationTask({ taskId: props.task!.id, comment: reason });
emit('reload');
},
});
}
/**
* 审批通过
*/
const [ApprovalModal, approvalModalApi] = useVbenModal({
connectedComponent: approvalModal,
});
function handleApproval() {
approvalModalApi.setData({ taskId: props.task?.id });
approvalModalApi.open();
}
/**
* TODO: 1提取公共函数 2原版是可以填写意见的(message参数)
*/
/**
* 委托
*/
const [DelegationModal, delegationModalApi] = useVbenModal({
connectedComponent: userSelectModal,
});
function handleDelegation(userList: User[]) {
if (userList.length === 0) return;
const current = userList[0];
approveWithReasonModal({
title: '委托',
description: `确定委托给[${current?.nickName}]吗?`,
onOk: async (reason) => {
await taskOperation(
{ taskId: props.task!.id, userId: current!.userId, message: reason },
'delegateTask',
);
emit('reload');
},
});
}
/**
* 转办
*/
const [TransferModal, transferModalApi] = useVbenModal({
connectedComponent: userSelectModal,
});
function handleTransfer(userList: User[]) {
if (userList.length === 0) return;
const current = userList[0];
approveWithReasonModal({
title: '转办',
description: `确定转办给[${current?.nickName}]吗?`,
onOk: async (reason) => {
await taskOperation(
{ taskId: props.task!.id, userId: current!.userId, message: reason },
'transferTask',
);
emit('reload');
},
});
}
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
connectedComponent: userSelectModal,
});
function handleAddSignature(userList: User[]) {
if (userList.length === 0) return;
const userIds = userList.map((user) => user.userId);
Modal.confirm({
title: '提示',
content: '确认加签吗?',
centered: true,
onOk: async () => {
await taskOperation({ taskId: props.task!.id, userIds }, 'addSignature');
emit('reload');
},
});
}
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
connectedComponent: userSelectModal,
});
function handleReductionSignature(userList: User[]) {
if (userList.length === 0) return;
const userIds = userList.map((user) => user.userId);
Modal.confirm({
title: '提示',
content: '确认减签吗?',
centered: true,
onOk: async () => {
await taskOperation(
{ taskId: props.task!.id, userIds },
'reductionSignature',
);
emit('reload');
},
});
}
// 流程干预
const [FlowInterfereModal, flowInterfereModalApi] = useVbenModal({
connectedComponent: flowInterfereModal,
});
function handleFlowInterfere() {
flowInterfereModalApi.setData({ taskId: props.task?.id });
flowInterfereModalApi.open();
}
// 修改办理人
const [UpdateAssigneeModal, updateAssigneeModalApi] = useVbenModal({
connectedComponent: userSelectModal,
});
function handleUpdateAssignee(userList: User[]) {
if (userList.length === 0) return;
const current = userList[0];
if (!current) return;
Modal.confirm({
title: '修改办理人',
content: `确定修改办理人为${current?.nickName}吗?`,
centered: true,
onOk: async () => {
await updateAssignee([props.task!.id], current.userId);
emit('reload');
},
});
}
/**
* 不加legacy在本地开发没有问题
* 打包后在一些设备会无法复制 使用legacy来保证兼容性
*/
const { copy } = useClipboard({ legacy: true });
async function handleCopy(text: string) {
await copy(text);
message.success('复制成功');
}
</script>
<template>
<Card
v-if="task"
:body-style="{ overflowY: 'auto', height: '100%' }"
:loading="loading"
class="thin-scrollbar flex-1 overflow-y-hidden"
size="small"
>
<template #title>
<div class="flex items-center gap-2">
<div>编号: {{ task.id }}</div>
<CopyOutlined class="cursor-pointer" @click="handleCopy(task.id)" />
</div>
</template>
<template #extra>
<a-button size="small" @click="() => handleLoadInfo(task)">
<div class="flex items-center justify-center">
<span class="icon-[material-symbols--refresh] size-24px"></span>
</div>
</a-button>
</template>
<div class="flex flex-col gap-5 p-4">
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<div class="text-2xl font-bold">{{ task.flowName }}</div>
<div>
<component
:is="renderDict(task.flowStatus, DictEnum.WF_BUSINESS_STATUS)"
/>
</div>
</div>
<div class="flex items-center gap-2">
<VbenAvatar
:alt="task.createByName"
class="bg-primary size-[28px] rounded-full text-white"
src=""
/>
<span>{{ task.createByName }}</span>
<div class="flex items-center opacity-50">
<div class="flex items-center gap-1">
<span class="icon-[bxs--category-alt] size-[16px]"></span>
流程分类: {{ task.categoryName }}
</div>
<Divider type="vertical" />
<div class="flex items-center gap-1">
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
提交时间: {{ task.createTime }}
</div>
</div>
</div>
</div>
<Tabs v-if="currentFlowInfo" class="flex-1">
<TabPane key="1" tab="审批详情">
<div class="h-fulloverflow-y-auto">
<!-- 约定${task.formPath}/frame 为内嵌表单 用于展示 需要在本地路由添加 -->
<iframe
v-show="iframeLoaded"
:src="`${task.formPath}/iframe?readonly=true&id=${task.businessId}`"
:style="{ height: `${iframeHeight}px` }"
class="w-full"
></iframe>
<Skeleton v-show="!iframeLoaded" :paragraph="{ rows: 6 }" active />
<Divider />
<ApprovalTimeline :list="currentFlowInfo.list" />
</div>
</TabPane>
<TabPane key="2" tab="审批流程图">
<img
:src="`data:image/png;base64,${currentFlowInfo.image}`"
class="rounded-lg border"
/>
</TabPane>
</Tabs>
</div>
<!-- 固定底部 -->
<div class="h-[57px]"></div>
<div
v-if="showFooter"
class="border-t-solid bg-background absolute bottom-0 left-0 w-full border-t-[1px] p-3"
>
<div class="flex justify-end">
<Space v-if="type === 'myself'">
<a-button
v-if="revocable"
danger
type="primary"
@click="handleCancel"
>
撤销申请
</a-button>
<a-button v-if="editableAndRemoveable" @click="handleEdit">
重新编辑
</a-button>
<a-button
v-if="editableAndRemoveable"
danger
type="primary"
@click="handleRemove"
>
删除
</a-button>
</Space>
<Space v-if="type === 'approve'">
<a-button type="primary" @click="handleApproval">通过</a-button>
<a-button danger type="primary" @click="handleTermination">
终止
</a-button>
<a-button danger type="primary" @click="handleRejection">
驳回
</a-button>
<Dropdown
:get-popup-container="getPopupContainer"
placement="bottomRight"
>
<template #overlay>
<Menu>
<MenuItem key="1" @click="() => delegationModalApi.open()">
委托
</MenuItem>
<MenuItem key="2" @click="() => transferModalApi.open()">
转办
</MenuItem>
<MenuItem
v-if="showMultiActions"
key="3"
@click="() => addSignatureModalApi.open()"
>
加签
</MenuItem>
<MenuItem
v-if="showMultiActions"
key="4"
@click="() => reductionSignatureModalApi.open()"
>
减签
</MenuItem>
</Menu>
</template>
<a-button> 其他 </a-button>
</Dropdown>
<ApprovalModal @complete="$emit('reload')" />
<RejectionModal @complete="$emit('reload')" />
<DelegationModal mode="single" @finish="handleDelegation" />
<TransferModal mode="single" @finish="handleTransfer" />
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
<ReductionSignatureModal
mode="multiple"
@finish="handleReductionSignature"
/>
</Space>
<Space v-if="type === 'admin'">
<a-button @click="handleFlowInterfere"> 流程干预 </a-button>
<a-button @click="() => updateAssigneeModalApi.open()">
修改办理人
</a-button>
<FlowInterfereModal @complete="$emit('reload')" />
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
</Space>
</div>
</div>
</Card>
<Fallback v-else title="点击左侧选择" />
</template>

View File

@@ -0,0 +1,131 @@
<!-- 审批驳回窗口 -->
<script setup lang="ts">
import { useVbenModal } from '@vben/common-ui';
import { cloneDeep, getPopupContainer } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import { backProcess, getBackTaskNode } from '#/api/workflow/task';
const emit = defineEmits<{ complete: [] }>();
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
// 默认占满两列
formItemClass: 'col-span-2',
// 默认label宽度 px
labelWidth: 100,
// 通用配置项 会影响到所有表单项
componentProps: {
class: 'w-full',
},
},
schema: [
{
fieldName: 'taskId',
component: 'Input',
label: '任务ID',
dependencies: {
show: false,
triggerFields: [''],
},
},
{
fieldName: 'messageType',
component: 'CheckboxGroup',
componentProps: {
options: [
{ label: '站内信', value: '1', disabled: true },
{ label: '邮件', value: '2' },
{ label: '短信', value: '3' },
],
},
label: '通知方式',
defaultValue: ['1'],
},
{
fieldName: 'nodeCode',
component: 'Select',
componentProps: {
getPopupContainer,
},
label: '驳回节点',
},
{
fieldName: 'message',
component: 'Textarea',
label: '审批意见',
formItemClass: 'items-baseline',
},
],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
interface ModalProps {
taskId: string;
definitionId: string;
nodeCode: string;
}
const [BasicModal, modalApi] = useVbenModal({
title: '审批驳回',
fullscreenButton: false,
class: 'min-h-[365px]',
onConfirm: handleSubmit,
async onOpenChange(isOpen) {
if (!isOpen) {
await formApi.resetForm();
return null;
}
modalApi.modalLoading(true);
const { taskId, definitionId, nodeCode } = modalApi.getData() as ModalProps;
await formApi.setFieldValue('taskId', taskId);
const resp = await getBackTaskNode(definitionId, nodeCode);
const options = resp.map((item) => ({
label: item.nodeName,
value: item.nodeCode,
}));
formApi.updateSchema([
{
fieldName: 'nodeCode',
componentProps: {
options,
},
},
]);
// 默认选中第一个节点
if (options.length > 0) {
formApi.setFieldValue('nodeCode', options[0]?.value);
}
modalApi.modalLoading(false);
},
});
async function handleSubmit() {
try {
modalApi.modalLoading(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
const data = cloneDeep(await formApi.getValues());
console.log(data);
await backProcess(data);
modalApi.close();
emit('complete');
} catch (error) {
console.error(error);
} finally {
modalApi.modalLoading(false);
}
}
</script>
<template>
<BasicModal>
<BasicForm />
</BasicModal>
</template>

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import type { Flow } from '#/api/workflow/instance/model';
import { onMounted, ref } from 'vue';
import { VbenAvatar } from '@vben/common-ui';
import { DictEnum } from '@vben/constants';
import { TimelineItem } from 'ant-design-vue';
import { ossInfo } from '#/api/system/oss';
import { renderDict } from '#/utils/render';
defineOptions({
name: 'ApprovalTimelineItem',
inheritAttrs: false,
});
const props = defineProps<{ item: Flow }>();
interface AttachmentInfo {
ossId: string;
url: string;
name: string;
}
/**
* 处理附件信息
*/
const attachmentInfo = ref<AttachmentInfo[]>([]);
onMounted(async () => {
if (!props.item.ext) {
return null;
}
const resp = await ossInfo(props.item.ext.split(','));
attachmentInfo.value = resp.map((item) => ({
ossId: item.ossId,
url: item.url,
name: item.originalName,
}));
});
</script>
<template>
<TimelineItem :key="item.id">
<template #dot>
<div class="relative rounded-full border">
<VbenAvatar
:alt="item.approveName"
class="bg-primary size-[36px] rounded-full text-white"
src=""
/>
</div>
</template>
<div class="ml-2 flex flex-col gap-0.5">
<div class="flex items-center gap-1">
<div class="font-bold">{{ item.nodeName }}</div>
<component :is="renderDict(item.flowStatus, DictEnum.WF_TASK_STATUS)" />
</div>
<div>{{ item.approveName }}</div>
<div>{{ item.updateTime }}</div>
<div v-if="item.message" class="rounded-lg border p-1">
<div class="break-all opacity-70">{{ item.message }}</div>
</div>
<div v-if="attachmentInfo.length > 0" class="flex flex-wrap gap-2">
<!-- 这里下载的文件名不是原始文件名 -->
<a
v-for="attachment in attachmentInfo"
:key="attachment.ossId"
:href="attachment.url"
class="text-primary"
target="_blank"
>
<div class="flex items-center gap-1">
<span class="icon-[mingcute--attachment-line] size-[18px]"></span>
<span>{{ attachment.name }}</span>
</div>
</a>
</div>
</div>
</TimelineItem>
</template>

View File

@@ -1,50 +1,21 @@
<script setup lang="ts">
import { Timeline, TimelineItem } from 'ant-design-vue';
import type { Flow } from '#/api/workflow/instance/model';
/**
* TODO: 仅为demo 后期会替换
*/
import { VbenAvatar } from '../../../../../../packages/@core/ui-kit/shadcn-ui/src/components';
import { Timeline } from 'ant-design-vue';
interface ApprovalItem {
id: string;
name: string;
status: string;
remark?: string;
time: string;
}
import ApprovalTimelineItem from './approval-timeline-item.vue';
const props = defineProps<{
list: ApprovalItem[];
list: Flow[];
}>();
</script>
<template>
<Timeline>
<TimelineItem v-for="item in props.list" :key="item.id">
<template #dot>
<div class="relative rounded-full border">
<VbenAvatar
class="size-[36px]"
src="https://plus.dapdap.top/minio-server/plus/2024/11/21/925ed278e2d441beb7f695b41e13c4dd.jpg"
/>
<div
class="border-background absolute bottom-0 right-0 size-[16px] rounded-full border-2 bg-green-500 content-['']"
>
<div class="flex items-center justify-center">
<span class="icon-[mdi--success-bold] text-white"></span>
</div>
</div>
</div>
</template>
<div class="ml-2 flex flex-col">
<div>发起人</div>
<div>疯狂的牛子Li</div>
<div>2022-01-01 12:00:00</div>
<div class="rounded-lg border p-1">
<span class="opacity-70">这里是备注信息</span>
</div>
</div>
</TimelineItem>
<Timeline v-if="props.list.length > 0">
<ApprovalTimelineItem
v-for="(item, index) in props.list"
:key="index"
:item="item"
/>
</Timeline>
</template>

View File

@@ -0,0 +1,85 @@
<!--抄送组件-->
<script setup lang="ts">
import type { User } from '#/api/system/user/model';
import { computed, type PropType } from 'vue';
import { useVbenModal, VbenAvatar } from '@vben/common-ui';
import { Avatar, AvatarGroup, Tooltip } from 'ant-design-vue';
import { userSelectModal } from '.';
defineOptions({
name: 'CopyComponent',
inheritAttrs: false,
});
const props = withDefaults(defineProps<{ ellipseNumber?: number }>(), {
/**
* 最大显示的头像数量 超过显示为省略号头像
*/
ellipseNumber: 3,
});
const emit = defineEmits<{ cancel: []; finish: [User[]] }>();
const [UserSelectModal, modalApi] = useVbenModal({
connectedComponent: userSelectModal,
});
const userListModel = defineModel('userList', {
type: Array as PropType<User[]>,
default: () => [],
});
function handleOpen() {
modalApi.setData({ userList: userListModel.value });
modalApi.open();
}
function handleFinish(userList: User[]) {
// 清空 直接赋值[]会丢失响应性
userListModel.value.splice(0, userListModel.value.length);
userListModel.value.push(...userList);
emit('finish', userList);
}
const displayedList = computed(() => {
return userListModel.value.slice(0, props.ellipseNumber);
});
</script>
<template>
<div class="flex items-center gap-2">
<AvatarGroup v-if="userListModel.length > 0">
<Tooltip
v-for="user in displayedList"
:key="user.userId"
:title="user.nickName"
placement="top"
>
<div>
<VbenAvatar
:alt="user.nickName"
class="bg-primary size-[36px] cursor-pointer rounded-full border text-white"
src=""
/>
</div>
</Tooltip>
<Tooltip
:title="`等${userListModel.length - props.ellipseNumber}人`"
placement="top"
>
<Avatar
v-if="userListModel.length > ellipseNumber"
class="flex size-[36px] cursor-pointer items-center justify-center rounded-full border bg-[gray] text-white"
>
+{{ userListModel.length - props.ellipseNumber }}
</Avatar>
</Tooltip>
</AvatarGroup>
<a-button size="small" @click="handleOpen">选择人员</a-button>
<UserSelectModal @cancel="$emit('cancel')" @finish="handleFinish" />
</div>
</template>

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { useAppConfig, useTabs } from '@vben/hooks';
import { stringify } from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { useEventListener } from '@vueuse/core';
defineOptions({ name: 'FlowDesigner' });
const route = useRoute();
const definitionId = route.query.definitionId as string;
const disabled = route.query.disabled === 'true';
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
const accessStore = useAccessStore();
const params = {
Authorization: `Bearer ${accessStore.accessToken}`,
id: definitionId,
clientid: clientId,
disabled,
};
/**
* iframe设计器的地址
*/
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
const { closeCurrentTab } = useTabs();
const router = useRouter();
function messageHandler(event: MessageEvent) {
switch (event.data.method) {
case 'close': {
// 关闭当前tab
closeCurrentTab();
// 跳转到流程定义列表
router.push('/workflow/processDefinition');
break;
}
}
}
// iframe监听组件内设计器保存事件
useEventListener('message', messageHandler);
</script>
<template>
<iframe :src="url" class="size-full"></iframe>
</template>

View File

@@ -0,0 +1,38 @@
<!-- 弹窗查看流程信息 -->
<script setup lang="ts">
import type { TaskInfo } from '#/api/workflow/task/model';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { getTaskByBusinessId } from '#/api/workflow/instance';
import { ApprovalPanel } from '.';
interface ModalProps {
businessId: string;
}
const taskInfo = ref<TaskInfo>();
const [BasicModal, modalApi] = useVbenModal({
title: '流程信息',
class: 'w-[1000px]',
footer: false,
onOpenChange: async (isOpen) => {
if (!isOpen) {
return null;
}
const { businessId } = modalApi.getData() as ModalProps;
const taskResp = await getTaskByBusinessId(businessId);
taskInfo.value = taskResp;
},
});
</script>
<template>
<BasicModal>
<ApprovalPanel :task="taskInfo" type="readonly" />
</BasicModal>
</template>

View File

@@ -0,0 +1,171 @@
<script setup lang="ts">
import type { User } from '#/api/system/user/model';
import type { TaskInfo } from '#/api/workflow/task/model';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Descriptions, DescriptionsItem, Modal } from 'ant-design-vue';
import {
getTaskByTaskId,
taskOperation,
terminationTask,
} from '#/api/workflow/task';
import { userSelectModal } from '.';
const emit = defineEmits<{ complete: [] }>();
const taskInfo = ref<TaskInfo>();
/**
* 是否显示 加签/减签操作
*/
const showMultiActions = computed(() => {
if (!taskInfo.value) {
return false;
}
if (Number(taskInfo.value.nodeRatio) > 0) {
return true;
}
return false;
});
const [BasicModal, modalApi] = useVbenModal({
title: '流程干预',
class: 'w-[800px]',
fullscreenButton: false,
async onOpenChange(isOpen) {
if (!isOpen) {
return null;
}
const { taskId } = modalApi.getData() as { taskId: string };
taskInfo.value = await getTaskByTaskId(taskId);
},
});
/**
* 转办
*/
const [TransferModal, transferModalApi] = useVbenModal({
connectedComponent: userSelectModal,
});
function handleTransfer(userList: User[]) {
if (userList.length === 0 || !taskInfo.value) return;
const current = userList[0];
Modal.confirm({
title: '转办',
content: `确定转办给${current?.nickName}吗?`,
centered: true,
onOk: async () => {
await taskOperation(
{ taskId: taskInfo.value!.id, userId: current!.userId },
'transferTask',
);
emit('complete');
},
});
}
/**
* 审批终止
*/
function handleTermination() {
if (!taskInfo.value) {
return;
}
Modal.confirm({
title: '审批终止',
content: '确定终止当前审批流程吗?',
centered: true,
okButtonProps: { danger: true },
onOk: async () => {
await terminationTask({ taskId: taskInfo.value!.id });
emit('complete');
},
});
}
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
connectedComponent: userSelectModal,
});
function handleAddSignature(userList: User[]) {
if (userList.length === 0 || !taskInfo.value) return;
const userIds = userList.map((user) => user.userId);
Modal.confirm({
title: '提示',
content: '确认加签吗?',
centered: true,
onOk: async () => {
await taskOperation(
{ taskId: taskInfo.value!.id, userIds },
'addSignature',
);
emit('complete');
},
});
}
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
connectedComponent: userSelectModal,
});
function handleReductionSignature(userList: User[]) {
if (userList.length === 0 || !taskInfo.value) return;
const userIds = userList.map((user) => user.userId);
Modal.confirm({
title: '提示',
content: '确认减签吗?',
centered: true,
onOk: async () => {
await taskOperation(
{ taskId: taskInfo.value!.id, userIds },
'reductionSignature',
);
emit('complete');
},
});
}
</script>
<template>
<BasicModal>
<Descriptions v-if="taskInfo" :column="2" bordered size="small">
<DescriptionsItem label="任务名称">
{{ taskInfo.nodeName }}
</DescriptionsItem>
<DescriptionsItem label="节点编码">
{{ taskInfo.nodeCode }}
</DescriptionsItem>
<DescriptionsItem label="开始时间">
{{ taskInfo.createTime }}
</DescriptionsItem>
<DescriptionsItem label="流程实例ID">
{{ taskInfo.instanceId }}
</DescriptionsItem>
<DescriptionsItem label="版本号">
{{ taskInfo.version }}
</DescriptionsItem>
<DescriptionsItem label="业务ID">
{{ taskInfo.businessId }}
</DescriptionsItem>
</Descriptions>
<TransferModal mode="single" @finish="handleTransfer" />
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
<ReductionSignatureModal
mode="multiple"
@finish="handleReductionSignature"
/>
<template #footer>
<template v-if="showMultiActions">
<a-button @click="() => addSignatureModalApi.open()">加签</a-button>
<a-button @click="() => reductionSignatureModalApi.open()">
减签
</a-button>
</template>
<a-button @click="() => transferModalApi.open()">转办</a-button>
<a-button danger type="primary" @click="handleTermination">终止</a-button>
</template>
</BasicModal>
</template>

View File

@@ -0,0 +1,60 @@
import { defineComponent, h, ref } from 'vue';
import { Modal } from 'ant-design-vue';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import ApprovalContent from './approval-content.vue';
export interface ApproveWithReasonModalProps {
title: string;
description: string;
onOk: (reason: string) => void;
}
/**
* 带审批意见的confirm
* @param props props
*/
export function approveWithReasonModal(props: ApproveWithReasonModalProps) {
const { onOk, title, description } = props;
const content = ref('');
Modal.confirm({
title,
content: h(
defineComponent({
setup() {
return () =>
h(ApprovalContent, {
description,
value: content.value,
'onUpdate:value': (v) => (content.value = v),
});
},
}),
),
centered: true,
okButtonProps: { danger: true },
onOk: () => onOk(content.value),
});
}
dayjs.extend(duration);
dayjs.extend(relativeTime);
/**
* 计算相差的时间
* @param dateTime 时间字符串
* @returns 相差的时间
*/
export function getDiffTimeString(dateTime: string) {
// 计算相差秒数
const diffSeconds = dayjs().diff(dayjs(dateTime), 'second');
/**
* 转为时间显示(x月 x天)
* https://dayjs.fenxianglu.cn/category/duration.html#%E4%BA%BA%E6%80%A7%E5%8C%96
*
*/
const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();
return diffText;
}

View File

@@ -1,2 +1,30 @@
export { default as applyModal } from './apply-modal.vue';
export { default as ApprovalCard } from './approval-card.vue';
/**
* 审批同意
*/
export { default as approvalModal } from './approval-modal.vue';
export { default as ApprovalPanel } from './approval-panel.vue';
/**
* 审批驳回
*/
export { default as approvalRejectionModal } from './approval-rejection-modal.vue';
export { default as ApprovalTimeline } from './approval-timeline.vue';
/**
* 选择抄送人
*/
export { default as CopyComponent } from './copy-component.vue';
/**
* 详情信息 modal
*/
export { default as flowInfoModal } from './flow-info-modal.vue';
/**
* 流程干预 modal
*/
export { default as flowInterfereModal } from './flow-interfere-modal.vue';
/**
* 选人 支持单选/多选
*/
export { default as userSelectModal } from './user-select-modal.vue';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,365 @@
<!-- eslint-disable no-use-before-define -->
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { User } from '#/api';
import { ref } from 'vue';
import { useVbenModal, VbenAvatar } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { userList } from '#/api/system/user';
import DeptTree from '#/views/system/user/dept-tree.vue';
defineOptions({
name: 'UserSelectModal',
inheritAttrs: false,
});
const props = withDefaults(defineProps<{ mode?: 'multiple' | 'single' }>(), {
mode: 'multiple',
});
const emit = defineEmits<{
/**
* 取消的事件
*/
cancel: [];
/**
* 选择完成的事件
*/
finish: [User[]];
}>();
const [BasicModal, modalApi] = useVbenModal({
title: '选择人员',
class: 'w-[1060px]',
fullscreenButton: false,
onClosed: () => emit('cancel'),
onConfirm: handleSubmit,
async onOpened() {
const { userList = [] } = modalApi.getData() as { userList: User[] };
// 暂时只处理多选 目前并没有单选的情况
if (props.mode === 'multiple') {
// 左边选中
await tableApi.grid.setCheckboxRow(userList, true);
// 右边赋值
await rightTableApi.grid.loadData(userList);
}
},
});
// 左边部门用
const selectDeptId = ref<string[]>([]);
const formOptions: VbenFormProps = {
schema: [
{
component: 'Input',
fieldName: 'userName',
label: '用户账号',
hideLabel: true,
componentProps: {
placeholder: '请输入账号',
},
},
],
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
wrapperClass: 'grid-cols-2',
handleReset: async () => {
selectDeptId.value = [];
const { formApi, reload } = tableApi;
await formApi.resetForm();
const formValues = formApi.form.values;
formApi.setLatestSubmissionValues(formValues);
await reload(formValues);
},
};
const gridOptions: VxeGridProps = {
checkboxConfig: {
// 翻页时保留选中状态
reserve: true,
// 点击行选中
trigger: 'row',
},
radioConfig: {
trigger: 'row',
strict: true,
},
columns: [
{
type: props.mode === 'single' ? 'radio' : 'checkbox',
width: 60,
resizable: false,
},
{
field: 'userName',
title: '用户',
headerAlign: 'left',
resizable: false,
slots: {
default: 'user',
},
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
layouts: ['PrevPage', 'Number', 'NextPage', 'Sizes', 'Total'],
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
// 部门树选择处理
if (selectDeptId.value.length === 1) {
formValues.deptId = selectDeptId.value[0];
} else {
Reflect.deleteProperty(formValues, 'deptId');
}
// 加载完毕需要设置选中的行
if (props.mode === 'multiple') {
const records = rightTableApi.grid.getData();
await tableApi.grid.setCheckboxRow(records, true);
}
if (props.mode === 'single') {
const records = rightTableApi.grid.getData();
if (records.length === 1) {
await tableApi.grid.setRadioRow(records[0]);
}
}
return await userList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'userId',
},
toolbarConfig: {
enabled: false,
},
showOverflow: false,
};
const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents: {
// 需要控制不同的事件 radio也会触发checkbox事件
checkboxChange: checkBoxEvent,
checkboxAll: checkBoxEvent,
radioChange: radioEvent,
},
});
function checkBoxEvent() {
if (props.mode !== 'multiple') {
return;
}
/**
* 给右边表格赋值
* records拿到的是当前页的选中数据
* reserveRecords拿到的是其他页选中的数据
*/
const records = tableApi.grid.getCheckboxRecords();
const reserveRecords = tableApi.grid.getCheckboxReserveRecords();
const realRecords = [...records, ...reserveRecords];
rightTableApi.grid.loadData(realRecords);
}
function radioEvent() {
if (props.mode !== 'single') {
return;
}
// 给右边表格赋值
const records = tableApi.grid.getRadioRecord();
rightTableApi.grid.loadData([records]);
}
const rightGridOptions: VxeGridProps = {
checkboxConfig: {},
columns: [
{
field: 'nickName',
title: '昵称',
width: 200,
resizable: false,
slots: {
default: 'user',
},
},
{
field: 'action',
title: '操作',
width: 120,
slots: { default: 'action' },
},
],
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
enabled: false,
},
rowConfig: {
keyField: 'userId',
},
toolbarConfig: {
enabled: false,
},
showOverflow: false,
};
const [RightBasicTable, rightTableApi] = useVbenVxeGrid({
gridOptions: rightGridOptions,
});
async function handleRemoveItem(row: any) {
if (props.mode === 'multiple') {
await tableApi.grid.setCheckboxRow(row, false);
}
if (props.mode === 'single') {
await tableApi.grid.clearRadioRow();
}
const data = rightTableApi.grid.getData();
await rightTableApi.grid.loadData(data.filter((item) => item !== row));
// 这个方法有问题
// await rightTableApi.grid.remove(row);
}
function handleRemoveAll() {
if (props.mode === 'multiple') {
tableApi.grid.clearCheckboxRow();
tableApi.grid.clearCheckboxReserve();
}
if (props.mode === 'single') {
tableApi.grid.clearRadioRow();
}
rightTableApi.grid.loadData([]);
}
async function handleDeptQuery() {
await tableApi.reload();
// 重置后恢复 保存勾选的数据
const records = rightTableApi.grid.getData();
if (props.mode === 'multiple') {
tableApi?.grid.setCheckboxRow(records, true);
}
if (props.mode === 'single' && records.length === 1) {
tableApi.grid.setRadioRow(records[0]);
}
}
function handleSubmit() {
const records = rightTableApi.grid.getData();
console.log(records);
emit('finish', records);
modalApi.close();
}
</script>
<template>
<BasicModal>
<div class="flex min-h-[600px]">
<DeptTree
v-model:select-dept-id="selectDeptId"
:show-search="false"
class="w-[230px]"
@reload="() => tableApi.reload()"
@select="handleDeptQuery"
/>
<div class="h-[600px] w-[450px]">
<BasicTable>
<template #user="{ row }">
<div class="flex items-center gap-2">
<VbenAvatar
:alt="row.nickName"
:src="row.avatar ?? ''"
:class="{ 'bg-primary': !row.avatar }"
class="size-[32px] rounded-full text-white"
/>
<div class="flex flex-col items-baseline text-[12px]">
<div>{{ row.nickName }}</div>
<div class="opacity-50">
{{ row.phonenumber || '暂无手机号' }}
</div>
</div>
</div>
</template>
</BasicTable>
</div>
<div class="flex h-[600px] flex-col">
<div class="flex w-full px-4">
<div class="flex w-full items-center justify-between">
<div>已选中人员</div>
<div>
<a-button size="small" @click="handleRemoveAll">
清空选中
</a-button>
</div>
</div>
</div>
<RightBasicTable id="user-select-right-table">
<template #user="{ row }">
<div class="flex items-center gap-2 overflow-hidden">
<VbenAvatar
:alt="row.nickName"
:src="row.avatar ?? ''"
:class="{ 'bg-primary': !row.avatar }"
class="size-[32px] rounded-full text-white"
/>
<div class="flex flex-col items-baseline text-[12px]">
<div class="overflow-ellipsis whitespace-nowrap">
{{ row.nickName }}
</div>
<div class="opacity-50">
{{ row.phonenumber || '暂无手机号' }}
</div>
</div>
</div>
</template>
<template #action="{ row }">
<a-button size="small" @click="handleRemoveItem(row)">
移除
</a-button>
</template>
</RightBasicTable>
</div>
</div>
</BasicModal>
</template>
<style scoped>
:deep(div.vben-link) {
display: none;
}
:deep(.vxe-body--row) {
cursor: pointer;
}
</style>
<style lang="scss">
/**
默认显示右边的滚动条 防止出现滚动条被挤压
*/
#user-select-right-table {
div.vxe-table--body-wrapper.body--wrapper {
overflow: scroll;
}
}
</style>

View File

@@ -1,9 +0,0 @@
<script setup lang="ts">
import CommonSkeleton from '#/views/common';
</script>
<template>
<div>
<CommonSkeleton />
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More