mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-04-18 10:13:16 +08:00
Compare commits
70 Commits
1.4.0-back
...
1.4.1-back
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
656d53a059 | ||
|
|
190c8c586e | ||
|
|
5767af5269 | ||
|
|
d3f4b936fb | ||
|
|
b78bc65ce7 | ||
|
|
b1fb623113 | ||
|
|
de14908fd3 | ||
|
|
5c3972196a | ||
|
|
3230781538 | ||
|
|
e7fd0e3b6a | ||
|
|
fb64d9f87a | ||
|
|
c4e3ff14b3 | ||
|
|
04796449da | ||
|
|
6ced4a44c8 | ||
|
|
3ebf0ac7df | ||
|
|
47ae02c571 | ||
|
|
f16bfe2cd0 | ||
|
|
383756c0aa | ||
|
|
2f7d1f009d | ||
|
|
946f91f387 | ||
|
|
445e6011da | ||
|
|
afc2a3de58 | ||
|
|
cb83bca12d | ||
|
|
f7ae821dc2 | ||
|
|
b737fa940a | ||
|
|
986eacae9a | ||
|
|
ce6867994a | ||
|
|
e10ddb421c | ||
|
|
af0bb9bd66 | ||
|
|
aec123a834 | ||
|
|
c09c089265 | ||
|
|
97b8e28a2b | ||
|
|
4baa0aed8b | ||
|
|
b2d3cf10aa | ||
|
|
63d2b38fd1 | ||
|
|
78cd6677c3 | ||
|
|
c0962fec18 | ||
|
|
d38093ca7d | ||
|
|
687c33ec29 | ||
|
|
8ba7bdf2bd | ||
|
|
b015fbc9fc | ||
|
|
b69320c070 | ||
|
|
dcccc213ce | ||
|
|
c0e601c020 | ||
|
|
017ed1a9e1 | ||
|
|
598f371568 | ||
|
|
ca2aadaf4a | ||
|
|
616db1c127 | ||
|
|
08de1a6f19 | ||
|
|
006370798b | ||
|
|
831700660c | ||
|
|
a53b9382f5 | ||
|
|
703586123a | ||
|
|
0295418f79 | ||
|
|
14b0d9b50f | ||
|
|
b9aef618fe | ||
|
|
4102cc2211 | ||
|
|
ea776aa710 | ||
|
|
feb96dc8ea | ||
|
|
470fd43b49 | ||
|
|
76d106e474 | ||
|
|
78c3c9da6f | ||
|
|
8b7d717b21 | ||
|
|
081d08a7f8 | ||
|
|
0da75418d0 | ||
|
|
55f0da3085 | ||
|
|
3849800388 | ||
|
|
f913955259 | ||
|
|
8d6ef40d3e | ||
|
|
96a10ca83f |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -235,12 +235,14 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"archiver",
|
"archiver",
|
||||||
"axios",
|
"axios",
|
||||||
|
"Cascader",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"isequal",
|
"isequal",
|
||||||
"jspm",
|
"jspm",
|
||||||
"napi",
|
"napi",
|
||||||
"nolebase",
|
"nolebase",
|
||||||
"rollup",
|
"rollup",
|
||||||
|
"tinymce",
|
||||||
"vitest"
|
"vitest"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,15 +1,34 @@
|
|||||||
|
# 1.4.1
|
||||||
|
|
||||||
|
**FEATURES**
|
||||||
|
|
||||||
|
- Tinymce添加在antd原生表单/useVbenForm下的校验样式
|
||||||
|
- useVbenForm 增加 Cascader(级联选择器) 组件
|
||||||
|
|
||||||
|
**BUG FIX**
|
||||||
|
|
||||||
|
- 菜单管理 路由地址的必填项不生效
|
||||||
|
- withDefaultPlaceholder中placeholder 在keepalive & 语言切换 & tab切换 显示不变的问题
|
||||||
|
|
||||||
|
**REFACTOR**
|
||||||
|
|
||||||
|
- 字典接口抛出异常(为什么会抛出异常?)无限调用接口 兼容处理
|
||||||
|
- 代码生成 字典下拉加载 改为每次进入编辑页面都加载
|
||||||
|
- ~~个人中心 账号绑定 样式/逻辑重构~~(回滚了 既要又要的问题)
|
||||||
|
- ~~个人中心 下拉卡片 昵称超长省略显示~~(回滚了 既要又要的问题)
|
||||||
|
|
||||||
# 1.4.0
|
# 1.4.0
|
||||||
|
|
||||||
**FEATURES**
|
**FEATURES**
|
||||||
|
|
||||||
- 菜单管理(通用方法) 保存表格滚动/展开状态并执行回调 用于树表在执行 新增/编辑/删除等操作后 依然在当前位置(体验优化)
|
- 菜单管理(通用方法) 保存表格滚动/展开状态并执行回调 用于树表在执行 新增/编辑/删除等操作后 依然在当前位置(体验优化)
|
||||||
|
-
|
||||||
- 菜单管理 级联删除 删除菜单和children
|
- 菜单管理 级联删除 删除菜单和children
|
||||||
|
|
||||||
**REFACTOR**
|
**REFACTOR**
|
||||||
|
|
||||||
- 除个人中心外所有本地路由改为从后端返回(需要执行更新sql)
|
- 除个人中心外所有本地路由改为从后端返回(需要执行更新sql)
|
||||||
- 流程图预览改为logicflow预览而非图片
|
- 流程图预览改为logicflow预览而非图片 ...然后后端又更新了 又改成iframe了
|
||||||
- 菜单管理 新增角色校验(与后端权限保持一致) 只有superadmin可进行增删改
|
- 菜单管理 新增角色校验(与后端权限保持一致) 只有superadmin可进行增删改
|
||||||
|
|
||||||
# 1.3.6
|
# 1.3.6
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/web-antd",
|
"name": "@vben/web-antd",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"homepage": "https://vben.pro",
|
"homepage": "https://vben.pro",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^7.0.1",
|
"@ant-design/icons-vue": "^7.0.1",
|
||||||
"@logicflow/core": "^2.0.13",
|
|
||||||
"@tinymce/tinymce-vue": "^6.0.1",
|
"@tinymce/tinymce-vue": "^6.0.1",
|
||||||
"@vben/access": "workspace:*",
|
"@vben/access": "workspace:*",
|
||||||
"@vben/common-ui": "workspace:*",
|
"@vben/common-ui": "workspace:*",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
|
|||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
computed,
|
||||||
defineAsyncComponent,
|
defineAsyncComponent,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
@@ -39,6 +40,9 @@ const AutoComplete = defineAsyncComponent(
|
|||||||
() => import('ant-design-vue/es/auto-complete'),
|
() => import('ant-design-vue/es/auto-complete'),
|
||||||
);
|
);
|
||||||
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
||||||
|
const Cascader = defineAsyncComponent(
|
||||||
|
() => import('ant-design-vue/es/cascader'),
|
||||||
|
);
|
||||||
const Checkbox = defineAsyncComponent(
|
const Checkbox = defineAsyncComponent(
|
||||||
() => import('ant-design-vue/es/checkbox'),
|
() => import('ant-design-vue/es/checkbox'),
|
||||||
);
|
);
|
||||||
@@ -90,10 +94,13 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
name: component.name,
|
name: component.name,
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
setup: (props: any, { attrs, expose, slots }) => {
|
setup: (props: any, { attrs, expose, slots }) => {
|
||||||
const placeholder =
|
// 改为placeholder 解决在keepalive & 语言切换 & tab切换 显示不变的问题
|
||||||
|
const computedPlaceholder = computed(
|
||||||
|
() =>
|
||||||
props?.placeholder ||
|
props?.placeholder ||
|
||||||
attrs?.placeholder ||
|
attrs?.placeholder ||
|
||||||
$t(`ui.placeholder.${type}`);
|
$t(`ui.placeholder.${type}`),
|
||||||
|
);
|
||||||
|
|
||||||
// 透传组件暴露的方法
|
// 透传组件暴露的方法
|
||||||
const innerRef = ref();
|
const innerRef = ref();
|
||||||
@@ -112,7 +119,7 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
component,
|
component,
|
||||||
{
|
{
|
||||||
...componentProps,
|
...componentProps,
|
||||||
placeholder,
|
placeholder: computedPlaceholder.value,
|
||||||
...props,
|
...props,
|
||||||
...attrs,
|
...attrs,
|
||||||
ref: innerRef,
|
ref: innerRef,
|
||||||
@@ -128,6 +135,7 @@ export type ComponentType =
|
|||||||
| 'ApiSelect'
|
| 'ApiSelect'
|
||||||
| 'ApiTreeSelect'
|
| 'ApiTreeSelect'
|
||||||
| 'AutoComplete'
|
| 'AutoComplete'
|
||||||
|
| 'Cascader'
|
||||||
| 'Checkbox'
|
| 'Checkbox'
|
||||||
| 'CheckboxGroup'
|
| 'CheckboxGroup'
|
||||||
| 'DatePicker'
|
| 'DatePicker'
|
||||||
@@ -191,6 +199,7 @@ async function initComponentAdapter() {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
|
Cascader: withDefaultPlaceholder(Cascader, 'select'),
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
|
|||||||
@@ -50,13 +50,15 @@ setupVbenVxeTable({
|
|||||||
// 右上角工具栏
|
// 右上角工具栏
|
||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
// 自定义列
|
// 自定义列
|
||||||
custom: {
|
custom: true,
|
||||||
|
customOptions: {
|
||||||
icon: 'vxe-icon-setting',
|
icon: 'vxe-icon-setting',
|
||||||
},
|
},
|
||||||
// 最大化
|
// 最大化
|
||||||
zoom: true,
|
zoom: true,
|
||||||
// 刷新
|
// 刷新
|
||||||
refresh: {
|
refresh: true,
|
||||||
|
refreshOptions: {
|
||||||
// 默认为reload 修改为在当前页刷新
|
// 默认为reload 修改为在当前页刷新
|
||||||
code: 'query',
|
code: 'query',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export interface OnlineUser {
|
|||||||
browser: string;
|
browser: string;
|
||||||
os: string;
|
os: string;
|
||||||
loginTime: number;
|
loginTime: number;
|
||||||
|
deviceType: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ const { apiURL, clientId, enableEncrypt } = useAppConfig(
|
|||||||
*/
|
*/
|
||||||
let isLogoutProcessing = false;
|
let isLogoutProcessing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义一个401专用异常 用于可能会用到的区分场景?
|
||||||
|
*/
|
||||||
|
export class UnauthorizedException extends Error {}
|
||||||
|
|
||||||
function createRequestClient(baseURL: string) {
|
function createRequestClient(baseURL: string) {
|
||||||
const client = new RequestClient({
|
const client = new RequestClient({
|
||||||
// 后端地址
|
// 后端地址
|
||||||
@@ -228,7 +233,7 @@ function createRequestClient(baseURL: string) {
|
|||||||
case 401: {
|
case 401: {
|
||||||
// 已经在登出过程中 不再执行
|
// 已经在登出过程中 不再执行
|
||||||
if (isLogoutProcessing) {
|
if (isLogoutProcessing) {
|
||||||
throw new Error(timeoutMsg);
|
throw new UnauthorizedException(timeoutMsg);
|
||||||
}
|
}
|
||||||
isLogoutProcessing = true;
|
isLogoutProcessing = true;
|
||||||
const _msg = $t('http.loginTimeout');
|
const _msg = $t('http.loginTimeout');
|
||||||
@@ -238,7 +243,7 @@ function createRequestClient(baseURL: string) {
|
|||||||
isLogoutProcessing = false;
|
isLogoutProcessing = false;
|
||||||
});
|
});
|
||||||
// 不再执行下面逻辑
|
// 不再执行下面逻辑
|
||||||
throw new Error(_msg);
|
throw new UnauthorizedException(_msg);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function pageByCurrent(params?: PageQuery) {
|
|||||||
*/
|
*/
|
||||||
export function flowInfo(businessId: string) {
|
export function flowInfo(businessId: string) {
|
||||||
return requestClient.get<FlowInfoResponse>(
|
return requestClient.get<FlowInfoResponse>(
|
||||||
`/workflow/instance/flowImage/${businessId}`,
|
`/workflow/instance/flowHisTaskList/${businessId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,6 @@ export interface Flow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FlowInfoResponse {
|
export interface FlowInfoResponse {
|
||||||
image: string;
|
instanceId: string;
|
||||||
list: Flow[];
|
list: Flow[];
|
||||||
defChart: {
|
|
||||||
defJson: Record<string, any>;
|
|
||||||
nodeJsonList: Record<string, any>[];
|
|
||||||
skipJsonList: Record<string, any>[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,13 +202,31 @@ const events = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
/***
|
// 展开层元素z-index
|
||||||
由于modal/drawer的zIndex升级后为2000
|
$dropdown-index: 2025;
|
||||||
这里会造成遮挡 修改为更高的zIndex
|
|
||||||
*/
|
@mixin tinymce-valid-fail($color) {
|
||||||
|
.app-tinymce {
|
||||||
|
// 最外层的tinymce容器
|
||||||
|
.tox-tinymce {
|
||||||
|
border-color: $color;
|
||||||
|
}
|
||||||
|
// focus样式
|
||||||
|
.tox .tox-edit-area::before {
|
||||||
|
border-color: $color;
|
||||||
|
border-right: none;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tox.tox-silver-sink.tox-tinymce-aux {
|
.tox.tox-silver-sink.tox-tinymce-aux {
|
||||||
/** 该样式默认为1300的zIndex */
|
/** 该样式默认为1300的zIndex */
|
||||||
z-index: 2025;
|
z-index: $dropdown-index;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tox-fullscreen .tox.tox-tinymce-aux {
|
||||||
|
z-index: $dropdown-index !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-tinymce {
|
.app-tinymce {
|
||||||
@@ -218,5 +236,29 @@ const events = computed(() => {
|
|||||||
.tox-promotion {
|
.tox-promotion {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保持focus时与primary色一致 */
|
||||||
|
.tox .tox-edit-area::before {
|
||||||
|
border-color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// antd原生表单 校验失败样式
|
||||||
|
.ant-form-item:has(.ant-form-item-explain-error) {
|
||||||
|
$error-color: #ff3860;
|
||||||
|
|
||||||
|
@include tinymce-valid-fail($error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// useVbenForm 校验失败样式
|
||||||
|
.form-valid-error {
|
||||||
|
$error-color: hsl(var(--destructive));
|
||||||
|
|
||||||
|
@include tinymce-valid-fail($error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全屏下样式处理 不去掉transform位置会异常
|
||||||
|
div[role='dialog']:has(.tox.tox-tinymce.tox-fullscreen) {
|
||||||
|
transform: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ watch(
|
|||||||
:avatar
|
:avatar
|
||||||
:menus
|
:menus
|
||||||
:text="userStore.userInfo?.realName"
|
:text="userStore.userInfo?.realName"
|
||||||
description="ann.vben@gmail.com"
|
:description="userStore.userInfo?.email || '未设置邮箱'"
|
||||||
tag-text="Pro"
|
:tag-text="userStore.userInfo?.username"
|
||||||
@logout="handleLogout"
|
@logout="handleLogout"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export const overridesPreferences = defineOverridesPreferences({
|
|||||||
* 这里可以设置默认头像 url链接或vite导入的图片链接
|
* 这里可以设置默认头像 url链接或vite导入的图片链接
|
||||||
*/
|
*/
|
||||||
// defaultAvatar: '',
|
// defaultAvatar: '',
|
||||||
|
/**
|
||||||
|
* 在这里设置应用标题
|
||||||
|
*/
|
||||||
name: import.meta.env.VITE_APP_TITLE,
|
name: import.meta.env.VITE_APP_TITLE,
|
||||||
/**
|
/**
|
||||||
* 不支持modal模式 需要改动的地方太多
|
* 不支持modal模式 需要改动的地方太多
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const NotFoundComponent = () => import('#/views/_core/fallback/not-found.vue');
|
|||||||
* 在这里定义映射
|
* 在这里定义映射
|
||||||
*/
|
*/
|
||||||
const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
|
const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
|
||||||
'/system/role-auth/user/:roleId(\\d+)': {
|
'/system/role-auth/user/:roleId': {
|
||||||
activePath: '/system/role',
|
activePath: '/system/role',
|
||||||
requireHomeRedirect: true,
|
requireHomeRedirect: true,
|
||||||
},
|
},
|
||||||
@@ -37,7 +37,7 @@ const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
|
|||||||
requireHomeRedirect: true,
|
requireHomeRedirect: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'/tool/gen-edit/index/:tableId(\\d+)': {
|
'/tool/gen-edit/index/:tableId': {
|
||||||
activePath: '/tool/gen',
|
activePath: '/tool/gen',
|
||||||
requireHomeRedirect: true,
|
requireHomeRedirect: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
roles,
|
roles,
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
username: user.userName,
|
username: user.userName,
|
||||||
|
email: user.email ?? '',
|
||||||
};
|
};
|
||||||
userStore.setUserInfo(userInfo);
|
userStore.setUserInfo(userInfo);
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { UnauthorizedException } from '#/api/request';
|
||||||
import { dictDataInfo } from '#/api/system/dict/dict-data';
|
import { dictDataInfo } from '#/api/system/dict/dict-data';
|
||||||
import { useDictStore } from '#/store/dict';
|
import { useDictStore } from '#/store/dict';
|
||||||
|
|
||||||
@@ -27,9 +28,16 @@ function fetchAndCacheDictData<T>(
|
|||||||
// 内部处理了push的逻辑 这里不用push
|
// 内部处理了push的逻辑 这里不用push
|
||||||
setDictInfo(dictName, resp, formatNumber);
|
setDictInfo(dictName, resp, formatNumber);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
|
/**
|
||||||
|
* 需要判断是否为401抛出的特定异常 401清除缓存
|
||||||
|
* 其他error清除缓存会导致无限循环调用字典接口 则不做处理
|
||||||
|
*/
|
||||||
|
if (error instanceof UnauthorizedException) {
|
||||||
// 401时 移除字典缓存 下次登录重新获取
|
// 401时 移除字典缓存 下次登录重新获取
|
||||||
dictRequestCache.delete(dictName);
|
dictRequestCache.delete(dictName);
|
||||||
|
}
|
||||||
|
// 其他不做处理
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
// 移除请求状态缓存
|
// 移除请求状态缓存
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Component, CSSProperties } from 'vue';
|
import type { Component, CSSProperties } from 'vue';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { markRaw, ref } from 'vue';
|
||||||
|
|
||||||
import { DEFAULT_TENANT_ID } from '@vben/constants';
|
import { DEFAULT_TENANT_ID } from '@vben/constants';
|
||||||
import {
|
import {
|
||||||
@@ -69,32 +69,32 @@ export async function handleAuthBinding(source: string) {
|
|||||||
*/
|
*/
|
||||||
export const accountBindList: BindItem[] = [
|
export const accountBindList: BindItem[] = [
|
||||||
{
|
{
|
||||||
avatar: GiteeIcon,
|
avatar: markRaw(GiteeIcon),
|
||||||
description: '绑定Gitee账号',
|
description: '绑定Gitee账号',
|
||||||
source: 'gitee',
|
source: 'gitee',
|
||||||
title: 'Gitee',
|
title: 'Gitee',
|
||||||
style: { color: '#c71d23' },
|
style: { color: '#c71d23' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
avatar: GithubOAuthIcon,
|
avatar: markRaw(GithubOAuthIcon),
|
||||||
description: '绑定Github账号',
|
description: '绑定Github账号',
|
||||||
source: 'github',
|
source: 'github',
|
||||||
title: 'Github',
|
title: 'Github',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
avatar: SvgMaxKeyIcon,
|
avatar: markRaw(SvgMaxKeyIcon),
|
||||||
description: '绑定MaxKey账号',
|
description: '绑定MaxKey账号',
|
||||||
source: 'maxkey',
|
source: 'maxkey',
|
||||||
title: 'MaxKey',
|
title: 'MaxKey',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
avatar: SvgTopiamIcon,
|
avatar: markRaw(SvgTopiamIcon),
|
||||||
description: '绑定topiam账号',
|
description: '绑定topiam账号',
|
||||||
source: 'topiam',
|
source: 'topiam',
|
||||||
title: 'Topiam',
|
title: 'Topiam',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
avatar: SvgWechatIcon,
|
avatar: markRaw(SvgWechatIcon),
|
||||||
description: '绑定wechat账号',
|
description: '绑定wechat账号',
|
||||||
source: 'wechat',
|
source: 'wechat',
|
||||||
title: 'Wechat',
|
title: 'Wechat',
|
||||||
|
|||||||
@@ -1,130 +1,80 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import type { VxeGridProps } from '@vben/plugins/vxe-table';
|
|
||||||
|
|
||||||
import type { BindItem } from '../../oauth-common';
|
import type { BindItem } from '../../oauth-common';
|
||||||
|
|
||||||
import { computed, ref, unref } from 'vue';
|
import type { SocialInfo } from '#/api/system/social/model';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { Alert, Avatar, Card, List, ListItem, Modal } from 'ant-design-vue';
|
import { Alert, Avatar, Card, Empty, Modal, Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
import { authUnbinding } from '#/api';
|
import { authUnbinding } from '#/api';
|
||||||
import { socialList } from '#/api/system/social';
|
import { socialList } from '#/api/system/social';
|
||||||
|
|
||||||
import { accountBindList, handleAuthBinding } from '../../oauth-common';
|
import { accountBindList, handleAuthBinding } from '../../oauth-common';
|
||||||
|
|
||||||
function buttonText(item: BindItem) {
|
interface BindItemWithInfo extends BindItem {
|
||||||
return item.bound ? '已绑定' : '绑定';
|
info?: SocialInfo;
|
||||||
|
bind?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const bindList = ref<BindItemWithInfo[]>([]);
|
||||||
* 已经绑定的平台
|
|
||||||
*/
|
|
||||||
const boundPlatformsList = ref<string[]>([]);
|
|
||||||
const bindList = computed<BindItem[]>(() => {
|
|
||||||
const list = [...accountBindList];
|
|
||||||
list.forEach((item) => {
|
|
||||||
item.bound = !!unref(boundPlatformsList).includes(item.source);
|
|
||||||
});
|
|
||||||
return list;
|
|
||||||
});
|
|
||||||
|
|
||||||
const gridOptions: VxeGridProps = {
|
async function loadData() {
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'source',
|
|
||||||
title: '绑定平台',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slots: {
|
|
||||||
default: ({ row }) => {
|
|
||||||
return <Avatar src={row.avatar} />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
field: 'avatar',
|
|
||||||
title: '头像',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
align: 'center',
|
|
||||||
field: 'userName',
|
|
||||||
title: '账号',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
align: 'center',
|
|
||||||
slots: {
|
|
||||||
default: 'action',
|
|
||||||
},
|
|
||||||
title: '操作',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
height: 220,
|
|
||||||
keepSource: true,
|
|
||||||
pagerConfig: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
toolbarConfig: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async () => {
|
|
||||||
const resp = await socialList();
|
const resp = await socialList();
|
||||||
|
|
||||||
|
const list: BindItemWithInfo[] = [...accountBindList];
|
||||||
|
list.forEach((item) => {
|
||||||
/**
|
/**
|
||||||
* 平台转小写
|
* 平台转小写
|
||||||
* 已经绑定的平台
|
|
||||||
*/
|
*/
|
||||||
boundPlatformsList.value = resp.map((item) =>
|
item.bound = resp
|
||||||
item.source.toLowerCase(),
|
.map((social) => social.source.toLowerCase())
|
||||||
|
.includes(item.source.toLowerCase());
|
||||||
|
/**
|
||||||
|
* 添加info信息
|
||||||
|
*/
|
||||||
|
if (item.bound) {
|
||||||
|
item.info = resp.find(
|
||||||
|
(social) => social.source.toLowerCase() === item.source,
|
||||||
);
|
);
|
||||||
return {
|
}
|
||||||
rows: resp,
|
});
|
||||||
};
|
bindList.value = list;
|
||||||
},
|
}
|
||||||
},
|
onMounted(loadData);
|
||||||
},
|
|
||||||
rowConfig: {
|
|
||||||
isCurrent: false,
|
|
||||||
keyField: 'id',
|
|
||||||
},
|
|
||||||
id: 'profile-bind-table',
|
|
||||||
};
|
|
||||||
|
|
||||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
|
||||||
gridOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解绑账号
|
* 解绑账号
|
||||||
*/
|
*/
|
||||||
function handleUnbind(record: Record<string, any>) {
|
function handleUnbind(record: BindItemWithInfo) {
|
||||||
|
if (!record.info) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
content: `确定解绑[${record.source}]平台的[${record.userName}]账号吗?`,
|
content: `确定解绑[${record.source}]平台的[${record.info.userName}]账号吗?`,
|
||||||
async onOk() {
|
async onOk() {
|
||||||
await authUnbinding(record.id);
|
await authUnbinding(record.info!.id);
|
||||||
await tableApi.reload();
|
await loadData();
|
||||||
},
|
},
|
||||||
title: '提示',
|
title: '提示',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-[16px]">
|
<div class="flex flex-col gap-4 pb-4">
|
||||||
<BasicTable>
|
<div
|
||||||
<template #action="{ row }">
|
v-if="bindList.length > 0"
|
||||||
<a-button type="link" @click="handleUnbind(row)">解绑</a-button>
|
class="grid grid-cols-1 gap-4 lg:grid-cols-2 2xl:grid-cols-3"
|
||||||
</template>
|
>
|
||||||
</BasicTable>
|
<Card
|
||||||
<div class="pb-3">
|
class="transition-shadow duration-300 hover:shadow-md"
|
||||||
<List
|
v-for="item in bindList"
|
||||||
:data-source="bindList"
|
:key="item.source"
|
||||||
:grid="{ gutter: 8, xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 3 }"
|
|
||||||
>
|
>
|
||||||
<template #renderItem="{ item }">
|
|
||||||
<ListItem>
|
|
||||||
<Card>
|
|
||||||
<div class="flex w-full items-center gap-4">
|
<div class="flex w-full items-center gap-4">
|
||||||
<component
|
<component
|
||||||
:is="item.avatar"
|
:is="item.avatar"
|
||||||
@@ -134,29 +84,48 @@ function handleUnbind(record: Record<string, any>) {
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-1 items-center justify-between">
|
<div class="flex flex-1 items-center justify-between">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h4
|
<h4 class="mb-[4px] text-[14px] text-black/85 dark:text-white/85">
|
||||||
class="mb-[4px] text-[14px] text-black/85 dark:text-white/85"
|
|
||||||
>
|
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</h4>
|
</h4>
|
||||||
<span class="text-black/45 dark:text-white/45">
|
<span class="text-black/45 dark:text-white/45">
|
||||||
|
<template v-if="!item.bound">
|
||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
|
</template>
|
||||||
|
<template v-if="item.bound && item.info">
|
||||||
|
<Tooltip>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex flex-col items-center gap-2 p-2">
|
||||||
|
<Avatar :size="36" :src="item.info.avatar" />
|
||||||
|
<div>绑定时间: {{ item.info.createTime }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="cursor-pointer">
|
||||||
|
已绑定: {{ item.info.nickName }}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- TODO: 这里有优化空间? -->
|
||||||
<a-button
|
<a-button
|
||||||
:disabled="item.bound"
|
|
||||||
size="small"
|
size="small"
|
||||||
type="link"
|
:type="item.bound ? 'default' : 'link'"
|
||||||
@click="handleAuthBinding(item.source)"
|
@click="
|
||||||
|
item.bound ? handleUnbind(item) : handleAuthBinding(item.source)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ buttonText(item) }}
|
{{ item.bound ? '取消绑定' : '绑定' }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</ListItem>
|
</div>
|
||||||
</template>
|
<div
|
||||||
</List>
|
v-if="bindList.length === 0"
|
||||||
|
class="flex items-center justify-center rounded-lg border py-4"
|
||||||
|
>
|
||||||
|
<Empty :image="simpleImage" description="暂无可绑定的第三方账户" />
|
||||||
|
</div>
|
||||||
<Alert message="说明" type="info">
|
<Alert message="说明" type="info">
|
||||||
<template #description>
|
<template #description>
|
||||||
<p>
|
<p>
|
||||||
@@ -169,7 +138,6 @@ function handleUnbind(record: Record<string, any>) {
|
|||||||
</template>
|
</template>
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||||||
fieldName: 'orderNum',
|
fieldName: 'orderNum',
|
||||||
label: '显示排序',
|
label: '显示排序',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||||||
fieldName: 'dictSort',
|
fieldName: 'dictSort',
|
||||||
label: '显示排序',
|
label: '显示排序',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Textarea',
|
component: 'Textarea',
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||||||
if (model.isFrame !== '0') {
|
if (model.isFrame !== '0') {
|
||||||
return z
|
return z
|
||||||
.string({ message: '请输入路由地址' })
|
.string({ message: '请输入路由地址' })
|
||||||
|
.min(1, '请输入路由地址')
|
||||||
.refine((val) => !val.startsWith('/'), {
|
.refine((val) => !val.startsWith('/'), {
|
||||||
message: '路由地址不需要带/',
|
message: '路由地址不需要带/',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { MaybePromise } from '@vben/types';
|
|
||||||
|
|
||||||
import type { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存表格滚动/展开状态并执行回调 用于树表在执行 新增/编辑/删除等操作后 依然在当前位置(体验优化)
|
|
||||||
*
|
|
||||||
* @param tableApi 表格api
|
|
||||||
* @param callback 回调
|
|
||||||
*/
|
|
||||||
export async function preserveTreeTableState(
|
|
||||||
tableApi: ReturnType<typeof useVbenVxeGrid>[1],
|
|
||||||
callback: () => MaybePromise<void>,
|
|
||||||
) {
|
|
||||||
// 保存当前状态
|
|
||||||
const scrollState = tableApi.grid.getScroll();
|
|
||||||
const expandRecords = tableApi.grid.getTreeExpandRecords();
|
|
||||||
|
|
||||||
// 执行回调
|
|
||||||
await callback();
|
|
||||||
|
|
||||||
// 恢复状态
|
|
||||||
tableApi.grid.setTreeExpand(expandRecords, true);
|
|
||||||
tableApi.grid.scrollTo(scrollState.scrollLeft, scrollState.scrollTop);
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|||||||
import { menuCascadeRemove, menuList, menuRemove } from '#/api/system/menu';
|
import { menuCascadeRemove, menuList, menuRemove } from '#/api/system/menu';
|
||||||
|
|
||||||
import { columns, querySchema } from './data';
|
import { columns, querySchema } from './data';
|
||||||
import { preserveTreeTableState } from './helper';
|
|
||||||
import menuDrawer from './menu-drawer.vue';
|
import menuDrawer from './menu-drawer.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +68,8 @@ const gridOptions: VxeGridProps = {
|
|||||||
rowField: 'menuId',
|
rowField: 'menuId',
|
||||||
// 自动转换为tree 由vxe处理 无需手动转换
|
// 自动转换为tree 由vxe处理 无需手动转换
|
||||||
transform: true,
|
transform: true,
|
||||||
|
// 刷新接口后 记录展开行的情况
|
||||||
|
reserve: true,
|
||||||
},
|
},
|
||||||
id: 'system-menu-index',
|
id: 'system-menu-index',
|
||||||
};
|
};
|
||||||
@@ -118,7 +119,6 @@ async function handleEdit(record: Menu) {
|
|||||||
*/
|
*/
|
||||||
const cascadingDeletion = ref(false);
|
const cascadingDeletion = ref(false);
|
||||||
async function handleDelete(row: Menu) {
|
async function handleDelete(row: Menu) {
|
||||||
await preserveTreeTableState(tableApi, async () => {
|
|
||||||
if (cascadingDeletion.value) {
|
if (cascadingDeletion.value) {
|
||||||
// 级联删除
|
// 级联删除
|
||||||
const menuAndChildren: Menu[] = treeToList([row], { id: 'menuId' });
|
const menuAndChildren: Menu[] = treeToList([row], { id: 'menuId' });
|
||||||
@@ -128,7 +128,6 @@ async function handleDelete(row: Menu) {
|
|||||||
await menuRemove([row.menuId]);
|
await menuRemove([row.menuId]);
|
||||||
}
|
}
|
||||||
await tableApi.query();
|
await tableApi.query();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeConfirmTitle(row: Menu) {
|
function removeConfirmTitle(row: Menu) {
|
||||||
@@ -147,7 +146,7 @@ function removeConfirmTitle(row: Menu) {
|
|||||||
* 编辑/添加成功后刷新表格
|
* 编辑/添加成功后刷新表格
|
||||||
*/
|
*/
|
||||||
async function afterEditOrAdd() {
|
async function afterEditOrAdd() {
|
||||||
await preserveTreeTableState(tableApi, () => tableApi.query());
|
tableApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||||||
fieldName: 'postSort',
|
fieldName: 'postSort',
|
||||||
label: '岗位排序',
|
label: '岗位排序',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'RadioGroup',
|
component: 'RadioGroup',
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||||||
fieldName: 'roleSort',
|
fieldName: 'roleSort',
|
||||||
label: '角色排序',
|
label: '角色排序',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
|
|||||||
@@ -11,14 +11,7 @@ import { useAccess } from '@vben/access';
|
|||||||
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||||
import { getVxePopupContainer } from '@vben/utils';
|
import { getVxePopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import {
|
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||||
Dropdown,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Modal,
|
|
||||||
Popconfirm,
|
|
||||||
Space,
|
|
||||||
} from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
@@ -200,6 +193,18 @@ function handleAssignRole(record: Role) {
|
|||||||
>
|
>
|
||||||
{{ $t('pages.common.edit') }}
|
{{ $t('pages.common.edit') }}
|
||||||
</ghost-button>
|
</ghost-button>
|
||||||
|
<ghost-button
|
||||||
|
v-access:code="['system:role:edit']"
|
||||||
|
@click.stop="handleAuthEdit(row)"
|
||||||
|
>
|
||||||
|
权限
|
||||||
|
</ghost-button>
|
||||||
|
<ghost-button
|
||||||
|
v-access:code="['system:role:edit']"
|
||||||
|
@click.stop="handleAssignRole(row)"
|
||||||
|
>
|
||||||
|
分配
|
||||||
|
</ghost-button>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
:get-popup-container="getVxePopupContainer"
|
:get-popup-container="getVxePopupContainer"
|
||||||
placement="left"
|
placement="left"
|
||||||
@@ -215,25 +220,6 @@ function handleAssignRole(record: Role) {
|
|||||||
</ghost-button>
|
</ghost-button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
<Dropdown placement="bottomRight">
|
|
||||||
<template #overlay>
|
|
||||||
<Menu>
|
|
||||||
<MenuItem key="1" @click="handleAuthEdit(row)">
|
|
||||||
数据权限
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem key="2" @click="handleAssignRole(row)">
|
|
||||||
分配用户
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</template>
|
|
||||||
<a-button
|
|
||||||
size="small"
|
|
||||||
type="link"
|
|
||||||
v-access:code="'system:role:edit'"
|
|
||||||
>
|
|
||||||
{{ $t('pages.common.more') }}
|
|
||||||
</a-button>
|
|
||||||
</Dropdown>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { cloneDeep, eachTree } from '@vben/utils';
|
|||||||
import { omit } from 'lodash-es';
|
import { omit } from 'lodash-es';
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
import { menuTreeSelect, tenantPackageMenuTreeSelect } from '#/api/system/menu';
|
import { tenantPackageMenuTreeSelect } from '#/api/system/menu';
|
||||||
import {
|
import {
|
||||||
packageAdd,
|
packageAdd,
|
||||||
packageInfo,
|
packageInfo,
|
||||||
@@ -40,30 +40,18 @@ const [BasicForm, formApi] = useVbenForm({
|
|||||||
|
|
||||||
const menuTree = ref<MenuOption[]>([]);
|
const menuTree = ref<MenuOption[]>([]);
|
||||||
async function setupMenuTree(id?: number | string) {
|
async function setupMenuTree(id?: number | string) {
|
||||||
if (id) {
|
// 0为新增使用 获取除了`租户管理`的所有菜单
|
||||||
const resp = await tenantPackageMenuTreeSelect(id);
|
const resp = await tenantPackageMenuTreeSelect(id ?? 0);
|
||||||
const menus = resp.menus;
|
const menus = resp.menus;
|
||||||
// i18n处理
|
// i18n处理
|
||||||
eachTree(menus, (node) => {
|
eachTree(menus, (node) => {
|
||||||
node.label = $t(node.label);
|
node.label = $t(node.label);
|
||||||
});
|
});
|
||||||
// 设置菜单信息
|
// 设置菜单信息
|
||||||
menuTree.value = resp.menus;
|
menuTree.value = menus;
|
||||||
// keys依赖于menu 需要先加载menu
|
// keys依赖于menu 需要先加载menu
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await formApi.setFieldValue('menuIds', resp.checkedKeys);
|
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', []);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function customFormValueGetter() {
|
async function customFormValueGetter() {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ onMounted(loadTree);
|
|||||||
v-model:value="searchValue"
|
v-model:value="searchValue"
|
||||||
:placeholder="$t('pages.common.search')"
|
:placeholder="$t('pages.common.search')"
|
||||||
size="small"
|
size="small"
|
||||||
|
allow-clear
|
||||||
>
|
>
|
||||||
<template #enterButton>
|
<template #enterButton>
|
||||||
<a-button @click="handleReload">
|
<a-button @click="handleReload">
|
||||||
@@ -102,9 +103,9 @@ onMounted(loadTree);
|
|||||||
@select="$emit('select')"
|
@select="$emit('select')"
|
||||||
>
|
>
|
||||||
<template #title="{ label }">
|
<template #title="{ label }">
|
||||||
<span v-if="label.indexOf(searchValue) > -1">
|
<span v-if="label.includes(searchValue)">
|
||||||
{{ label.substring(0, label.indexOf(searchValue)) }}
|
{{ label.substring(0, label.indexOf(searchValue)) }}
|
||||||
<span style="color: #f50">{{ searchValue }}</span>
|
<span class="text-primary">{{ searchValue }}</span>
|
||||||
{{
|
{{
|
||||||
label.substring(
|
label.substring(
|
||||||
label.indexOf(searchValue) + searchValue.length,
|
label.indexOf(searchValue) + searchValue.length,
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import type { Ref } from 'vue';
|
|||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
import type { GenInfo } from '#/api/tool/gen/model';
|
import type { GenInfo } from '#/api/tool/gen/model';
|
||||||
|
|
||||||
import { inject } from 'vue';
|
import { inject, onMounted, reactive } from 'vue';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { dictOptionSelectList } from '#/api/system/dict/dict-type';
|
||||||
|
|
||||||
import { validRules, vxeTableColumns } from './gen-data';
|
import { validRules, vxeTableColumns } from './gen-data';
|
||||||
|
|
||||||
@@ -15,8 +16,26 @@ import { validRules, vxeTableColumns } from './gen-data';
|
|||||||
*/
|
*/
|
||||||
const genInfoData = inject('genInfoData') as Ref<GenInfo['info']>;
|
const genInfoData = inject('genInfoData') as Ref<GenInfo['info']>;
|
||||||
|
|
||||||
|
const dictOptions = reactive<{ label: string; value: string }[]>([
|
||||||
|
{ label: '未设置', value: '' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载字典下拉数据
|
||||||
|
*/
|
||||||
|
onMounted(async () => {
|
||||||
|
const resp = await dictOptionSelectList();
|
||||||
|
|
||||||
|
const options = resp.map((dict) => ({
|
||||||
|
label: `${dict.dictName} | ${dict.dictType}`,
|
||||||
|
value: dict.dictType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
dictOptions.push(...options);
|
||||||
|
});
|
||||||
|
|
||||||
const gridOptions: VxeGridProps = {
|
const gridOptions: VxeGridProps = {
|
||||||
columns: vxeTableColumns,
|
columns: vxeTableColumns(dictOptions),
|
||||||
keepSource: true,
|
keepSource: true,
|
||||||
editConfig: { trigger: 'click', mode: 'cell', showStatus: true },
|
editConfig: { trigger: 'click', mode: 'cell', showStatus: true },
|
||||||
editRules: validRules,
|
editRules: validRules,
|
||||||
|
|||||||
@@ -2,14 +2,10 @@ import type { Recordable } from '@vben/types';
|
|||||||
|
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { reactive } from 'vue';
|
|
||||||
|
|
||||||
import { getPopupContainer } from '@vben/utils';
|
import { getPopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import { Checkbox, Input, Select } from 'ant-design-vue';
|
import { Checkbox, Input, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
import { dictOptionSelectList } from '#/api/system/dict/dict-type';
|
|
||||||
|
|
||||||
const JavaTypes: string[] = [
|
const JavaTypes: string[] = [
|
||||||
'Long',
|
'Long',
|
||||||
'String',
|
'String',
|
||||||
@@ -45,24 +41,6 @@ const componentsOptions = [
|
|||||||
{ label: '富文本', value: 'editor' },
|
{ label: '富文本', value: 'editor' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const dictOptions = reactive<{ label: string; value: string }[]>([
|
|
||||||
{ label: '未设置', value: '' },
|
|
||||||
]);
|
|
||||||
/**
|
|
||||||
* 在这里初始化字典下拉框
|
|
||||||
*/
|
|
||||||
(async function init() {
|
|
||||||
const ret = await dictOptionSelectList();
|
|
||||||
|
|
||||||
ret.forEach((dict) => {
|
|
||||||
const option = {
|
|
||||||
label: `${dict.dictName} | ${dict.dictType}`,
|
|
||||||
value: dict.dictType,
|
|
||||||
};
|
|
||||||
dictOptions.push(option);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
function renderBooleanTag(row: Recordable<any>, field: string) {
|
function renderBooleanTag(row: Recordable<any>, field: string) {
|
||||||
const value = row[field] ? '是' : '否';
|
const value = row[field] ? '是' : '否';
|
||||||
const className = row[field] ? 'text-green-500' : 'text-red-500';
|
const className = row[field] ? 'text-green-500' : 'text-red-500';
|
||||||
@@ -78,7 +56,10 @@ export const validRules: VxeGridProps['editRules'] = {
|
|||||||
javaField: [{ required: true, message: '请输入' }],
|
javaField: [{ required: true, message: '请输入' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const vxeTableColumns: VxeGridProps['columns'] = [
|
// 内部依赖的字典从外部通过函数传入
|
||||||
|
export const vxeTableColumns: (
|
||||||
|
dictOptions: { label: string; value: string }[],
|
||||||
|
) => VxeGridProps['columns'] = (dictOptions) => [
|
||||||
{
|
{
|
||||||
title: '序号',
|
title: '序号',
|
||||||
type: 'seq',
|
type: 'seq',
|
||||||
|
|||||||
@@ -86,11 +86,13 @@ const gridOptions: VxeGridProps = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
rowConfig: {
|
rowConfig: {
|
||||||
keyField: 'tableId',
|
keyField: 'tableName',
|
||||||
},
|
},
|
||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
id: 'import-table-modal',
|
||||||
|
cellClassName: 'cursor-pointer',
|
||||||
};
|
};
|
||||||
|
|
||||||
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });
|
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ import {
|
|||||||
import { renderDict } from '#/utils/render';
|
import { renderDict } from '#/utils/render';
|
||||||
|
|
||||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
|
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
|
||||||
import FlowPreview from '../components/flow-preview/index.vue';
|
|
||||||
import ApprovalDetails from './approval-details.vue';
|
import ApprovalDetails from './approval-details.vue';
|
||||||
|
import FlowPreview from './flow-preview.vue';
|
||||||
import { approveWithReasonModal } from './helper';
|
import { approveWithReasonModal } from './helper';
|
||||||
import userSelectModal from './user-select-modal.vue';
|
import userSelectModal from './user-select-modal.vue';
|
||||||
|
|
||||||
@@ -443,10 +443,7 @@ async function handleCopy(text: string) {
|
|||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="2" tab="审批流程图">
|
<TabPane key="2" tab="审批流程图">
|
||||||
<FlowPreview
|
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
||||||
v-if="currentFlowInfo.defChart"
|
|
||||||
:data="currentFlowInfo.defChart.defJson"
|
|
||||||
/>
|
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { stringify } from '@vben/request';
|
|||||||
import { useAccessStore } from '@vben/stores';
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
import { useEventListener } from '@vueuse/core';
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
import { Alert } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'FlowDesigner' });
|
defineOptions({ name: 'FlowDesigner' });
|
||||||
|
|
||||||
@@ -48,5 +49,13 @@ useEventListener('message', messageHandler);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="size-full">
|
||||||
|
<Alert
|
||||||
|
class="mx-4 my-2"
|
||||||
|
type="warning"
|
||||||
|
:show-icon="true"
|
||||||
|
message="这是iframe页面! iframe页面! iframe页面! 不是我写的真服了"
|
||||||
|
/>
|
||||||
<iframe :src="url" class="size-full"></iframe>
|
<iframe :src="url" class="size-full"></iframe>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
32
apps/web-antd/src/views/workflow/components/flow-preview.vue
Normal file
32
apps/web-antd/src/views/workflow/components/flow-preview.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppConfig } from '@vben/hooks';
|
||||||
|
import { stringify } from '@vben/request';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { useWarmflowIframe } from './hook';
|
||||||
|
|
||||||
|
defineOptions({ name: 'FlowPreview' });
|
||||||
|
|
||||||
|
const props = defineProps<{ instanceId: string }>();
|
||||||
|
|
||||||
|
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const params = {
|
||||||
|
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||||
|
id: props.instanceId,
|
||||||
|
clientid: clientId,
|
||||||
|
type: 'FlowChart',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iframe地址
|
||||||
|
*/
|
||||||
|
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
||||||
|
|
||||||
|
const { iframeRef } = useWarmflowIframe();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<iframe ref="iframeRef" :src="url" class="h-[500px] w-full border"></iframe>
|
||||||
|
</template>
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { ZoomParamType } from '@logicflow/core';
|
|
||||||
|
|
||||||
import { onMounted, shallowRef, useTemplateRef, watch } from 'vue';
|
|
||||||
|
|
||||||
import { usePreferences } from '@vben/preferences';
|
|
||||||
|
|
||||||
import {
|
|
||||||
MinusCircleOutlined,
|
|
||||||
PlusCircleOutlined,
|
|
||||||
ShrinkOutlined,
|
|
||||||
} from '@ant-design/icons-vue';
|
|
||||||
import LogicFlow from '@logicflow/core';
|
|
||||||
|
|
||||||
import Between from './model/between';
|
|
||||||
import End from './model/end';
|
|
||||||
import Parallel from './model/parallel';
|
|
||||||
import Serial from './model/serial';
|
|
||||||
import Skip from './model/skip';
|
|
||||||
import Start from './model/start';
|
|
||||||
import { json2LogicFlowJson } from './model/tool';
|
|
||||||
|
|
||||||
import '@logicflow/core/lib/style/index.css';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ data?: object }>(), {
|
|
||||||
data: () => ({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = useTemplateRef('container');
|
|
||||||
const lf = shallowRef<LogicFlow | null>(null);
|
|
||||||
|
|
||||||
function zoomViewport(zoom: ZoomParamType) {
|
|
||||||
if (!lf.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lf.value.zoom(zoom);
|
|
||||||
// 将内容平移至画布中心
|
|
||||||
lf.value.translateCenter();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (props.data && container.value) {
|
|
||||||
const data = json2LogicFlowJson(props.data);
|
|
||||||
lf.value = new LogicFlow({
|
|
||||||
container: container.value,
|
|
||||||
isSilentMode: true,
|
|
||||||
textEdit: false,
|
|
||||||
grid: {
|
|
||||||
size: 20,
|
|
||||||
type: 'dot',
|
|
||||||
config: {
|
|
||||||
color: '#ccc',
|
|
||||||
thickness: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
lf.value.register(Start);
|
|
||||||
lf.value.register(Between);
|
|
||||||
lf.value.register(Serial);
|
|
||||||
lf.value.register(Parallel);
|
|
||||||
lf.value.register(End);
|
|
||||||
lf.value.register(Skip);
|
|
||||||
|
|
||||||
lf.value.render(data);
|
|
||||||
lf.value.translateCenter();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { isDark } = usePreferences();
|
|
||||||
watch(isDark, (v) => {
|
|
||||||
if (!lf.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lf.value.graphModel.background = {
|
|
||||||
background: v ? '#333' : '#fff',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center justify-between py-2">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<a-button @click="zoomViewport(1)">
|
|
||||||
<template #icon>
|
|
||||||
<ShrinkOutlined />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
<a-button @click="zoomViewport(true)">
|
|
||||||
<template #icon>
|
|
||||||
<PlusCircleOutlined />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
<a-button @click="zoomViewport(false)">
|
|
||||||
<template #icon>
|
|
||||||
<MinusCircleOutlined />
|
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3 font-semibold">
|
|
||||||
<span class="rounded-md border border-[#000] px-2">未办理</span>
|
|
||||||
<span
|
|
||||||
class="rounded-md border border-dashed border-[#ffcd17] bg-[#fff8dc] px-2 dark:text-black"
|
|
||||||
>
|
|
||||||
待办理
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="rounded-md border border-[#9dff00] bg-[#f0ffd9] px-2 dark:text-black"
|
|
||||||
>
|
|
||||||
已完成
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 容器区域 -->
|
|
||||||
<div class="h-[500px] w-full border" ref="container"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import LogicFlow, { RectNode, RectNodeModel } from '@logicflow/core';
|
|
||||||
|
|
||||||
class BetweenModel extends RectNodeModel {
|
|
||||||
override getNodeStyle() {
|
|
||||||
return super.getNodeStyle();
|
|
||||||
}
|
|
||||||
override initNodeData(data: LogicFlow.NodeConfig) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
this.width = 100;
|
|
||||||
this.height = 80;
|
|
||||||
this.radius = 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BetweenView extends RectNode {}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
type: 'between',
|
|
||||||
model: BetweenModel,
|
|
||||||
view: BetweenView,
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import LogicFlow, { CircleNode, CircleNodeModel } from '@logicflow/core';
|
|
||||||
|
|
||||||
class endModel extends CircleNodeModel {
|
|
||||||
override initNodeData(data: LogicFlow.NodeConfig) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
this.r = 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class endView extends CircleNode {}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
type: 'end',
|
|
||||||
model: endModel,
|
|
||||||
view: endView,
|
|
||||||
};
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import type { GraphModel } from '@logicflow/core';
|
|
||||||
|
|
||||||
import LogicFlow, { h, PolygonNode, PolygonNodeModel } from '@logicflow/core';
|
|
||||||
|
|
||||||
class ParallelModel extends PolygonNodeModel {
|
|
||||||
static extendKey = 'ParallelModel';
|
|
||||||
|
|
||||||
constructor(data: LogicFlow.NodeConfig, graphModel: GraphModel) {
|
|
||||||
if (!data.text) {
|
|
||||||
data.text = '';
|
|
||||||
}
|
|
||||||
if (data.text && typeof data.text === 'string') {
|
|
||||||
data.text = {
|
|
||||||
value: data.text,
|
|
||||||
x: data.x,
|
|
||||||
y: data.y + 40,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
super(data, graphModel);
|
|
||||||
this.points = [
|
|
||||||
[25, 0],
|
|
||||||
[50, 25],
|
|
||||||
[25, 50],
|
|
||||||
[0, 25],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ParallelView extends PolygonNode {
|
|
||||||
static extendKey = 'ParallelNode';
|
|
||||||
|
|
||||||
override getShape() {
|
|
||||||
const { model } = this.props;
|
|
||||||
const { x, y, width, height, points } = model;
|
|
||||||
const style = model.getNodeStyle();
|
|
||||||
return h(
|
|
||||||
'g',
|
|
||||||
{
|
|
||||||
transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`,
|
|
||||||
},
|
|
||||||
h('polygon', {
|
|
||||||
...style,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
points,
|
|
||||||
}),
|
|
||||||
h('path', {
|
|
||||||
d: 'm 23,10 0,12.5 -12.5,0 0,5 12.5,0 0,12.5 5,0 0,-12.5 12.5,0 0,-5 -12.5,0 0,-12.5 -5,0 z',
|
|
||||||
...style,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
type: 'parallel',
|
|
||||||
view: ParallelView,
|
|
||||||
model: ParallelModel,
|
|
||||||
};
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import type { GraphModel } from '@logicflow/core';
|
|
||||||
|
|
||||||
import LogicFlow, { h, PolygonNode, PolygonNodeModel } from '@logicflow/core';
|
|
||||||
|
|
||||||
class SerialModel extends PolygonNodeModel {
|
|
||||||
static extendKey = 'SerialModel';
|
|
||||||
|
|
||||||
constructor(data: LogicFlow.NodeConfig, graphModel: GraphModel) {
|
|
||||||
if (!data.text) {
|
|
||||||
data.text = '';
|
|
||||||
}
|
|
||||||
if (data.text && typeof data.text === 'string') {
|
|
||||||
data.text = {
|
|
||||||
value: data.text,
|
|
||||||
x: data.x,
|
|
||||||
y: data.y + 40,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
super(data, graphModel);
|
|
||||||
this.points = [
|
|
||||||
[25, 0],
|
|
||||||
[50, 25],
|
|
||||||
[25, 50],
|
|
||||||
[0, 25],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SerialView extends PolygonNode {
|
|
||||||
static extendKey = 'SerialNode';
|
|
||||||
|
|
||||||
override getShape() {
|
|
||||||
const { model } = this.props;
|
|
||||||
const { x, y, width, height, points } = model;
|
|
||||||
const style = model.getNodeStyle();
|
|
||||||
return h(
|
|
||||||
'g',
|
|
||||||
{
|
|
||||||
transform: `matrix(1 0 0 1 ${x - width / 2} ${y - height / 2})`,
|
|
||||||
},
|
|
||||||
h('polygon', {
|
|
||||||
...style,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
points,
|
|
||||||
}),
|
|
||||||
h('path', {
|
|
||||||
d: 'm 16,15 7.42857142857143,9.714285714285715 -7.42857142857143,9.714285714285715 3.428571428571429,0 5.714285714285715,-7.464228571428572 5.714285714285715,7.464228571428572 3.428571428571429,0 -7.42857142857143,-9.714285714285715 7.42857142857143,-9.714285714285715 -3.428571428571429,0 -5.714285714285715,7.464228571428572 -5.714285714285715,-7.464228571428572 -3.428571428571429,0 z',
|
|
||||||
...style,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
type: 'serial',
|
|
||||||
view: SerialView,
|
|
||||||
model: SerialModel,
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { PolylineEdge, PolylineEdgeModel } from '@logicflow/core';
|
|
||||||
|
|
||||||
class SkipModel extends PolylineEdgeModel {
|
|
||||||
/**
|
|
||||||
* 重写此方法,使保存数据是能带上锚点数据。
|
|
||||||
*/
|
|
||||||
override getData() {
|
|
||||||
const data = super.getData();
|
|
||||||
data.sourceAnchorId = this.sourceAnchorId;
|
|
||||||
data.targetAnchorId = this.targetAnchorId;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
override getEdgeStyle() {
|
|
||||||
const style = super.getEdgeStyle();
|
|
||||||
const { properties } = this;
|
|
||||||
if (properties.isActived) {
|
|
||||||
style.strokeDasharray = '4 4';
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
override setAttributes() {
|
|
||||||
this.offset = 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
type: 'skip',
|
|
||||||
view: PolylineEdge,
|
|
||||||
model: SkipModel,
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import LogicFlow, { CircleNode, CircleNodeModel } from '@logicflow/core';
|
|
||||||
|
|
||||||
class StartModel extends CircleNodeModel {
|
|
||||||
override initNodeData(data: LogicFlow.NodeConfig) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
this.r = 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StartView extends CircleNode {}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
type: 'start',
|
|
||||||
model: StartModel,
|
|
||||||
view: StartView,
|
|
||||||
};
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
/* eslint-disable unicorn/no-array-reduce */
|
|
||||||
const NODE_TYPE_MAP = {
|
|
||||||
0: 'start',
|
|
||||||
1: 'between',
|
|
||||||
2: 'end',
|
|
||||||
3: 'serial',
|
|
||||||
4: 'parallel',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将warm-flow的定义json数据转成LogicFlow支持的数据格式
|
|
||||||
* @param {*} json
|
|
||||||
* @returns LogicFlow的数据
|
|
||||||
*/
|
|
||||||
export const json2LogicFlowJson = (definition: any) => {
|
|
||||||
const graphData: any = {
|
|
||||||
nodes: [],
|
|
||||||
edges: [],
|
|
||||||
};
|
|
||||||
// 解析definition属性
|
|
||||||
graphData.flowCode = definition.flowCode;
|
|
||||||
graphData.flowName = definition.flowName;
|
|
||||||
graphData.version = definition.version;
|
|
||||||
graphData.fromCustom = definition.fromCustom;
|
|
||||||
graphData.fromPath = definition.fromPath;
|
|
||||||
// 解析节点
|
|
||||||
const allSkips = definition.nodeList.reduce((acc: any, node: any) => {
|
|
||||||
if (node.skipList && Array.isArray(node.skipList)) {
|
|
||||||
acc.push(...node.skipList);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
const allNodes = definition.nodeList;
|
|
||||||
// 解析节点
|
|
||||||
if (allNodes.length > 0) {
|
|
||||||
for (let i = 0, len = allNodes.length; i < len; i++) {
|
|
||||||
const node = allNodes[i];
|
|
||||||
const lfNode: any = {
|
|
||||||
text: {},
|
|
||||||
properties: {},
|
|
||||||
};
|
|
||||||
// 处理节点
|
|
||||||
lfNode.type = (NODE_TYPE_MAP as any)[node.nodeType];
|
|
||||||
lfNode.id = node.nodeCode;
|
|
||||||
const coordinate = node.coordinate;
|
|
||||||
if (coordinate) {
|
|
||||||
const attr = coordinate.split('|');
|
|
||||||
const nodeXy = attr[0].split(',');
|
|
||||||
lfNode.x = Number.parseInt(nodeXy[0]);
|
|
||||||
lfNode.y = Number.parseInt(nodeXy[1]);
|
|
||||||
if (attr.length === 2) {
|
|
||||||
const textXy = attr[1].split(',');
|
|
||||||
lfNode.text.x = Number.parseInt(textXy[0]);
|
|
||||||
lfNode.text.y = Number.parseInt(textXy[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lfNode.text.value = node.nodeName;
|
|
||||||
lfNode.properties.nodeRatio = node.nodeRatio.toString();
|
|
||||||
lfNode.properties.permissionFlag = node.permissionFlag;
|
|
||||||
lfNode.properties.anyNodeSkip = node.anyNodeSkip;
|
|
||||||
lfNode.properties.listenerType = node.listenerType;
|
|
||||||
lfNode.properties.listenerPath = node.listenerPath;
|
|
||||||
lfNode.properties.formCustom = node.formCustom;
|
|
||||||
lfNode.properties.formPath = node.formPath;
|
|
||||||
lfNode.properties.ext = {};
|
|
||||||
if (node.ext && typeof node.ext === 'string') {
|
|
||||||
try {
|
|
||||||
node.ext = JSON.parse(node.ext);
|
|
||||||
node.ext.forEach((e: any) => {
|
|
||||||
lfNode.properties.ext[e.code] = String(e.value).includes(',')
|
|
||||||
? e.value.split(',')
|
|
||||||
: String(e.value);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing JSON:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lfNode.properties.style = {};
|
|
||||||
if (node.status === 2) {
|
|
||||||
lfNode.properties.style.fill = '#F0FFD9';
|
|
||||||
lfNode.properties.style.stroke = '#9DFF00';
|
|
||||||
}
|
|
||||||
if (node.status === 1) {
|
|
||||||
lfNode.properties.style.fill = '#FFF8DC';
|
|
||||||
lfNode.properties.style.stroke = '#FFCD17';
|
|
||||||
}
|
|
||||||
graphData.nodes.push(lfNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (allSkips.length > 0) {
|
|
||||||
// 处理边
|
|
||||||
let skipEle = null;
|
|
||||||
let edge: any = {};
|
|
||||||
for (let j = 0, lenn = allSkips.length; j < lenn; j++) {
|
|
||||||
skipEle = allSkips[j];
|
|
||||||
edge = {
|
|
||||||
text: {},
|
|
||||||
properties: {},
|
|
||||||
};
|
|
||||||
edge.id = skipEle.id;
|
|
||||||
edge.type = 'skip';
|
|
||||||
edge.sourceNodeId = skipEle.nowNodeCode;
|
|
||||||
edge.targetNodeId = skipEle.nextNodeCode;
|
|
||||||
edge.text = { value: skipEle.skipName };
|
|
||||||
edge.properties.skipCondition = skipEle.skipCondition;
|
|
||||||
edge.properties.skipName = skipEle.skipName;
|
|
||||||
edge.properties.skipType = skipEle.skipType;
|
|
||||||
const expr = skipEle.expr;
|
|
||||||
if (expr) {
|
|
||||||
edge.properties.expr = skipEle.expr;
|
|
||||||
}
|
|
||||||
const coordinate = skipEle.coordinate;
|
|
||||||
if (coordinate) {
|
|
||||||
const coordinateXy = coordinate.split('|');
|
|
||||||
edge.pointsList = [];
|
|
||||||
coordinateXy[0].split(';').forEach((item: any) => {
|
|
||||||
const pointArr = item.split(',');
|
|
||||||
edge.pointsList.push({
|
|
||||||
x: Number.parseInt(pointArr[0]),
|
|
||||||
y: Number.parseInt(pointArr[1]),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
edge.startPoint = edge.pointsList[0];
|
|
||||||
edge.endPoint = edge.pointsList[edge.pointsList.length - 1];
|
|
||||||
if (coordinateXy.length > 1) {
|
|
||||||
const textXy = coordinateXy[1].split(',');
|
|
||||||
edge.text.x = Number.parseInt(textXy[0]);
|
|
||||||
edge.text.y = Number.parseInt(textXy[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
graphData.edges.push(edge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(graphData);
|
|
||||||
return graphData;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将LogicFlow的数据转成warm-flow的json定义文件
|
|
||||||
* @param {*} data(...definitionInfo,nodes,edges)
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const logicFlowJsonToWarmFlow = (data: any) => {
|
|
||||||
// 先构建成流程对象
|
|
||||||
const definition: any = {
|
|
||||||
nodeList: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据节点的类型值,获取key
|
|
||||||
* @param {*} mapValue 节点类型映射
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getNodeTypeValue = (mapValue: any) => {
|
|
||||||
for (const key in NODE_TYPE_MAP) {
|
|
||||||
if ((NODE_TYPE_MAP as any)[key] === mapValue) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 根据节点的编码,获取节点的类型
|
|
||||||
* @param {*} nodeCode 当前节点名称
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getNodeType = (nodeCode: any) => {
|
|
||||||
for (const node of definition.nodeList) {
|
|
||||||
if (nodeCode === node.nodeCode) {
|
|
||||||
return node.nodeType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 拼接skip坐标
|
|
||||||
* @param {*} edge logicFlow的edge
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getCoordinate = (edge: any) => {
|
|
||||||
let coordinate = '';
|
|
||||||
for (let i = 0; i < edge.pointsList.length; i++) {
|
|
||||||
coordinate = `${
|
|
||||||
coordinate + Number.parseInt(edge.pointsList[i].x)
|
|
||||||
},${Number.parseInt(edge.pointsList[i].y)}`;
|
|
||||||
if (i !== edge.pointsList.length - 1) {
|
|
||||||
coordinate = `${coordinate};`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (edge.text) {
|
|
||||||
coordinate = `${coordinate}|${Number.parseInt(edge.text.x)},${Number.parseInt(edge.text.y)}`;
|
|
||||||
}
|
|
||||||
return coordinate;
|
|
||||||
};
|
|
||||||
// 流程定义
|
|
||||||
definition.id = data.id;
|
|
||||||
definition.flowCode = data.flowCode;
|
|
||||||
definition.flowName = data.flowName;
|
|
||||||
definition.version = data.version;
|
|
||||||
definition.fromCustom = data.fromCustom;
|
|
||||||
definition.fromPath = data.fromPath;
|
|
||||||
// 流程节点
|
|
||||||
data.nodes.forEach((anyNode: any) => {
|
|
||||||
const node: any = {};
|
|
||||||
node.nodeType = getNodeTypeValue(anyNode.type);
|
|
||||||
node.nodeCode = anyNode.id;
|
|
||||||
if (anyNode.text) {
|
|
||||||
node.nodeName = anyNode.text.value;
|
|
||||||
}
|
|
||||||
node.permissionFlag = anyNode.properties.permissionFlag;
|
|
||||||
node.nodeRatio = anyNode.properties.nodeRatio;
|
|
||||||
node.anyNodeSkip = anyNode.properties.anyNodeSkip;
|
|
||||||
node.listenerType = anyNode.properties.listenerType;
|
|
||||||
node.listenerPath = anyNode.properties.listenerPath;
|
|
||||||
node.formCustom = anyNode.properties.formCustom;
|
|
||||||
node.formPath = anyNode.properties.formPath;
|
|
||||||
node.ext = [];
|
|
||||||
for (const key in anyNode.properties.ext) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(anyNode.properties.ext, key)) {
|
|
||||||
const e = anyNode.properties.ext[key];
|
|
||||||
node.ext.push({ code: key, value: Array.isArray(e) ? e.join(',') : e });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.ext = JSON.stringify(node.ext);
|
|
||||||
node.coordinate = `${anyNode.x},${anyNode.y}`;
|
|
||||||
if (anyNode.text && anyNode.text.x && anyNode.text.y) {
|
|
||||||
node.coordinate = `${node.coordinate}|${anyNode.text.x},${anyNode.text.y}`;
|
|
||||||
}
|
|
||||||
node.handlerType = anyNode.properties.handlerType;
|
|
||||||
node.handlerPath = anyNode.properties.handlerPath;
|
|
||||||
node.version = definition.version;
|
|
||||||
node.skipList = [];
|
|
||||||
data.edges.forEach((anyEdge: any) => {
|
|
||||||
if (anyEdge.sourceNodeId === anyNode.id) {
|
|
||||||
const skip: any = {};
|
|
||||||
skip.skipType = anyEdge.properties.skipType;
|
|
||||||
skip.skipCondition = anyEdge.properties.skipCondition;
|
|
||||||
skip.skipName = anyEdge?.text?.value || anyEdge.properties.skipName;
|
|
||||||
skip.nowNodeCode = anyEdge.sourceNodeId;
|
|
||||||
skip.nowNodeType = getNodeType(skip.nowNodeCode);
|
|
||||||
skip.nextNodeCode = anyEdge.targetNodeId;
|
|
||||||
skip.nextNodeType = getNodeType(skip.nextNodeCode);
|
|
||||||
skip.coordinate = getCoordinate(anyEdge);
|
|
||||||
node.skipList.push(skip);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
definition.nodeList.push(node);
|
|
||||||
});
|
|
||||||
return JSON.stringify(definition);
|
|
||||||
};
|
|
||||||
37
apps/web-antd/src/views/workflow/components/hook.ts
Normal file
37
apps/web-antd/src/views/workflow/components/hook.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { onMounted, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { usePreferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* warmflow ref相关操作
|
||||||
|
* @returns hook
|
||||||
|
*/
|
||||||
|
export function useWarmflowIframe() {
|
||||||
|
const iframeRef = useTemplateRef<HTMLIFrameElement>('iframeRef');
|
||||||
|
const { isDark } = usePreferences();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
/**
|
||||||
|
* load只是iframe加载完 而非vue加载完
|
||||||
|
*/
|
||||||
|
iframeRef.value?.addEventListener('load', async () => {
|
||||||
|
/**
|
||||||
|
* TODO: 这里可以优化 因为拿不到内部vue的mount状态
|
||||||
|
*/
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
const theme = isDark.value ? 'theme-dark' : 'theme-light';
|
||||||
|
iframeRef.value?.contentWindow?.postMessage({ type: theme });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听主题切换 通知iframe切换
|
||||||
|
watch(isDark, (dark) => {
|
||||||
|
if (!iframeRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const theme = dark ? 'theme-dark' : 'theme-light';
|
||||||
|
iframeRef.value.contentWindow?.postMessage({ type: theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
return { iframeRef };
|
||||||
|
}
|
||||||
@@ -73,6 +73,7 @@ onMounted(loadTree);
|
|||||||
v-model:value="searchValue"
|
v-model:value="searchValue"
|
||||||
:placeholder="$t('pages.common.search')"
|
:placeholder="$t('pages.common.search')"
|
||||||
size="small"
|
size="small"
|
||||||
|
allow-clear
|
||||||
>
|
>
|
||||||
<template #enterButton>
|
<template #enterButton>
|
||||||
<a-button @click="handleReload">
|
<a-button @click="handleReload">
|
||||||
@@ -95,9 +96,9 @@ onMounted(loadTree);
|
|||||||
@select="$emit('select')"
|
@select="$emit('select')"
|
||||||
>
|
>
|
||||||
<template #title="{ label }">
|
<template #title="{ label }">
|
||||||
<span v-if="label.indexOf(searchValue) > -1">
|
<span v-if="label.includes(searchValue)">
|
||||||
{{ label.substring(0, label.indexOf(searchValue)) }}
|
{{ label.substring(0, label.indexOf(searchValue)) }}
|
||||||
<span style="color: #f50">{{ searchValue }}</span>
|
<span class="text-primary">{{ searchValue }}</span>
|
||||||
{{
|
{{
|
||||||
label.substring(
|
label.substring(
|
||||||
label.indexOf(searchValue) + searchValue.length,
|
label.indexOf(searchValue) + searchValue.length,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/docs",
|
"name": "@vben/docs",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vitepress build",
|
"build": "vitepress build",
|
||||||
|
|||||||
@@ -150,8 +150,8 @@ export async function saveUserApi(user: UserInfo) {
|
|||||||
```ts
|
```ts
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export async function deleteUserApi(user: UserInfo) {
|
export async function deleteUserApi(userId: number) {
|
||||||
return requestClient.delete<boolean>(`/user/${user.id}`, user);
|
return requestClient.delete<boolean>(`/user/${userId}`);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/
|
|||||||
console.log(import.meta.env.VITE_PROT);
|
console.log(import.meta.env.VITE_PROT);
|
||||||
```
|
```
|
||||||
|
|
||||||
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging. :::
|
- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -138,6 +138,27 @@ To add a new dynamically modifiable configuration item, simply follow the steps
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- In `packages/effects/hooks/src/use-app-config.ts`, add the corresponding configuration item, such as:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function useAppConfig(
|
||||||
|
env: Record<string, any>,
|
||||||
|
isProduction: boolean,
|
||||||
|
): ApplicationConfig {
|
||||||
|
// In production environment, directly use the window._VBEN_ADMIN_PRO_APP_CONF_ global variable
|
||||||
|
const config = isProduction
|
||||||
|
? window._VBEN_ADMIN_PRO_APP_CONF_
|
||||||
|
: (env as VbenAdminProAppConfigRaw);
|
||||||
|
|
||||||
|
const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++]
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiURL: VITE_GLOB_API_URL,
|
||||||
|
otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
At this point, you can use the `useAppConfig` method within the project to access the newly added configuration item.
|
At this point, you can use the `useAppConfig` method within the project to access the newly added configuration item.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -180,8 +180,8 @@ export async function saveUserApi(user: UserInfo) {
|
|||||||
```ts
|
```ts
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export async function deleteUserApi(user: UserInfo) {
|
export async function deleteUserApi(userId: number) {
|
||||||
return requestClient.delete<boolean>(`/user/${user.id}`, user);
|
return requestClient.delete<boolean>(`/user/${userId}`);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
console.log(import.meta.env.VITE_PROT);
|
console.log(import.meta.env.VITE_PROT);
|
||||||
```
|
```
|
||||||
|
|
||||||
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中. :::
|
- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -137,6 +137,27 @@ const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- 在 `packages/effects/hooks/src/use-app-config.ts` 中,新增对应的配置项,如:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function useAppConfig(
|
||||||
|
env: Record<string, any>,
|
||||||
|
isProduction: boolean,
|
||||||
|
): ApplicationConfig {
|
||||||
|
// 生产环境下,直接使用 window._VBEN_ADMIN_PRO_APP_CONF_ 全局变量
|
||||||
|
const config = isProduction
|
||||||
|
? window._VBEN_ADMIN_PRO_APP_CONF_
|
||||||
|
: (env as VbenAdminProAppConfigRaw);
|
||||||
|
|
||||||
|
const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++]
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiURL: VITE_GLOB_API_URL,
|
||||||
|
otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
到这里,就可以在项目内使用 `useAppConfig`方法获取到新增的配置项了。
|
到这里,就可以在项目内使用 `useAppConfig`方法获取到新增的配置项了。
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/commitlint-config",
|
"name": "@vben/commitlint-config",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/stylelint-config",
|
"name": "@vben/stylelint-config",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/node-utils",
|
"name": "@vben/node-utils",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/tailwind-config",
|
"name": "@vben/tailwind-config",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/tsconfig",
|
"name": "@vben/tsconfig",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vben-admin-monorepo",
|
"name": "vben-admin-monorepo",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"monorepo",
|
"monorepo",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/design",
|
"name": "@vben-core/design",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/icons",
|
"name": "@vben-core/icons",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/shared",
|
"name": "@vben-core/shared",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/typings",
|
"name": "@vben-core/typings",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
4
packages/@core/base/typings/src/basic.d.ts
vendored
4
packages/@core/base/typings/src/basic.d.ts
vendored
@@ -12,6 +12,10 @@ interface BasicUserInfo {
|
|||||||
* 头像
|
* 头像
|
||||||
*/
|
*/
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
/**
|
||||||
|
* 邮箱
|
||||||
|
*/
|
||||||
|
email: string;
|
||||||
/**
|
/**
|
||||||
* 用户权限
|
* 用户权限
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/composables",
|
"name": "@vben-core/composables",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/preferences",
|
"name": "@vben-core/preferences",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/form-ui",
|
"name": "@vben-core/form-ui",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/layout-ui",
|
"name": "@vben-core/layout-ui",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/menu-ui",
|
"name": "@vben-core/menu-ui",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ const props = withDefaults(defineProps<AlertProps>(), {
|
|||||||
bordered: true,
|
bordered: true,
|
||||||
buttonAlign: 'end',
|
buttonAlign: 'end',
|
||||||
centered: true,
|
centered: true,
|
||||||
containerClass: 'w-[520px]',
|
|
||||||
});
|
});
|
||||||
const emits = defineEmits(['closed', 'confirm', 'opened']);
|
const emits = defineEmits(['closed', 'confirm', 'opened']);
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
@@ -148,7 +147,7 @@ async function handleOpenChange(val: boolean) {
|
|||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
containerClass,
|
containerClass,
|
||||||
'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]',
|
'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:w-[520px] sm:max-w-[80%] sm:rounded-[var(--radius)]',
|
||||||
{
|
{
|
||||||
'border-border border': bordered,
|
'border-border border': bordered,
|
||||||
'shadow-3xl': !bordered,
|
'shadow-3xl': !bordered,
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
|
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
|
||||||
|
|
||||||
import { computed, provide, ref, unref, useId, watch } from 'vue';
|
import {
|
||||||
|
computed,
|
||||||
|
onDeactivated,
|
||||||
|
provide,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
useId,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
@@ -94,6 +102,16 @@ const {
|
|||||||
// },
|
// },
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
|
||||||
|
*/
|
||||||
|
onDeactivated(() => {
|
||||||
|
// 如果弹窗没有被挂载到内容区域,则关闭弹窗
|
||||||
|
if (!appendToMain.value) {
|
||||||
|
props.drawerApi?.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function interactOutside(e: Event) {
|
function interactOutside(e: Event) {
|
||||||
if (!closeOnClickModal.value || submitting.value) {
|
if (!closeOnClickModal.value || submitting.value) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
h,
|
h,
|
||||||
inject,
|
inject,
|
||||||
nextTick,
|
nextTick,
|
||||||
onDeactivated,
|
|
||||||
provide,
|
provide,
|
||||||
reactive,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
@@ -72,13 +71,6 @@ export function useVbenDrawer<
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
|
|
||||||
*/
|
|
||||||
onDeactivated(() => {
|
|
||||||
(extendedApi as ExtendedDrawerApi)?.close?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
|
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { ExtendedModalApi, ModalProps } from './modal';
|
import type { ExtendedModalApi, ModalProps } from './modal';
|
||||||
|
|
||||||
import { computed, nextTick, provide, ref, unref, useId, watch } from 'vue';
|
import {
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onDeactivated,
|
||||||
|
provide,
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
useId,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
@@ -96,10 +105,17 @@ const shouldDraggable = computed(
|
|||||||
() => draggable.value && !shouldFullscreen.value && header.value,
|
() => draggable.value && !shouldFullscreen.value && header.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getAppendTo = computed(() => {
|
||||||
|
return appendToMain.value
|
||||||
|
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
|
||||||
|
: undefined;
|
||||||
|
});
|
||||||
|
|
||||||
const { dragging, transform } = useModalDraggable(
|
const { dragging, transform } = useModalDraggable(
|
||||||
dialogRef,
|
dialogRef,
|
||||||
headerRef,
|
headerRef,
|
||||||
shouldDraggable,
|
shouldDraggable,
|
||||||
|
getAppendTo,
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstOpened = ref(false);
|
const firstOpened = ref(false);
|
||||||
@@ -135,6 +151,16 @@ watch(
|
|||||||
// },
|
// },
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
|
||||||
|
*/
|
||||||
|
onDeactivated(() => {
|
||||||
|
// 如果弹窗没有被挂载到内容区域,则关闭弹窗
|
||||||
|
if (!appendToMain.value) {
|
||||||
|
props.modalApi?.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function handleFullscreen() {
|
function handleFullscreen() {
|
||||||
props.modalApi?.setState((prev) => {
|
props.modalApi?.setState((prev) => {
|
||||||
// if (prev.fullscreen) {
|
// if (prev.fullscreen) {
|
||||||
@@ -179,11 +205,6 @@ function handleFocusOutside(e: Event) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
const getAppendTo = computed(() => {
|
|
||||||
return appendToMain.value
|
|
||||||
? `#${ELEMENT_ID_MAIN_CONTENT}>div:not(.absolute)>div`
|
|
||||||
: undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getForceMount = computed(() => {
|
const getForceMount = computed(() => {
|
||||||
return !unref(destroyOnClose) && unref(firstOpened);
|
return !unref(destroyOnClose) && unref(firstOpened);
|
||||||
@@ -205,7 +226,8 @@ function handleClosed() {
|
|||||||
:append-to="getAppendTo"
|
:append-to="getAppendTo"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-[var(--radius)]',
|
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0',
|
||||||
|
shouldFullscreen ? 'sm:rounded-none' : 'sm:rounded-[var(--radius)]',
|
||||||
modalClass,
|
modalClass,
|
||||||
{
|
{
|
||||||
'border-border border': bordered,
|
'border-border border': bordered,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export function useModalDraggable(
|
|||||||
targetRef: Ref<HTMLElement | undefined>,
|
targetRef: Ref<HTMLElement | undefined>,
|
||||||
dragRef: Ref<HTMLElement | undefined>,
|
dragRef: Ref<HTMLElement | undefined>,
|
||||||
draggable: ComputedRef<boolean>,
|
draggable: ComputedRef<boolean>,
|
||||||
|
containerSelector?: ComputedRef<string | undefined>,
|
||||||
) {
|
) {
|
||||||
const transform = reactive({
|
const transform = reactive({
|
||||||
offsetX: 0,
|
offsetX: 0,
|
||||||
@@ -29,20 +30,36 @@ export function useModalDraggable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetRect = targetRef.value.getBoundingClientRect();
|
const targetRect = targetRef.value.getBoundingClientRect();
|
||||||
|
|
||||||
const { offsetX, offsetY } = transform;
|
const { offsetX, offsetY } = transform;
|
||||||
const targetLeft = targetRect.left;
|
const targetLeft = targetRect.left;
|
||||||
const targetTop = targetRect.top;
|
const targetTop = targetRect.top;
|
||||||
const targetWidth = targetRect.width;
|
const targetWidth = targetRect.width;
|
||||||
const targetHeight = targetRect.height;
|
const targetHeight = targetRect.height;
|
||||||
|
|
||||||
|
let containerRect: DOMRect | null = null;
|
||||||
|
|
||||||
|
if (containerSelector?.value) {
|
||||||
|
const container = document.querySelector(containerSelector.value);
|
||||||
|
if (container) {
|
||||||
|
containerRect = container.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxLeft, maxTop, minLeft, minTop;
|
||||||
|
if (containerRect) {
|
||||||
|
minLeft = containerRect.left - targetLeft + offsetX;
|
||||||
|
maxLeft = containerRect.right - targetLeft - targetWidth + offsetX;
|
||||||
|
minTop = containerRect.top - targetTop + offsetY;
|
||||||
|
maxTop = containerRect.bottom - targetTop - targetHeight + offsetY;
|
||||||
|
} else {
|
||||||
const docElement = document.documentElement;
|
const docElement = document.documentElement;
|
||||||
const clientWidth = docElement.clientWidth;
|
const clientWidth = docElement.clientWidth;
|
||||||
const clientHeight = docElement.clientHeight;
|
const clientHeight = docElement.clientHeight;
|
||||||
|
minLeft = -targetLeft + offsetX;
|
||||||
const minLeft = -targetLeft + offsetX;
|
minTop = -targetTop + offsetY;
|
||||||
const minTop = -targetTop + offsetY;
|
maxLeft = clientWidth - targetLeft - targetWidth + offsetX;
|
||||||
const maxLeft = clientWidth - targetLeft - targetWidth + offsetX;
|
maxTop = clientHeight - targetTop - targetHeight + offsetY;
|
||||||
const maxTop = clientHeight - targetTop - targetHeight + offsetY;
|
}
|
||||||
|
|
||||||
const onMousemove = (e: MouseEvent) => {
|
const onMousemove = (e: MouseEvent) => {
|
||||||
let moveX = offsetX + e.clientX - downX;
|
let moveX = offsetX + e.clientX - downX;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
h,
|
h,
|
||||||
inject,
|
inject,
|
||||||
nextTick,
|
nextTick,
|
||||||
onDeactivated,
|
|
||||||
provide,
|
provide,
|
||||||
reactive,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
@@ -71,13 +70,6 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* 在开启keepAlive情况下 直接通过浏览器按钮/手势等返回 不会关闭弹窗
|
|
||||||
*/
|
|
||||||
onDeactivated(() => {
|
|
||||||
(extendedApi as ExtendedModalApi)?.close?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
return [Modal, extendedApi as ExtendedModalApi] as const;
|
return [Modal, extendedApi as ExtendedModalApi] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,8 +86,9 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
injectData.options?.onOpenChange?.(isOpen);
|
injectData.options?.onOpenChange?.(isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClosed = mergedOptions.onClosed;
|
||||||
mergedOptions.onClosed = () => {
|
mergedOptions.onClosed = () => {
|
||||||
options.onClosed?.();
|
onClosed?.();
|
||||||
if (mergedOptions.destroyOnClose) {
|
if (mergedOptions.destroyOnClose) {
|
||||||
injectData.reCreateModal?.();
|
injectData.reCreateModal?.();
|
||||||
}
|
}
|
||||||
@@ -129,6 +122,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
injectData.extendApi?.(extendedApi);
|
injectData.extendApi?.(extendedApi);
|
||||||
|
|
||||||
return [Modal, extendedApi] as const;
|
return [Modal, extendedApi] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/shadcn-ui",
|
"name": "@vben-core/shadcn-ui",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"#main": "./dist/index.mjs",
|
"#main": "./dist/index.mjs",
|
||||||
"#module": "./dist/index.mjs",
|
"#module": "./dist/index.mjs",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
|
|||||||
@@ -29,14 +29,25 @@ export type ValueType = boolean | number | string;
|
|||||||
|
|
||||||
export interface VbenButtonGroupProps
|
export interface VbenButtonGroupProps
|
||||||
extends Pick<VbenButtonProps, 'disabled'> {
|
extends Pick<VbenButtonProps, 'disabled'> {
|
||||||
|
/** 单选模式下允许清除选中 */
|
||||||
|
allowClear?: boolean;
|
||||||
|
/** 值改变前的回调 */
|
||||||
beforeChange?: (
|
beforeChange?: (
|
||||||
value: ValueType,
|
value: ValueType,
|
||||||
isChecked: boolean,
|
isChecked: boolean,
|
||||||
) => boolean | PromiseLike<boolean | undefined> | undefined;
|
) => boolean | PromiseLike<boolean | undefined> | undefined;
|
||||||
|
/** 按钮样式 */
|
||||||
btnClass?: any;
|
btnClass?: any;
|
||||||
|
/** 按钮间隔距离 */
|
||||||
gap?: number;
|
gap?: number;
|
||||||
|
/** 多选模式下限制最多选择的数量。0表示不限制 */
|
||||||
|
maxCount?: number;
|
||||||
|
/** 是否允许多选 */
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
/** 选项 */
|
||||||
options?: { [key: string]: any; label: CustomRenderType; value: ValueType }[];
|
options?: { [key: string]: any; label: CustomRenderType; value: ValueType }[];
|
||||||
|
/** 显示图标 */
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
|
/** 尺寸 */
|
||||||
size?: 'large' | 'middle' | 'small';
|
size?: 'large' | 'middle' | 'small';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
showIcon: true,
|
showIcon: true,
|
||||||
size: 'middle',
|
size: 'middle',
|
||||||
|
allowClear: false,
|
||||||
|
maxCount: 0,
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['btnClick']);
|
const emit = defineEmits(['btnClick']);
|
||||||
const btnDefaultProps = computed(() => {
|
const btnDefaultProps = computed(() => {
|
||||||
@@ -82,13 +84,23 @@ async function onBtnClick(value: ValueType) {
|
|||||||
if (innerValue.value.includes(value)) {
|
if (innerValue.value.includes(value)) {
|
||||||
innerValue.value = innerValue.value.filter((item) => item !== value);
|
innerValue.value = innerValue.value.filter((item) => item !== value);
|
||||||
} else {
|
} else {
|
||||||
|
if (props.maxCount > 0 && innerValue.value.length >= props.maxCount) {
|
||||||
|
innerValue.value = innerValue.value.slice(0, props.maxCount - 1);
|
||||||
|
}
|
||||||
innerValue.value.push(value);
|
innerValue.value.push(value);
|
||||||
}
|
}
|
||||||
modelValue.value = innerValue.value;
|
modelValue.value = innerValue.value;
|
||||||
|
} else {
|
||||||
|
if (props.allowClear && innerValue.value.includes(value)) {
|
||||||
|
innerValue.value = [];
|
||||||
|
modelValue.value = undefined;
|
||||||
|
emit('btnClick', undefined);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
innerValue.value = [value];
|
innerValue.value = [value];
|
||||||
modelValue.value = value;
|
modelValue.value = value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
emit('btnClick', value);
|
emit('btnClick', value);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -110,14 +122,21 @@ async function onBtnClick(value: ValueType) {
|
|||||||
v-bind="btnDefaultProps"
|
v-bind="btnDefaultProps"
|
||||||
:variant="innerValue.includes(btn.value) ? 'default' : 'outline'"
|
:variant="innerValue.includes(btn.value) ? 'default' : 'outline'"
|
||||||
@click="onBtnClick(btn.value)"
|
@click="onBtnClick(btn.value)"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<div class="icon-wrapper" v-if="props.showIcon">
|
<div class="icon-wrapper" v-if="props.showIcon">
|
||||||
|
<slot
|
||||||
|
name="icon"
|
||||||
|
:loading="loadingValues.includes(btn.value)"
|
||||||
|
:checked="innerValue.includes(btn.value)"
|
||||||
|
>
|
||||||
<LoaderCircle
|
<LoaderCircle
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
v-if="loadingValues.includes(btn.value)"
|
v-if="loadingValues.includes(btn.value)"
|
||||||
/>
|
/>
|
||||||
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
|
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
|
||||||
<Circle v-else />
|
<Circle v-else />
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<slot name="option" :label="btn.label" :value="btn.value" :data="btn">
|
<slot name="option" :label="btn.label" :value="btn.value" :data="btn">
|
||||||
<VbenRenderContent :content="btn.label" />
|
<VbenRenderContent :content="btn.label" />
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ defineExpose({
|
|||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'z-popup bg-background w-full p-6 shadow-lg outline-none sm:rounded-xl',
|
'z-popup bg-background p-6 shadow-lg outline-none sm:rounded-xl',
|
||||||
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||||
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const props = withDefaults(defineProps<TreeProps>(), {
|
|||||||
defaultExpandedKeys: () => [],
|
defaultExpandedKeys: () => [],
|
||||||
defaultExpandedLevel: 0,
|
defaultExpandedLevel: 0,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
disabledField: 'disabled',
|
||||||
expanded: () => [],
|
expanded: () => [],
|
||||||
iconField: 'icon',
|
iconField: 'icon',
|
||||||
labelField: 'label',
|
labelField: 'label',
|
||||||
@@ -101,16 +102,37 @@ function updateTreeValue() {
|
|||||||
if (val === undefined) {
|
if (val === undefined) {
|
||||||
treeValue.value = undefined;
|
treeValue.value = undefined;
|
||||||
} else {
|
} else {
|
||||||
treeValue.value = Array.isArray(val)
|
if (Array.isArray(val)) {
|
||||||
? val.map((v) => getItemByValue(v))
|
const filteredValues = val.filter((v) => {
|
||||||
: getItemByValue(val);
|
const item = getItemByValue(v);
|
||||||
|
return item && !get(item, props.disabledField);
|
||||||
|
});
|
||||||
|
treeValue.value = filteredValues.map((v) => getItemByValue(v));
|
||||||
|
|
||||||
|
if (filteredValues.length !== val.length) {
|
||||||
|
modelValue.value = filteredValues;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const item = getItemByValue(val);
|
||||||
|
if (item && !get(item, props.disabledField)) {
|
||||||
|
treeValue.value = item;
|
||||||
|
} else {
|
||||||
|
treeValue.value = undefined;
|
||||||
|
modelValue.value = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateModelValue(val: Arrayable<Recordable<any>>) {
|
function updateModelValue(val: Arrayable<Recordable<any>>) {
|
||||||
modelValue.value = Array.isArray(val)
|
if (Array.isArray(val)) {
|
||||||
? val.map((v) => get(v, props.valueField))
|
const filteredVal = val.filter((v) => !get(v, props.disabledField));
|
||||||
: get(val, props.valueField);
|
modelValue.value = filteredVal.map((v) => get(v, props.valueField));
|
||||||
|
} else {
|
||||||
|
if (val && !get(val, props.disabledField)) {
|
||||||
|
modelValue.value = get(val, props.valueField);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandToLevel(level: number) {
|
function expandToLevel(level: number) {
|
||||||
@@ -149,10 +171,18 @@ function collapseAll() {
|
|||||||
expanded.value = [];
|
expanded.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isNodeDisabled(item: FlattenedItem<Recordable<any>>) {
|
||||||
|
return props.disabled || get(item.value, props.disabledField);
|
||||||
|
}
|
||||||
|
|
||||||
function onToggle(item: FlattenedItem<Recordable<any>>) {
|
function onToggle(item: FlattenedItem<Recordable<any>>) {
|
||||||
emits('expand', item);
|
emits('expand', item);
|
||||||
}
|
}
|
||||||
function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
|
function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
|
||||||
|
if (isNodeDisabled(item)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!props.checkStrictly &&
|
!props.checkStrictly &&
|
||||||
props.multiple &&
|
props.multiple &&
|
||||||
@@ -224,28 +254,34 @@ defineExpose({
|
|||||||
:class="
|
:class="
|
||||||
cn('cursor-pointer', getNodeClass?.(item), {
|
cn('cursor-pointer', getNodeClass?.(item), {
|
||||||
'data-[selected]:bg-accent': !multiple,
|
'data-[selected]:bg-accent': !multiple,
|
||||||
'cursor-not-allowed': disabled,
|
'cursor-not-allowed': isNodeDisabled(item),
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
v-bind="
|
v-bind="
|
||||||
Object.assign(item.bind, {
|
Object.assign(item.bind, {
|
||||||
onfocus: disabled ? 'this.blur()' : undefined,
|
onfocus: isNodeDisabled(item) ? 'this.blur()' : undefined,
|
||||||
|
disabled: isNodeDisabled(item),
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
@select="
|
@select="
|
||||||
(event) => {
|
(event: any) => {
|
||||||
|
if (isNodeDisabled(item)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.detail.originalEvent.type === 'click') {
|
if (event.detail.originalEvent.type === 'click') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
!disabled && onSelect(item, event.detail.isSelected);
|
onSelect(item, event.detail.isSelected);
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@toggle="
|
@toggle="
|
||||||
(event) => {
|
(event: any) => {
|
||||||
if (event.detail.originalEvent.type === 'click') {
|
if (event.detail.originalEvent.type === 'click') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
!disabled && onToggle(item);
|
!isNodeDisabled(item) && onToggle(item);
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
|
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
|
||||||
@@ -266,24 +302,32 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="multiple"
|
v-if="multiple"
|
||||||
:checked="isSelected"
|
:checked="isSelected && !isNodeDisabled(item)"
|
||||||
:disabled="disabled"
|
:disabled="isNodeDisabled(item)"
|
||||||
:indeterminate="isIndeterminate"
|
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
(event: MouseEvent) => {
|
||||||
!disabled && handleSelect();
|
if (isNodeDisabled(item)) {
|
||||||
// onSelect(item, !isSelected);
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleSelect();
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1 pl-2"
|
class="flex items-center gap-1 pl-2"
|
||||||
@click="
|
@click="
|
||||||
(_event) => {
|
(event: MouseEvent) => {
|
||||||
// $event.stopPropagation();
|
if (isNodeDisabled(item)) {
|
||||||
// $event.preventDefault();
|
event.preventDefault();
|
||||||
!disabled && handleSelect();
|
event.stopPropagation();
|
||||||
// onSelect(item, !isSelected);
|
return;
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
handleSelect();
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export interface TreeProps {
|
|||||||
defaultValue?: Arrayable<number | string>;
|
defaultValue?: Arrayable<number | string>;
|
||||||
/** 禁用 */
|
/** 禁用 */
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
/** 禁用字段名 */
|
||||||
|
disabledField?: string;
|
||||||
/** 自定义节点类名 */
|
/** 自定义节点类名 */
|
||||||
getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string;
|
getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string;
|
||||||
iconField?: string;
|
iconField?: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/tabs-ui",
|
"name": "@vben-core/tabs-ui",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/constants",
|
"name": "@vben/constants",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/access",
|
"name": "@vben/access",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/common-ui",
|
"name": "@vben/common-ui",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
"@vueuse/core": "catalog:",
|
"@vueuse/core": "catalog:",
|
||||||
"@vueuse/integrations": "catalog:",
|
"@vueuse/integrations": "catalog:",
|
||||||
"codemirror": "6.0.1",
|
"codemirror": "6.0.1",
|
||||||
|
"json-bigint": "catalog:",
|
||||||
"qrcode": "catalog:",
|
"qrcode": "catalog:",
|
||||||
"tippy.js": "catalog:",
|
"tippy.js": "catalog:",
|
||||||
"vditor": "3.10.9",
|
"vditor": "3.10.9",
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import type { Component } from 'vue';
|
|||||||
|
|
||||||
import type { AnyPromiseFunction } from '@vben/types';
|
import type { AnyPromiseFunction } from '@vben/types';
|
||||||
|
|
||||||
import { computed, ref, unref, useAttrs, watch } from 'vue';
|
import { computed, nextTick, ref, unref, useAttrs, watch } from 'vue';
|
||||||
|
|
||||||
import { LoaderCircle } from '@vben/icons';
|
import { LoaderCircle } from '@vben/icons';
|
||||||
|
|
||||||
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
|
import { cloneDeep, get, isEqual, isFunction } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { objectOmit } from '@vueuse/core';
|
import { objectOmit } from '@vueuse/core';
|
||||||
|
|
||||||
@@ -104,6 +104,8 @@ const refOptions = ref<OptionsItem[]>([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
// 首次是否加载过了
|
// 首次是否加载过了
|
||||||
const isFirstLoaded = ref(false);
|
const isFirstLoaded = ref(false);
|
||||||
|
// 标记是否有待处理的请求
|
||||||
|
const hasPendingRequest = ref(false);
|
||||||
|
|
||||||
const getOptions = computed(() => {
|
const getOptions = computed(() => {
|
||||||
const { labelField, valueField, childrenField, numberToString } = props;
|
const { labelField, valueField, childrenField, numberToString } = props;
|
||||||
@@ -146,18 +148,26 @@ const bindProps = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function fetchApi() {
|
async function fetchApi() {
|
||||||
let { api, beforeFetch, afterFetch, params, resultField } = props;
|
const { api, beforeFetch, afterFetch, resultField } = props;
|
||||||
|
|
||||||
if (!api || !isFunction(api) || loading.value) {
|
if (!api || !isFunction(api)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果正在加载,标记有待处理的请求并返回
|
||||||
|
if (loading.value) {
|
||||||
|
hasPendingRequest.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refOptions.value = [];
|
refOptions.value = [];
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
let finalParams = unref(mergedParams);
|
||||||
if (beforeFetch && isFunction(beforeFetch)) {
|
if (beforeFetch && isFunction(beforeFetch)) {
|
||||||
params = (await beforeFetch(params)) || params;
|
finalParams = (await beforeFetch(cloneDeep(finalParams))) || finalParams;
|
||||||
}
|
}
|
||||||
let res = await api(params);
|
let res = await api(finalParams);
|
||||||
if (afterFetch && isFunction(afterFetch)) {
|
if (afterFetch && isFunction(afterFetch)) {
|
||||||
res = (await afterFetch(res)) || res;
|
res = (await afterFetch(res)) || res;
|
||||||
}
|
}
|
||||||
@@ -177,6 +187,13 @@ async function fetchApi() {
|
|||||||
isFirstLoaded.value = false;
|
isFirstLoaded.value = false;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
// 如果有待处理的请求,立即触发新的请求
|
||||||
|
if (hasPendingRequest.value) {
|
||||||
|
hasPendingRequest.value = false;
|
||||||
|
// 使用 nextTick 确保状态更新完成后再触发新请求
|
||||||
|
await nextTick();
|
||||||
|
fetchApi();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +207,7 @@ async function handleFetchForVisible(visible: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = computed(() => {
|
const mergedParams = computed(() => {
|
||||||
return {
|
return {
|
||||||
...props.params,
|
...props.params,
|
||||||
...unref(innerParams),
|
...unref(innerParams),
|
||||||
@@ -198,7 +215,7 @@ const params = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
params,
|
mergedParams,
|
||||||
(value, oldValue) => {
|
(value, oldValue) => {
|
||||||
if (isEqual(value, oldValue)) {
|
if (isEqual(value, oldValue)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ const keyword = ref('');
|
|||||||
const keywordDebounce = refDebounced(keyword, 300);
|
const keywordDebounce = refDebounced(keyword, 300);
|
||||||
const innerIcons = ref<string[]>([]);
|
const innerIcons = ref<string[]>([]);
|
||||||
|
|
||||||
|
/* 当检索关键词变化时,重置分页 */
|
||||||
|
watch(keywordDebounce, () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
setCurrentPage(1);
|
||||||
|
});
|
||||||
|
|
||||||
watchDebounced(
|
watchDebounced(
|
||||||
() => props.prefix,
|
() => props.prefix,
|
||||||
async (prefix) => {
|
async (prefix) => {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import { $t } from '@vben/locales';
|
|||||||
|
|
||||||
import { isBoolean } from '@vben-core/shared/utils';
|
import { isBoolean } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import JsonBigint from 'json-bigint';
|
||||||
|
|
||||||
defineOptions({ name: 'JsonViewer' });
|
defineOptions({ name: 'JsonViewer' });
|
||||||
|
|
||||||
const props = withDefaults(defineProps<JsonViewerProps>(), {
|
const props = withDefaults(defineProps<JsonViewerProps>(), {
|
||||||
@@ -68,6 +71,20 @@ function handleClick(event: MouseEvent) {
|
|||||||
emit('click', event);
|
emit('click', event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 支持显示 bigint 数据,如较长的订单号
|
||||||
|
const jsonData = computed<Record<string, any>>(() => {
|
||||||
|
if (typeof props.value !== 'string') {
|
||||||
|
return props.value || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JsonBigint({ storeAsString: true }).parse(props.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JSON parse error:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const bindProps = computed<Recordable<any>>(() => {
|
const bindProps = computed<Recordable<any>>(() => {
|
||||||
const copyable = {
|
const copyable = {
|
||||||
copyText: $t('ui.jsonViewer.copy'),
|
copyText: $t('ui.jsonViewer.copy'),
|
||||||
@@ -79,6 +96,7 @@ const bindProps = computed<Recordable<any>>(() => {
|
|||||||
return {
|
return {
|
||||||
...props,
|
...props,
|
||||||
...attrs,
|
...attrs,
|
||||||
|
value: jsonData.value,
|
||||||
onCopied: (event: JsonViewerAction) => emit('copied', event),
|
onCopied: (event: JsonViewerAction) => emit('copied', event),
|
||||||
onKeyclick: (key: string) => emit('keyClick', key),
|
onKeyclick: (key: string) => emit('keyClick', key),
|
||||||
onClick: (event: MouseEvent) => handleClick(event),
|
onClick: (event: MouseEvent) => handleClick(event),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { AuthenticationProps } from './types';
|
|||||||
|
|
||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben-core/popup-ui';
|
import { useVbenModal } from '@vben-core/popup-ui';
|
||||||
import { Slot, VbenAvatar } from '@vben-core/shadcn-ui';
|
import { Slot, VbenAvatar } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/hooks",
|
"name": "@vben/hooks",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -2,48 +2,139 @@ import type { Arrayable, MaybeElementRef } from '@vueuse/core';
|
|||||||
|
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
import { computed, onUnmounted, ref, unref, watch } from 'vue';
|
import { computed, effectScope, onUnmounted, ref, unref, watch } from 'vue';
|
||||||
|
|
||||||
import { isFunction } from '@vben/utils';
|
import { isFunction } from '@vben/utils';
|
||||||
|
|
||||||
import { useElementHover } from '@vueuse/core';
|
import { useElementHover } from '@vueuse/core';
|
||||||
|
|
||||||
|
interface HoverDelayOptions {
|
||||||
|
/** 鼠标进入延迟时间 */
|
||||||
|
enterDelay?: (() => number) | number;
|
||||||
|
/** 鼠标离开延迟时间 */
|
||||||
|
leaveDelay?: (() => number) | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_LEAVE_DELAY = 500; // 鼠标离开延迟时间,默认为 500ms
|
||||||
|
const DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间,默认为 0(立即响应)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false
|
* 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false
|
||||||
* @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
|
* @param refElement 所有需要检测的元素。支持单个元素、元素数组或响应式引用的元素数组。如果鼠标在任何一个元素内部都会返回 true
|
||||||
* @param delay 延迟更新状态的时间
|
* @param delay 延迟更新状态的时间,可以是数字或包含进入/离开延迟的配置对象
|
||||||
* @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用
|
* @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用
|
||||||
*/
|
*/
|
||||||
export function useHoverToggle(
|
export function useHoverToggle(
|
||||||
refElement: Arrayable<MaybeElementRef>,
|
refElement: Arrayable<MaybeElementRef> | Ref<HTMLElement[] | null>,
|
||||||
delay: (() => number) | number = 500,
|
delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY,
|
||||||
) {
|
) {
|
||||||
const isHovers: Array<Ref<boolean>> = [];
|
// 兼容旧版本API
|
||||||
|
const normalizedOptions: HoverDelayOptions =
|
||||||
|
typeof delay === 'number' || isFunction(delay)
|
||||||
|
? { enterDelay: DEFAULT_ENTER_DELAY, leaveDelay: delay }
|
||||||
|
: {
|
||||||
|
enterDelay: DEFAULT_ENTER_DELAY,
|
||||||
|
leaveDelay: DEFAULT_LEAVE_DELAY,
|
||||||
|
...delay,
|
||||||
|
};
|
||||||
|
|
||||||
const value = ref(false);
|
const value = ref(false);
|
||||||
const timer = ref<ReturnType<typeof setTimeout> | undefined>();
|
const enterTimer = ref<ReturnType<typeof setTimeout> | undefined>();
|
||||||
const refs = Array.isArray(refElement) ? refElement : [refElement];
|
const leaveTimer = ref<ReturnType<typeof setTimeout> | undefined>();
|
||||||
refs.forEach((refEle) => {
|
const hoverScopes = ref<ReturnType<typeof effectScope>[]>([]);
|
||||||
|
|
||||||
|
// 使用计算属性包装 refElement,使其响应式变化
|
||||||
|
const refs = computed(() => {
|
||||||
|
const raw = unref(refElement);
|
||||||
|
if (raw === null) return [];
|
||||||
|
return Array.isArray(raw) ? raw : [raw];
|
||||||
|
});
|
||||||
|
// 存储所有 hover 状态
|
||||||
|
const isHovers = ref<Array<Ref<boolean>>>([]);
|
||||||
|
|
||||||
|
// 更新 hover 监听的函数
|
||||||
|
function updateHovers() {
|
||||||
|
// 停止并清理之前的作用域
|
||||||
|
hoverScopes.value.forEach((scope) => scope.stop());
|
||||||
|
hoverScopes.value = [];
|
||||||
|
|
||||||
|
isHovers.value = refs.value.map((refEle) => {
|
||||||
|
if (!refEle) {
|
||||||
|
return ref(false);
|
||||||
|
}
|
||||||
const eleRef = computed(() => {
|
const eleRef = computed(() => {
|
||||||
const ele = unref(refEle);
|
const ele = unref(refEle);
|
||||||
return ele instanceof Element ? ele : (ele?.$el as Element);
|
return ele instanceof Element ? ele : (ele?.$el as Element);
|
||||||
});
|
});
|
||||||
const isHover = useElementHover(eleRef);
|
|
||||||
isHovers.push(isHover);
|
|
||||||
});
|
|
||||||
const isOutsideAll = computed(() => isHovers.every((v) => !v.value));
|
|
||||||
|
|
||||||
function setValueDelay(val: boolean) {
|
// 为每个元素创建独立的作用域
|
||||||
timer.value && clearTimeout(timer.value);
|
const scope = effectScope();
|
||||||
timer.value = setTimeout(
|
const hoverRef = scope.run(() => useElementHover(eleRef)) || ref(false);
|
||||||
() => {
|
hoverScopes.value.push(scope);
|
||||||
value.value = val;
|
|
||||||
timer.value = undefined;
|
return hoverRef;
|
||||||
},
|
});
|
||||||
isFunction(delay) ? delay() : delay,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const watcher = watch(
|
// 监听元素数量变化,避免过度执行
|
||||||
|
const elementsCount = computed(() => {
|
||||||
|
const raw = unref(refElement);
|
||||||
|
if (raw === null) return 0;
|
||||||
|
return Array.isArray(raw) ? raw.length : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始设置
|
||||||
|
updateHovers();
|
||||||
|
|
||||||
|
// 只在元素数量变化时重新设置监听器
|
||||||
|
const stopWatcher = watch(elementsCount, updateHovers, { deep: false });
|
||||||
|
|
||||||
|
const isOutsideAll = computed(() => isHovers.value.every((v) => !v.value));
|
||||||
|
|
||||||
|
function clearTimers() {
|
||||||
|
if (enterTimer.value) {
|
||||||
|
clearTimeout(enterTimer.value);
|
||||||
|
enterTimer.value = undefined;
|
||||||
|
}
|
||||||
|
if (leaveTimer.value) {
|
||||||
|
clearTimeout(leaveTimer.value);
|
||||||
|
leaveTimer.value = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValueDelay(val: boolean) {
|
||||||
|
clearTimers();
|
||||||
|
|
||||||
|
if (val) {
|
||||||
|
// 鼠标进入
|
||||||
|
const enterDelay = normalizedOptions.enterDelay ?? DEFAULT_ENTER_DELAY;
|
||||||
|
const delayTime = isFunction(enterDelay) ? enterDelay() : enterDelay;
|
||||||
|
|
||||||
|
if (delayTime <= 0) {
|
||||||
|
value.value = true;
|
||||||
|
} else {
|
||||||
|
enterTimer.value = setTimeout(() => {
|
||||||
|
value.value = true;
|
||||||
|
enterTimer.value = undefined;
|
||||||
|
}, delayTime);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 鼠标离开
|
||||||
|
const leaveDelay = normalizedOptions.leaveDelay ?? DEFAULT_LEAVE_DELAY;
|
||||||
|
const delayTime = isFunction(leaveDelay) ? leaveDelay() : leaveDelay;
|
||||||
|
|
||||||
|
if (delayTime <= 0) {
|
||||||
|
value.value = false;
|
||||||
|
} else {
|
||||||
|
leaveTimer.value = setTimeout(() => {
|
||||||
|
value.value = false;
|
||||||
|
leaveTimer.value = undefined;
|
||||||
|
}, delayTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoverWatcher = watch(
|
||||||
isOutsideAll,
|
isOutsideAll,
|
||||||
(val) => {
|
(val) => {
|
||||||
setValueDelay(!val);
|
setValueDelay(!val);
|
||||||
@@ -53,15 +144,19 @@ export function useHoverToggle(
|
|||||||
|
|
||||||
const controller = {
|
const controller = {
|
||||||
enable() {
|
enable() {
|
||||||
watcher.resume();
|
hoverWatcher.resume();
|
||||||
},
|
},
|
||||||
disable() {
|
disable() {
|
||||||
watcher.pause();
|
hoverWatcher.pause();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
timer.value && clearTimeout(timer.value);
|
clearTimers();
|
||||||
|
// 停止监听器
|
||||||
|
stopWatcher();
|
||||||
|
// 停止所有剩余的作用域
|
||||||
|
hoverScopes.value.forEach((scope) => scope.stop());
|
||||||
});
|
});
|
||||||
|
|
||||||
return [value, controller] as [typeof value, typeof controller];
|
return [value, controller] as [typeof value, typeof controller];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/layouts",
|
"name": "@vben/layouts",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
|
|||||||
</template>
|
</template>
|
||||||
</AuthenticationFormView>
|
</AuthenticationFormView>
|
||||||
|
|
||||||
|
<slot name="logo">
|
||||||
<!-- 头部 Logo 和应用名称 -->
|
<!-- 头部 Logo 和应用名称 -->
|
||||||
<div
|
<div
|
||||||
v-if="logo || appName"
|
v-if="logo || appName"
|
||||||
@@ -77,6 +78,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</slot>
|
||||||
|
|
||||||
<!-- 系统介绍 -->
|
<!-- 系统介绍 -->
|
||||||
<div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block">
|
<div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block">
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ interface Props {
|
|||||||
/**
|
/**
|
||||||
* 菜单数组
|
* 菜单数组
|
||||||
*/
|
*/
|
||||||
menus?: Array<{ handler: AnyFunction; icon?: Component; text: string }>;
|
menus?: Array<{
|
||||||
|
handler: AnyFunction;
|
||||||
|
icon?: Component | Function | string;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标签文本
|
* 标签文本
|
||||||
@@ -206,11 +210,21 @@ if (enableShortcutKey.value) {
|
|||||||
<div
|
<div
|
||||||
v-if="tagText || text || $slots.tagText"
|
v-if="tagText || text || $slots.tagText"
|
||||||
class="text-foreground mb-1 flex items-center text-sm font-medium"
|
class="text-foreground mb-1 flex items-center text-sm font-medium"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="max-w-[100px] overflow-hidden text-ellipsis break-keep"
|
||||||
|
:title="text"
|
||||||
>
|
>
|
||||||
{{ text }}
|
{{ text }}
|
||||||
|
</div>
|
||||||
<slot name="tagText">
|
<slot name="tagText">
|
||||||
<Badge v-if="tagText" class="ml-2 text-green-400">
|
<Badge v-if="tagText" class="ml-2 text-green-400">
|
||||||
|
<div
|
||||||
|
class="max-w-[50px] overflow-hidden text-ellipsis"
|
||||||
|
:title="tagText"
|
||||||
|
>
|
||||||
{{ tagText }}
|
{{ tagText }}
|
||||||
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import type { ExtendedFormApi } from '@vben-core/form-ui';
|
|
||||||
import type { VxeGridInstance } from 'vxe-table';
|
import type { VxeGridInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { ExtendedFormApi } from '@vben-core/form-ui';
|
||||||
|
|
||||||
import type { VxeGridProps } from './types';
|
import type { VxeGridProps } from './types';
|
||||||
|
|
||||||
|
import { toRaw } from 'vue';
|
||||||
|
|
||||||
import { Store } from '@vben-core/shared/store';
|
import { Store } from '@vben-core/shared/store';
|
||||||
import {
|
import {
|
||||||
bindMethods,
|
bindMethods,
|
||||||
@@ -11,7 +14,6 @@ import {
|
|||||||
mergeWithArrayOverride,
|
mergeWithArrayOverride,
|
||||||
StateHandler,
|
StateHandler,
|
||||||
} from '@vben-core/shared/utils';
|
} from '@vben-core/shared/utils';
|
||||||
import { toRaw } from 'vue';
|
|
||||||
|
|
||||||
function getDefaultState(): VxeGridProps {
|
function getDefaultState(): VxeGridProps {
|
||||||
return {
|
return {
|
||||||
@@ -24,18 +26,18 @@ function getDefaultState(): VxeGridProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VxeGridApi {
|
export class VxeGridApi<T extends Record<string, any> = any> {
|
||||||
private isMounted = false;
|
|
||||||
|
|
||||||
private stateHandler: StateHandler;
|
|
||||||
public formApi = {} as ExtendedFormApi;
|
public formApi = {} as ExtendedFormApi;
|
||||||
|
|
||||||
// private prevState: null | VxeGridProps = null;
|
// private prevState: null | VxeGridProps = null;
|
||||||
public grid = {} as VxeGridInstance;
|
public grid = {} as VxeGridInstance<T>;
|
||||||
|
public state: null | VxeGridProps<T> = null;
|
||||||
|
|
||||||
public state: null | VxeGridProps = null;
|
public store: Store<VxeGridProps<T>>;
|
||||||
|
|
||||||
public store: Store<VxeGridProps>;
|
private isMounted = false;
|
||||||
|
|
||||||
|
private stateHandler: StateHandler;
|
||||||
|
|
||||||
constructor(options: VxeGridProps = {}) {
|
constructor(options: VxeGridProps = {}) {
|
||||||
const storeState = { ...options };
|
const storeState = { ...options };
|
||||||
@@ -97,8 +99,8 @@ export class VxeGridApi {
|
|||||||
|
|
||||||
setState(
|
setState(
|
||||||
stateOrFn:
|
stateOrFn:
|
||||||
| ((prev: VxeGridProps) => Partial<VxeGridProps>)
|
| ((prev: VxeGridProps<T>) => Partial<VxeGridProps<T>>)
|
||||||
| Partial<VxeGridProps>,
|
| Partial<VxeGridProps<T>>,
|
||||||
) {
|
) {
|
||||||
if (isFunction(stateOrFn)) {
|
if (isFunction(stateOrFn)) {
|
||||||
this.store.setState((prev) => {
|
this.store.setState((prev) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { Ref } from 'vue';
|
|||||||
|
|
||||||
import type { ClassType, DeepPartial } from '@vben/types';
|
import type { ClassType, DeepPartial } from '@vben/types';
|
||||||
|
|
||||||
import type { VbenFormProps } from '@vben-core/form-ui';
|
import type { BaseFormComponentType, VbenFormProps } from '@vben-core/form-ui';
|
||||||
|
|
||||||
import type { VxeGridApi } from './api';
|
import type { VxeGridApi } from './api';
|
||||||
|
|
||||||
@@ -35,7 +35,11 @@ export interface SeparatorOptions {
|
|||||||
show?: boolean;
|
show?: boolean;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
}
|
}
|
||||||
export interface VxeGridProps {
|
|
||||||
|
export interface VxeGridProps<
|
||||||
|
T extends Record<string, any> = any,
|
||||||
|
D extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
> {
|
||||||
/**
|
/**
|
||||||
* 标题
|
* 标题
|
||||||
*/
|
*/
|
||||||
@@ -55,15 +59,15 @@ export interface VxeGridProps {
|
|||||||
/**
|
/**
|
||||||
* vxe-grid 配置
|
* vxe-grid 配置
|
||||||
*/
|
*/
|
||||||
gridOptions?: DeepPartial<VxeTableGridOptions>;
|
gridOptions?: DeepPartial<VxeTableGridOptions<T>>;
|
||||||
/**
|
/**
|
||||||
* vxe-grid 事件
|
* vxe-grid 事件
|
||||||
*/
|
*/
|
||||||
gridEvents?: Partial<VxeGridListeners>;
|
gridEvents?: Partial<VxeGridListeners<T>>;
|
||||||
/**
|
/**
|
||||||
* 表单配置
|
* 表单配置
|
||||||
*/
|
*/
|
||||||
formOptions?: VbenFormProps;
|
formOptions?: VbenFormProps<D>;
|
||||||
/**
|
/**
|
||||||
* 显示搜索表单
|
* 显示搜索表单
|
||||||
*/
|
*/
|
||||||
@@ -74,9 +78,12 @@ export interface VxeGridProps {
|
|||||||
separator?: boolean | SeparatorOptions;
|
separator?: boolean | SeparatorOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedVxeGridApi = VxeGridApi & {
|
export type ExtendedVxeGridApi<
|
||||||
useStore: <T = NoInfer<VxeGridProps>>(
|
D extends Record<string, any> = any,
|
||||||
selector?: (state: NoInfer<VxeGridProps>) => T,
|
F extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
> = VxeGridApi<D> & {
|
||||||
|
useStore: <T = NoInfer<VxeGridProps<D, F>>>(
|
||||||
|
selector?: (state: NoInfer<VxeGridProps<any, any>>) => T,
|
||||||
) => Readonly<Ref<T>>;
|
) => Readonly<Ref<T>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { BaseFormComponentType } from '@vben-core/form-ui';
|
||||||
|
|
||||||
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
|
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
|
||||||
|
|
||||||
import { defineComponent, h, onBeforeUnmount } from 'vue';
|
import { defineComponent, h, onBeforeUnmount } from 'vue';
|
||||||
@@ -7,16 +9,19 @@ import { useStore } from '@vben-core/shared/store';
|
|||||||
import { VxeGridApi } from './api';
|
import { VxeGridApi } from './api';
|
||||||
import VxeGrid from './use-vxe-grid.vue';
|
import VxeGrid from './use-vxe-grid.vue';
|
||||||
|
|
||||||
export function useVbenVxeGrid(options: VxeGridProps) {
|
export function useVbenVxeGrid<
|
||||||
|
T extends Record<string, any> = any,
|
||||||
|
D extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
>(options: VxeGridProps<T, D>) {
|
||||||
// const IS_REACTIVE = isReactive(options);
|
// const IS_REACTIVE = isReactive(options);
|
||||||
const api = new VxeGridApi(options);
|
const api = new VxeGridApi(options);
|
||||||
const extendedApi: ExtendedVxeGridApi = api as ExtendedVxeGridApi;
|
const extendedApi: ExtendedVxeGridApi<T, D> = api as ExtendedVxeGridApi<T, D>;
|
||||||
extendedApi.useStore = (selector) => {
|
extendedApi.useStore = (selector) => {
|
||||||
return useStore(api.store, selector);
|
return useStore(api.store, selector);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Grid = defineComponent(
|
const Grid = defineComponent(
|
||||||
(props: VxeGridProps, { attrs, slots }) => {
|
(props: VxeGridProps<T>, { attrs, slots }) => {
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
api.unmount();
|
api.unmount();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -280,6 +280,15 @@ const delegatedFormSlots = computed(() => {
|
|||||||
return resultSlots.map((key) => key.replace(FORM_SLOT_PREFIX, ''));
|
return resultSlots.map((key) => key.replace(FORM_SLOT_PREFIX, ''));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showDefaultEmpty = computed(() => {
|
||||||
|
// 检查是否有原生的 VXE Table 空状态配置
|
||||||
|
const hasEmptyText = options.value.emptyText !== undefined;
|
||||||
|
const hasEmptyRender = options.value.emptyRender !== undefined;
|
||||||
|
|
||||||
|
// 如果有原生配置,就不显示默认的空状态
|
||||||
|
return !hasEmptyText && !hasEmptyRender;
|
||||||
|
});
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};
|
const globalGridConfig = VxeUI?.getConfig()?.grid ?? {};
|
||||||
@@ -464,7 +473,7 @@ onUnmounted(() => {
|
|||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<!-- 统一控状态 -->
|
<!-- 统一控状态 -->
|
||||||
<template #empty>
|
<template v-if="showDefaultEmpty" #empty>
|
||||||
<slot name="empty">
|
<slot name="empty">
|
||||||
<EmptyIcon class="mx-auto" />
|
<EmptyIcon class="mx-auto" />
|
||||||
<div class="mt-2">{{ $t('common.noData') }}</div>
|
<div class="mt-2">{{ $t('common.noData') }}</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/request",
|
"name": "@vben/request",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { RequestClient } from '../request-client';
|
import type { RequestClient } from '../request-client';
|
||||||
import type { RequestClientConfig } from '../types';
|
import type { RequestClientConfig } from '../types';
|
||||||
|
|
||||||
|
import { isUndefined } from '@vben/utils';
|
||||||
|
|
||||||
class FileUploader {
|
class FileUploader {
|
||||||
private client: RequestClient;
|
private client: RequestClient;
|
||||||
|
|
||||||
@@ -18,10 +20,10 @@ class FileUploader {
|
|||||||
Object.entries(data).forEach(([key, value]) => {
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach((item, index) => {
|
value.forEach((item, index) => {
|
||||||
formData.append(`${key}[${index}]`, item);
|
!isUndefined(item) && formData.append(`${key}[${index}]`, item);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
formData.append(key, value);
|
!isUndefined(value) && formData.append(key, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/icons",
|
"name": "@vben/icons",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/locales",
|
"name": "@vben/locales",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/preferences",
|
"name": "@vben/preferences",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/stores",
|
"name": "@vben/stores",
|
||||||
"version": "5.5.6",
|
"version": "5.5.7",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ interface BasicUserInfo {
|
|||||||
* 头像
|
* 头像
|
||||||
*/
|
*/
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
/**
|
||||||
|
* 邮箱
|
||||||
|
*/
|
||||||
|
email: string;
|
||||||
/**
|
/**
|
||||||
* 用户权限
|
* 用户权限
|
||||||
*/
|
*/
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user