mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-04-03 01:23:22 +08:00
Compare commits
135 Commits
1.3.5-back
...
1.4.1-back
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
2141c93399 | ||
|
|
906502f49b | ||
|
|
96a10ca83f | ||
|
|
1f68fd31b7 | ||
|
|
f31360ba4e | ||
|
|
4eb16d6d3a | ||
|
|
9db6ade1ed | ||
|
|
2569e1da0d | ||
|
|
2de9cd2334 | ||
|
|
739e04816a | ||
|
|
d9c57dfb61 | ||
|
|
2217c96cd9 | ||
|
|
752c1ac3ed | ||
|
|
53304514b6 | ||
|
|
cf913f8b8d | ||
|
|
ad9c465622 | ||
|
|
bea7c1a094 | ||
|
|
95859e36a2 | ||
|
|
8bfc482a7f | ||
|
|
6fbf1387f5 | ||
|
|
c3edbec3f0 | ||
|
|
e5c937396d | ||
|
|
45de9b7547 | ||
|
|
6daedd1de5 | ||
|
|
c45eed90d9 | ||
|
|
845719d951 | ||
|
|
4ef974ca4e | ||
|
|
af186f878d | ||
|
|
2dc7e564b2 | ||
|
|
97894a940e | ||
|
|
48d70182b4 | ||
|
|
a1091bad46 | ||
|
|
9f9be21e2a | ||
|
|
a2bdcd6e49 | ||
|
|
a38f2de982 | ||
|
|
d039c53053 | ||
|
|
11b2b5bcc2 | ||
|
|
ebef2c91e2 | ||
|
|
0c3edb10b0 | ||
|
|
801514dbe3 | ||
|
|
2dce7718d6 | ||
|
|
8a8e090792 | ||
|
|
6fc2c4e3cc | ||
|
|
8ac97688da | ||
|
|
c89ec0088b | ||
|
|
0a076f5e6e | ||
|
|
4fd68bc083 | ||
|
|
2efacb3e5b | ||
|
|
dae46abb71 | ||
|
|
5ee2a74e2d | ||
|
|
79d89005b6 | ||
|
|
d0b8349a2d | ||
|
|
34c4ecb047 | ||
|
|
3d9dba965f | ||
|
|
96b8ae94fd | ||
|
|
024c01d350 | ||
|
|
10b8b81954 | ||
|
|
2adb8acd80 | ||
|
|
a23bc4cb5c | ||
|
|
cf17a45d8d | ||
|
|
b46ebe756e | ||
|
|
1f50c95c66 | ||
|
|
cd4706b717 | ||
|
|
769aceb55f | ||
|
|
e89cf400c0 | ||
|
|
9e67929ee7 | ||
|
|
90625782c0 |
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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,3 +1,43 @@
|
|||||||
|
# 1.4.1
|
||||||
|
|
||||||
|
**FEATURES**
|
||||||
|
|
||||||
|
- Tinymce添加在antd原生表单/useVbenForm下的校验样式
|
||||||
|
- useVbenForm 增加 Cascader(级联选择器) 组件
|
||||||
|
|
||||||
|
**BUG FIX**
|
||||||
|
|
||||||
|
- 菜单管理 路由地址的必填项不生效
|
||||||
|
- withDefaultPlaceholder中placeholder 在keepalive & 语言切换 & tab切换 显示不变的问题
|
||||||
|
|
||||||
|
**REFACTOR**
|
||||||
|
|
||||||
|
- 字典接口抛出异常(为什么会抛出异常?)无限调用接口 兼容处理
|
||||||
|
- 代码生成 字典下拉加载 改为每次进入编辑页面都加载
|
||||||
|
- ~~个人中心 账号绑定 样式/逻辑重构~~(回滚了 既要又要的问题)
|
||||||
|
- ~~个人中心 下拉卡片 昵称超长省略显示~~(回滚了 既要又要的问题)
|
||||||
|
|
||||||
|
# 1.4.0
|
||||||
|
|
||||||
|
**FEATURES**
|
||||||
|
|
||||||
|
- 菜单管理(通用方法) 保存表格滚动/展开状态并执行回调 用于树表在执行 新增/编辑/删除等操作后 依然在当前位置(体验优化)
|
||||||
|
-
|
||||||
|
- 菜单管理 级联删除 删除菜单和children
|
||||||
|
|
||||||
|
**REFACTOR**
|
||||||
|
|
||||||
|
- 除个人中心外所有本地路由改为从后端返回(需要执行更新sql)
|
||||||
|
- 流程图预览改为logicflow预览而非图片 ...然后后端又更新了 又改成iframe了
|
||||||
|
- 菜单管理 新增角色校验(与后端权限保持一致) 只有superadmin可进行增删改
|
||||||
|
|
||||||
|
# 1.3.6
|
||||||
|
|
||||||
|
**BUG FIX**
|
||||||
|
|
||||||
|
- oss配置switch切换 导致报错`存储类型找不到`
|
||||||
|
- 文件上传无法正确清除(innerList)
|
||||||
|
|
||||||
# 1.3.5
|
# 1.3.5
|
||||||
|
|
||||||
**BUG FIX**
|
**BUG FIX**
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
|
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
|
||||||
|
|
||||||
目前对应后端版本: **分布式5.3.1/微服务2.3.0**
|
目前对应后端版本: **分布式5.4.0/微服务2.4.0**
|
||||||
|
|
||||||
V1.1.0版本已支持离线图标
|
V1.1.0版本已支持离线图标
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ V1.2.0版本对接warmflow工作流
|
|||||||
|
|
||||||
| 组件/框架 | 版本 |
|
| 组件/框架 | 版本 |
|
||||||
| :------------- | :----- |
|
| :------------- | :----- |
|
||||||
| vben | 5.5.4 |
|
| vben | 5.5.6 |
|
||||||
| ant-design-vue | 4.2.6 |
|
| ant-design-vue | 4.2.6 |
|
||||||
| vue | 3.5.13 |
|
| vue | 3.5.13 |
|
||||||
|
|
||||||
@@ -46,14 +46,6 @@ admin 账号: admin admin123
|
|||||||
|
|
||||||
[RuoYi-Plus 文档地址](https://plus-doc.dromara.org/#/)
|
[RuoYi-Plus 文档地址](https://plus-doc.dromara.org/#/)
|
||||||
|
|
||||||
## 关于表单
|
|
||||||
|
|
||||||
如果你觉得`useVbenForm`难度很大, 完全可以**使用原生antd表单**进行开发, 不一定非得用`useVbenForm`进行开发
|
|
||||||
|
|
||||||
`apps/web-antd/src/views/system/notice/notice-modal.vue`即`通知公告modal`使用**原生antd form**进行(反向🤔)重构, 不想用`useVbenForm`可参考该页面进行表单开发
|
|
||||||
|
|
||||||
复杂表单(如各种联动, 需要自定义样式布局, 需要自定义组件)**优先使用原生表单**(反正说了也没人听听😅)
|
|
||||||
|
|
||||||
## 预览图
|
## 预览图
|
||||||
|
|
||||||
        
|
        
|
||||||
|
|||||||
28
apps/backend-mock/api/demo/bigint.ts
Normal file
28
apps/backend-mock/api/demo/bigint.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export default eventHandler(async (event) => {
|
||||||
|
const userinfo = verifyAccessToken(event);
|
||||||
|
if (!userinfo) {
|
||||||
|
return unAuthorizedResponse(event);
|
||||||
|
}
|
||||||
|
const data = `
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 123456789012345678901234567890123456789012345678901234567890,
|
||||||
|
"name": "John Doe",
|
||||||
|
"age": 30,
|
||||||
|
"email": "john-doe@demo.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 987654321098765432109876543210987654321098765432109876543210,
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"age": 25,
|
||||||
|
"email": "jane@demo.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
setHeader(event, 'Content-Type', 'application/json');
|
||||||
|
return data;
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/web-antd",
|
"name": "@vben/web-antd",
|
||||||
"version": "1.3.5",
|
"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": {
|
||||||
|
|||||||
@@ -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切换 显示不变的问题
|
||||||
props?.placeholder ||
|
const computedPlaceholder = computed(
|
||||||
attrs?.placeholder ||
|
() =>
|
||||||
$t(`ui.placeholder.${type}`);
|
props?.placeholder ||
|
||||||
|
attrs?.placeholder ||
|
||||||
|
$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,
|
||||||
|
|||||||
@@ -10,44 +10,46 @@ import { $t } from '@vben/locales';
|
|||||||
|
|
||||||
import { isArray } from 'lodash-es';
|
import { isArray } from 'lodash-es';
|
||||||
|
|
||||||
setupVbenForm<ComponentType>({
|
async function initSetupVbenForm() {
|
||||||
config: {
|
setupVbenForm<ComponentType>({
|
||||||
// ant design vue组件库默认都是 v-model:value
|
config: {
|
||||||
baseModelPropName: 'value',
|
// ant design vue组件库默认都是 v-model:value
|
||||||
|
baseModelPropName: 'value',
|
||||||
|
|
||||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||||
modelPropNameMap: {
|
modelPropNameMap: {
|
||||||
Checkbox: 'checked',
|
Checkbox: 'checked',
|
||||||
Radio: 'checked',
|
Radio: 'checked',
|
||||||
RichTextarea: 'modelValue',
|
RichTextarea: 'modelValue',
|
||||||
Switch: 'checked',
|
Switch: 'checked',
|
||||||
Upload: 'fileList',
|
Upload: 'fileList',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
defineRules: {
|
||||||
defineRules: {
|
// 输入项目必填国际化适配
|
||||||
// 输入项目必填国际化适配
|
required: (value, _params, ctx) => {
|
||||||
required: (value, _params, ctx) => {
|
if (value === undefined || value === null || value.length === 0) {
|
||||||
if (value === undefined || value === null || value.length === 0) {
|
return $t('ui.formRules.required', [ctx.label]);
|
||||||
return $t('ui.formRules.required', [ctx.label]);
|
}
|
||||||
}
|
return true;
|
||||||
return true;
|
},
|
||||||
|
// 选择项目必填国际化适配
|
||||||
|
selectRequired: (value, _params, ctx) => {
|
||||||
|
if (
|
||||||
|
[false, null, undefined].includes(value) ||
|
||||||
|
(isArray(value) && value.length === 0)
|
||||||
|
) {
|
||||||
|
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// 选择项目必填国际化适配
|
});
|
||||||
selectRequired: (value, _params, ctx) => {
|
}
|
||||||
if (
|
|
||||||
[false, null, undefined].includes(value) ||
|
|
||||||
(isArray(value) && value.length === 0)
|
|
||||||
) {
|
|
||||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const useVbenForm = useForm<ComponentType>;
|
const useVbenForm = useForm<ComponentType>;
|
||||||
|
|
||||||
export { useVbenForm, z };
|
export { initSetupVbenForm, useVbenForm, z };
|
||||||
|
|
||||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||||
export type { VbenFormProps };
|
export type { VbenFormProps };
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { GrantType } from '@vben/common-ui';
|
import type { GrantType } from '@vben/common-ui';
|
||||||
|
import type { HttpResponse } from '@vben/request';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { useAppConfig } from '@vben/hooks';
|
import { useAppConfig } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
const { clientId, sseEnable } = useAppConfig(
|
const { clientId, sseEnable } = useAppConfig(
|
||||||
@@ -90,8 +95,32 @@ export async function loginApi(data: AuthApi.LoginParams) {
|
|||||||
* 用户登出
|
* 用户登出
|
||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
export function doLogout() {
|
export async function doLogout() {
|
||||||
return requestClient.post<void>('/auth/logout');
|
const resp = await requestClient.post<HttpResponse<void>>(
|
||||||
|
'/auth/logout',
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
isTransformResponse: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// 无奈之举 对错误用法的提示
|
||||||
|
if (resp.code === 401 && import.meta.env.DEV) {
|
||||||
|
Modal.destroyAll();
|
||||||
|
Modal.warn({
|
||||||
|
title: '后端配置出现错误',
|
||||||
|
centered: true,
|
||||||
|
content: h('div', { class: 'flex flex-col gap-2' }, [
|
||||||
|
`检测到你的logout接口返回了401, 导致前端一直进入循环逻辑???`,
|
||||||
|
...Array.from({ length: 3 }, () =>
|
||||||
|
h(
|
||||||
|
'span',
|
||||||
|
{ class: 'font-bold text-red-500 text-[18px]' },
|
||||||
|
'去检查你的后端配置!别盯着前端找问题了!这不是前端问题!',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -81,3 +81,12 @@ export function tenantPackageMenuTreeSelect(packageId: ID) {
|
|||||||
`${Api.tenantPackageMenuTreeselect}/${packageId}`,
|
`${Api.tenantPackageMenuTreeselect}/${packageId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除菜单
|
||||||
|
* @param menuIds 菜单ids
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function menuCascadeRemove(menuIds: IDS) {
|
||||||
|
return requestClient.deleteWithMsg<void>(`${Api.root}/cascade/${menuIds}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ export function ossConfigRemove(ossConfigIds: IDS) {
|
|||||||
|
|
||||||
// 更改OSS配置的状态
|
// 更改OSS配置的状态
|
||||||
export function ossConfigChangeStatus(data: any) {
|
export function ossConfigChangeStatus(data: any) {
|
||||||
const requestData = {
|
const requestData: Partial<OssConfig> = {
|
||||||
ossConfigId: data.ossConfigId,
|
ossConfigId: data.ossConfigId,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
|
configKey: data.configKey,
|
||||||
};
|
};
|
||||||
return requestClient.putWithMsg(Api.ossConfigChangeStatus, requestData);
|
return requestClient.putWithMsg(Api.ossConfigChangeStatus, requestData);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +36,6 @@ export interface Flow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FlowInfoResponse {
|
export interface FlowInfoResponse {
|
||||||
image: string;
|
instanceId: string;
|
||||||
list: Flow[];
|
list: Flow[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { App, ConfigProvider, theme } from 'ant-design-vue';
|
|||||||
|
|
||||||
import { antdLocale } from '#/locales';
|
import { antdLocale } from '#/locales';
|
||||||
|
|
||||||
|
import { useUploadTip } from './upload-tip';
|
||||||
|
|
||||||
defineOptions({ name: 'App' });
|
defineOptions({ name: 'App' });
|
||||||
|
|
||||||
const { isDark } = usePreferences();
|
const { isDark } = usePreferences();
|
||||||
@@ -28,6 +30,8 @@ const tokenTheme = computed(() => {
|
|||||||
token: tokens,
|
token: tokens,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useUploadTip();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { setupGlobalComponent } from '#/components/global';
|
|||||||
import { $t, setupI18n } from '#/locales';
|
import { $t, setupI18n } from '#/locales';
|
||||||
|
|
||||||
import { initComponentAdapter } from './adapter/component';
|
import { initComponentAdapter } from './adapter/component';
|
||||||
|
import { initSetupVbenForm } from './adapter/form';
|
||||||
import App from './app.vue';
|
import App from './app.vue';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
|
|
||||||
@@ -20,6 +21,9 @@ async function bootstrap(namespace: string) {
|
|||||||
// 初始化组件适配器
|
// 初始化组件适配器
|
||||||
await initComponentAdapter();
|
await initComponentAdapter();
|
||||||
|
|
||||||
|
// 初始化表单组件
|
||||||
|
await initSetupVbenForm();
|
||||||
|
|
||||||
// // 设置弹窗的默认配置
|
// // 设置弹窗的默认配置
|
||||||
// setDefaultModalProps({
|
// setDefaultModalProps({
|
||||||
// fullscreenButton: false,
|
// fullscreenButton: false,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -322,6 +322,8 @@ export function useUpload(
|
|||||||
() => bindValue.value,
|
() => bindValue.value,
|
||||||
async (value) => {
|
async (value) => {
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
|
// 清空绑定值时,同时清空innerFileList,避免外部使用时还能读取到
|
||||||
|
innerFileList.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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模式 需要改动的地方太多
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ComponentRecordType,
|
ComponentRecordType,
|
||||||
GenerateMenuAndRoutesOptions,
|
GenerateMenuAndRoutesOptions,
|
||||||
|
RouteMeta,
|
||||||
RouteRecordStringComponent,
|
RouteRecordStringComponent,
|
||||||
} from '@vben/types';
|
} from '@vben/types';
|
||||||
|
|
||||||
@@ -21,6 +22,37 @@ import { localMenuList } from './routes/local';
|
|||||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||||
const NotFoundComponent = () => import('#/views/_core/fallback/not-found.vue');
|
const NotFoundComponent = () => import('#/views/_core/fallback/not-found.vue');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后端返回的meta有时候不包括需要的信息 比如activePath等
|
||||||
|
* 在这里定义映射
|
||||||
|
*/
|
||||||
|
const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
|
||||||
|
'/system/role-auth/user/:roleId': {
|
||||||
|
activePath: '/system/role',
|
||||||
|
requireHomeRedirect: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
'/system/oss-config/index': {
|
||||||
|
activePath: '/system/oss',
|
||||||
|
requireHomeRedirect: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
'/tool/gen-edit/index/:tableId': {
|
||||||
|
activePath: '/tool/gen',
|
||||||
|
requireHomeRedirect: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
'/workflow/design/index': {
|
||||||
|
activePath: '/workflow/processDefinition',
|
||||||
|
requireHomeRedirect: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
'/workflow/leaveEdit/index': {
|
||||||
|
activePath: '/demo/leave',
|
||||||
|
requireHomeRedirect: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台路由转vben路由
|
* 后台路由转vben路由
|
||||||
* @param menuList 后台菜单
|
* @param menuList 后台菜单
|
||||||
@@ -98,6 +130,17 @@ function backMenuToVbenMenu(
|
|||||||
path: menu.path,
|
path: menu.path,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理meta映射
|
||||||
|
if (Object.keys(routeMetaMapping).includes(vbenRoute.path)) {
|
||||||
|
const routeMeta = routeMetaMapping[vbenRoute.path];
|
||||||
|
if (routeMeta) {
|
||||||
|
vbenRoute.meta = {
|
||||||
|
...vbenRoute.meta,
|
||||||
|
...(routeMeta as RouteMeta),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 添加路由参数信息
|
// 添加路由参数信息
|
||||||
if (menu.query) {
|
if (menu.query) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
|
|||||||
// 记录已经加载的页面
|
// 记录已经加载的页面
|
||||||
const loadedPaths = new Set<string>();
|
const loadedPaths = new Set<string>();
|
||||||
|
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach((to) => {
|
||||||
to.meta.loaded = loadedPaths.has(to.path);
|
to.meta.loaded = loadedPaths.has(to.path);
|
||||||
|
|
||||||
// 页面加载进度条
|
// 页面加载进度条
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面
|
* 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面
|
||||||
|
* 也可以直接在菜单管理配置
|
||||||
*/
|
*/
|
||||||
const localRoutes: RouteRecordStringComponent[] = [
|
const localRoutes: RouteRecordStringComponent[] = [
|
||||||
{
|
{
|
||||||
@@ -22,69 +23,6 @@ const localRoutes: RouteRecordStringComponent[] = [
|
|||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: '/system/oss-config/index',
|
|
||||||
meta: {
|
|
||||||
activePath: '/system/oss',
|
|
||||||
icon: 'ant-design:setting-outlined',
|
|
||||||
title: 'oss配置',
|
|
||||||
hideInMenu: true,
|
|
||||||
requireHomeRedirect: true,
|
|
||||||
},
|
|
||||||
name: 'OssConfig',
|
|
||||||
path: '/system/oss-config',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: '/tool/gen/edit-gen',
|
|
||||||
meta: {
|
|
||||||
activePath: '/tool/gen',
|
|
||||||
icon: 'tabler:code',
|
|
||||||
title: '生成配置',
|
|
||||||
hideInMenu: true,
|
|
||||||
requireHomeRedirect: true,
|
|
||||||
},
|
|
||||||
name: 'GenConfig',
|
|
||||||
path: '/code-gen/edit/:tableId',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: '/system/role-assign/index',
|
|
||||||
meta: {
|
|
||||||
activePath: '/system/role',
|
|
||||||
icon: 'eos-icons:role-binding-outlined',
|
|
||||||
title: '分配角色',
|
|
||||||
hideInMenu: true,
|
|
||||||
requireHomeRedirect: true,
|
|
||||||
},
|
|
||||||
name: 'RoleAssign',
|
|
||||||
path: '/system/role-assign/:roleId',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: '/workflow/components/flow-designer',
|
|
||||||
meta: {
|
|
||||||
activePath: '/workflow/processDefinition',
|
|
||||||
icon: 'fluent-mdl2:flow',
|
|
||||||
title: '流程设计',
|
|
||||||
hideInMenu: true,
|
|
||||||
requireHomeRedirect: true,
|
|
||||||
},
|
|
||||||
name: 'WorkflowDesigner',
|
|
||||||
path: '/workflow/designer',
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 需要添加iframe路由 同目录的./workflow-iframe.ts
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
component: 'workflow/leave/leave-form',
|
|
||||||
meta: {
|
|
||||||
icon: 'flat-color-icons:leave',
|
|
||||||
title: '请假申请',
|
|
||||||
activePath: '/demo/leave',
|
|
||||||
hideInMenu: true,
|
|
||||||
requireHomeRedirect: true,
|
|
||||||
},
|
|
||||||
name: 'WorkflowLeaveIndex',
|
|
||||||
path: '/workflow/leaveEdit/index',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
/**
|
/**
|
||||||
|
|||||||
36
apps/web-antd/src/upload-tip.ts
Normal file
36
apps/web-antd/src/upload-tip.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
|
import { Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
|
export function useUploadTip() {
|
||||||
|
const readTip = useLocalStorage<boolean>('__upload_tip_read_5.4.0', false);
|
||||||
|
onMounted(() => {
|
||||||
|
if (readTip.value || !import.meta.env.DEV) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modalInstance = Modal.info({
|
||||||
|
title: '提示',
|
||||||
|
centered: true,
|
||||||
|
content:
|
||||||
|
'如果你的版本是从低版本升级到后端>5.4.0, 记得执行升级sql, 否则跳转页面(如oss 代码生成配置)等会404',
|
||||||
|
okButtonProps: { disabled: true },
|
||||||
|
onOk() {
|
||||||
|
modalInstance.destroy();
|
||||||
|
readTip.value = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let time = 3;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
modalInstance.update({
|
||||||
|
okText: time === 0 ? '我知道了, 不再弹出' : `${time}秒后关闭`,
|
||||||
|
okButtonProps: { disabled: time > 0 },
|
||||||
|
});
|
||||||
|
if (time <= 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
time--;
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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时 移除字典缓存 下次登录重新获取
|
/**
|
||||||
dictRequestCache.delete(dictName);
|
* 需要判断是否为401抛出的特定异常 401清除缓存
|
||||||
|
* 其他error清除缓存会导致无限循环调用字典接口 则不做处理
|
||||||
|
*/
|
||||||
|
if (error instanceof UnauthorizedException) {
|
||||||
|
// 401时 移除字典缓存 下次登录重新获取
|
||||||
|
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,174 +1,142 @@
|
|||||||
<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[]>([]);
|
||||||
* 已经绑定的平台
|
|
||||||
*/
|
async function loadData() {
|
||||||
const boundPlatformsList = ref<string[]>([]);
|
const resp = await socialList();
|
||||||
const bindList = computed<BindItem[]>(() => {
|
|
||||||
const list = [...accountBindList];
|
const list: BindItemWithInfo[] = [...accountBindList];
|
||||||
list.forEach((item) => {
|
list.forEach((item) => {
|
||||||
item.bound = !!unref(boundPlatformsList).includes(item.source);
|
/**
|
||||||
|
* 平台转小写
|
||||||
|
*/
|
||||||
|
item.bound = resp
|
||||||
|
.map((social) => social.source.toLowerCase())
|
||||||
|
.includes(item.source.toLowerCase());
|
||||||
|
/**
|
||||||
|
* 添加info信息
|
||||||
|
*/
|
||||||
|
if (item.bound) {
|
||||||
|
item.info = resp.find(
|
||||||
|
(social) => social.source.toLowerCase() === item.source,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return list;
|
bindList.value = list;
|
||||||
});
|
}
|
||||||
|
onMounted(loadData);
|
||||||
const gridOptions: VxeGridProps = {
|
|
||||||
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();
|
|
||||||
/**
|
|
||||||
* 平台转小写
|
|
||||||
* 已经绑定的平台
|
|
||||||
*/
|
|
||||||
boundPlatformsList.value = resp.map((item) =>
|
|
||||||
item.source.toLowerCase(),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
rows: resp,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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 }">
|
<div class="flex w-full items-center gap-4">
|
||||||
<ListItem>
|
<component
|
||||||
<Card>
|
:is="item.avatar"
|
||||||
<div class="flex w-full items-center gap-4">
|
v-if="item.avatar"
|
||||||
<component
|
:style="item?.style ?? {}"
|
||||||
:is="item.avatar"
|
class="size-[40px]"
|
||||||
v-if="item.avatar"
|
/>
|
||||||
:style="item?.style ?? {}"
|
<div class="flex flex-1 items-center justify-between">
|
||||||
class="size-[40px]"
|
<div class="flex flex-col">
|
||||||
/>
|
<h4 class="mb-[4px] text-[14px] text-black/85 dark:text-white/85">
|
||||||
<div class="flex flex-1 items-center justify-between">
|
{{ item.title }}
|
||||||
<div class="flex flex-col">
|
</h4>
|
||||||
<h4
|
<span class="text-black/45 dark:text-white/45">
|
||||||
class="mb-[4px] text-[14px] text-black/85 dark:text-white/85"
|
<template v-if="!item.bound">
|
||||||
>
|
{{ item.description }}
|
||||||
{{ item.title }}
|
</template>
|
||||||
</h4>
|
<template v-if="item.bound && item.info">
|
||||||
<span class="text-black/45 dark:text-white/45">
|
<Tooltip>
|
||||||
{{ item.description }}
|
<template #title>
|
||||||
</span>
|
<div class="flex flex-col items-center gap-2 p-2">
|
||||||
</div>
|
<Avatar :size="36" :src="item.info.avatar" />
|
||||||
<a-button
|
<div>绑定时间: {{ item.info.createTime }}</div>
|
||||||
:disabled="item.bound"
|
</div>
|
||||||
size="small"
|
</template>
|
||||||
type="link"
|
<div class="cursor-pointer">
|
||||||
@click="handleAuthBinding(item.source)"
|
已绑定: {{ item.info.nickName }}
|
||||||
>
|
</div>
|
||||||
{{ buttonText(item) }}
|
</Tooltip>
|
||||||
</a-button>
|
</template>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<!-- TODO: 这里有优化空间? -->
|
||||||
</ListItem>
|
<a-button
|
||||||
</template>
|
size="small"
|
||||||
</List>
|
:type="item.bound ? 'default' : 'link'"
|
||||||
<Alert message="说明" type="info">
|
@click="
|
||||||
<template #description>
|
item.bound ? handleUnbind(item) : handleAuthBinding(item.source)
|
||||||
<p>
|
"
|
||||||
需要添加第三方账号在
|
>
|
||||||
<span class="font-bold">
|
{{ item.bound ? '取消绑定' : '绑定' }}
|
||||||
apps\web-antd\src\views\_core\oauth-common.ts
|
</a-button>
|
||||||
</span>
|
</div>
|
||||||
中accountBindList按模板添加
|
</div>
|
||||||
</p>
|
</Card>
|
||||||
</template>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
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">
|
||||||
|
<template #description>
|
||||||
|
<p>
|
||||||
|
需要添加第三方账号在
|
||||||
|
<span class="font-bold">
|
||||||
|
apps\web-antd\src\views\_core\oauth-common.ts
|
||||||
|
</span>
|
||||||
|
中accountBindList按模板添加
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export const columns: VxeGridProps['columns'] = [
|
|||||||
field: 'deptName',
|
field: 'deptName',
|
||||||
title: '部门名称',
|
title: '部门名称',
|
||||||
treeNode: true,
|
treeNode: true,
|
||||||
width: 200,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'deptCategory',
|
field: 'deptCategory',
|
||||||
@@ -39,13 +38,9 @@ export const columns: VxeGridProps['columns'] = [
|
|||||||
{
|
{
|
||||||
field: 'orderNum',
|
field: 'orderNum',
|
||||||
title: '排序',
|
title: '排序',
|
||||||
resizable: false,
|
|
||||||
width: 'auto',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'status',
|
||||||
resizable: false,
|
|
||||||
width: 'auto',
|
|
||||||
title: '状态',
|
title: '状态',
|
||||||
slots: {
|
slots: {
|
||||||
default: ({ row }) => {
|
default: ({ row }) => {
|
||||||
@@ -62,7 +57,8 @@ export const columns: VxeGridProps['columns'] = [
|
|||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
slots: { default: 'action' },
|
slots: { default: 'action' },
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 200,
|
resizable: false,
|
||||||
|
width: 'auto',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -99,6 +95,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||||||
fieldName: 'orderNum',
|
fieldName: 'orderNum',
|
||||||
label: '显示排序',
|
label: '显示排序',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
|||||||
6
apps/web-antd/src/views/system/dict/data.vue
Normal file
6
apps/web-antd/src/views/system/dict/data.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
ele版本会使用这个文件 只是为了不报错`未找到对应组件`才新建的这个文件
|
||||||
|
无实际意义
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -99,6 +99,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||||||
fieldName: 'dictSort',
|
fieldName: 'dictSort',
|
||||||
label: '显示排序',
|
label: '显示排序',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Textarea',
|
component: 'Textarea',
|
||||||
|
|||||||
281
apps/web-antd/src/views/system/dict/type/index-refactor.vue
Normal file
281
apps/web-antd/src/views/system/dict/type/index-refactor.vue
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<!-- 使用vxe实现成本最小 且自带虚拟滚动 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
import type { DictType } from '#/api/system/dict/dict-type-model';
|
||||||
|
|
||||||
|
import { h, ref, shallowRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { cn } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
ExportOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Popconfirm,
|
||||||
|
Space,
|
||||||
|
Tooltip,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import {
|
||||||
|
dictTypeExport,
|
||||||
|
dictTypeList,
|
||||||
|
dictTypeRemove,
|
||||||
|
refreshDictTypeCache,
|
||||||
|
} from '#/api/system/dict/dict-type';
|
||||||
|
import { commonDownloadExcel } from '#/utils/file/download';
|
||||||
|
|
||||||
|
import { emitter } from '../mitt';
|
||||||
|
import dictTypeModal from './dict-type-modal.vue';
|
||||||
|
|
||||||
|
const tableAllData = shallowRef<DictType[]>([]);
|
||||||
|
const gridOptions: VxeGridProps = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
title: 'name',
|
||||||
|
field: 'render',
|
||||||
|
slots: { default: 'render' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async () => {
|
||||||
|
const resp = await dictTypeList();
|
||||||
|
|
||||||
|
total.value = resp.total;
|
||||||
|
tableAllData.value = resp.rows;
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'dictId',
|
||||||
|
// 高亮当前行
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
cellConfig: {
|
||||||
|
height: 60,
|
||||||
|
},
|
||||||
|
showHeader: false,
|
||||||
|
toolbarConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
// 开启虚拟滚动
|
||||||
|
scrollY: {
|
||||||
|
enabled: false,
|
||||||
|
gt: 0,
|
||||||
|
},
|
||||||
|
rowClassName: 'cursor-pointer',
|
||||||
|
id: 'system-dict-data-index',
|
||||||
|
};
|
||||||
|
|
||||||
|
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||||
|
gridOptions,
|
||||||
|
gridEvents: {
|
||||||
|
cellClick: ({ row }) => {
|
||||||
|
handleRowClick(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DictTypeModal, modalApi] = useVbenModal({
|
||||||
|
connectedComponent: dictTypeModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleAdd() {
|
||||||
|
modalApi.setData({});
|
||||||
|
modalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEdit(record: DictType) {
|
||||||
|
modalApi.setData({ id: record.dictId });
|
||||||
|
modalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row: DictType) {
|
||||||
|
await dictTypeRemove([row.dictId]);
|
||||||
|
await tableApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReset() {
|
||||||
|
currentRowId.value = '';
|
||||||
|
searchValue.value = '';
|
||||||
|
await tableApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownloadExcel() {
|
||||||
|
commonDownloadExcel(dictTypeExport, '字典类型数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRefreshCache() {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '确认刷新字典类型缓存吗?',
|
||||||
|
okButtonProps: {
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
onOk: async () => {
|
||||||
|
await refreshDictTypeCache();
|
||||||
|
await tableApi.query();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastDictType = ref<string>('');
|
||||||
|
const currentRowId = ref<null | number | string>(null);
|
||||||
|
function handleRowClick(row: DictType) {
|
||||||
|
if (lastDictType.value === row.dictType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentRowId.value = row.dictId;
|
||||||
|
emitter.emit('rowClick', row.dictType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchValue = ref('');
|
||||||
|
const total = ref(0);
|
||||||
|
watch(searchValue, (value) => {
|
||||||
|
if (!tableApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
const names = tableAllData.value.filter((item) =>
|
||||||
|
item.dictName.includes(searchValue.value),
|
||||||
|
);
|
||||||
|
const types = tableAllData.value.filter((item) =>
|
||||||
|
item.dictType.includes(searchValue.value),
|
||||||
|
);
|
||||||
|
const filtered = [...new Set([...names, ...types])];
|
||||||
|
total.value = filtered.length;
|
||||||
|
tableApi.grid.loadData(filtered);
|
||||||
|
} else {
|
||||||
|
total.value = tableAllData.value.length;
|
||||||
|
tableApi.grid.loadData(tableAllData.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'bg-background flex max-h-[100vh] w-[360px] flex-col overflow-y-hidden',
|
||||||
|
'rounded-lg',
|
||||||
|
'dict-type-card',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div :class="cn('flex items-center justify-between', 'border-b px-4 py-2')">
|
||||||
|
<span class="font-semibold">字典项列表</span>
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="刷新缓存">
|
||||||
|
<a-button
|
||||||
|
v-access:code="['system:dict:edit']"
|
||||||
|
:icon="h(SyncOutlined)"
|
||||||
|
@click="handleRefreshCache"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :title="$t('pages.common.export')">
|
||||||
|
<a-button
|
||||||
|
v-access:code="['system:dict:export']"
|
||||||
|
:icon="h(ExportOutlined)"
|
||||||
|
@click="handleDownloadExcel"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :title="$t('pages.common.add')">
|
||||||
|
<a-button
|
||||||
|
v-access:code="['system:dict:add']"
|
||||||
|
:icon="h(PlusOutlined)"
|
||||||
|
@click="handleAdd"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 flex-col overflow-y-hidden p-4">
|
||||||
|
<Alert
|
||||||
|
class="mb-4"
|
||||||
|
show-icon
|
||||||
|
message="如果你的数据量大 自行开启虚拟滚动"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索字典项名称/类型"
|
||||||
|
v-model:value="searchValue"
|
||||||
|
allow-clear
|
||||||
|
>
|
||||||
|
<template #addonAfter>
|
||||||
|
<Tooltip title="重置/刷新">
|
||||||
|
<SyncOutlined
|
||||||
|
v-access:code="['system:dict:edit']"
|
||||||
|
@click="handleReset"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
</Input>
|
||||||
|
<BasicTable class="flex-1 overflow-hidden">
|
||||||
|
<template #render="{ row: item }">
|
||||||
|
<div :class="cn('flex items-center justify-between px-2 py-2')">
|
||||||
|
<div class="flex flex-col items-baseline overflow-hidden">
|
||||||
|
<span class="font-medium">{{ item.dictName }}</span>
|
||||||
|
<div
|
||||||
|
class="max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{{ item.dictType }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 text-[17px]">
|
||||||
|
<EditOutlined
|
||||||
|
class="text-primary"
|
||||||
|
v-access:code="['system:dict:edit']"
|
||||||
|
@click.stop="handleEdit(item)"
|
||||||
|
/>
|
||||||
|
<Popconfirm
|
||||||
|
placement="left"
|
||||||
|
:title="`确认删除 [${item.dictName}]?`"
|
||||||
|
@confirm="handleDelete(item)"
|
||||||
|
>
|
||||||
|
<DeleteOutlined
|
||||||
|
v-access:code="['system:dict:remove']"
|
||||||
|
class="text-destructive"
|
||||||
|
@click.stop=""
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BasicTable>
|
||||||
|
</div>
|
||||||
|
<div class="border-t px-4 py-3">共 {{ total }} 条数据</div>
|
||||||
|
<DictTypeModal @reload="tableApi.query()" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.dict-type-card {
|
||||||
|
.vxe-grid {
|
||||||
|
padding: 12px 0 0;
|
||||||
|
|
||||||
|
.vxe-body--row {
|
||||||
|
&.row--current {
|
||||||
|
// 选中行背景色
|
||||||
|
background-color: hsl(var(--accent-hover)) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-alert {
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -219,6 +219,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||||||
fieldName: 'orderNum',
|
fieldName: 'orderNum',
|
||||||
help: '排序, 数字越小越靠前',
|
help: '排序, 数字越小越靠前',
|
||||||
label: '显示排序',
|
label: '显示排序',
|
||||||
|
defaultValue: 0,
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -237,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: '路由地址不需要带/',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import type { VbenFormProps } from '@vben/common-ui';
|
|||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
import type { Menu } from '#/api/system/menu/model';
|
import type { Menu } from '#/api/system/menu/model';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { useAccess } from '@vben/access';
|
import { useAccess } from '@vben/access';
|
||||||
import { Fallback, Page, useVbenDrawer } from '@vben/common-ui';
|
import { Fallback, Page, useVbenDrawer } from '@vben/common-ui';
|
||||||
import { eachTree, getVxePopupContainer } from '@vben/utils';
|
import { $t } from '@vben/locales';
|
||||||
|
import { eachTree, getVxePopupContainer, treeToList } from '@vben/utils';
|
||||||
|
|
||||||
import { Popconfirm, Space } from 'ant-design-vue';
|
import { Popconfirm, Space, Switch, Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { 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 menuDrawer from './menu-drawer.vue';
|
import menuDrawer from './menu-drawer.vue';
|
||||||
@@ -67,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',
|
||||||
};
|
};
|
||||||
@@ -111,11 +114,41 @@ async function handleEdit(record: Menu) {
|
|||||||
drawerApi.open();
|
drawerApi.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否级联删除
|
||||||
|
*/
|
||||||
|
const cascadingDeletion = ref(false);
|
||||||
async function handleDelete(row: Menu) {
|
async function handleDelete(row: Menu) {
|
||||||
await menuRemove([row.menuId]);
|
if (cascadingDeletion.value) {
|
||||||
|
// 级联删除
|
||||||
|
const menuAndChildren: Menu[] = treeToList([row], { id: 'menuId' });
|
||||||
|
await menuCascadeRemove(menuAndChildren.map((item) => item.menuId));
|
||||||
|
} else {
|
||||||
|
// 单删除
|
||||||
|
await menuRemove([row.menuId]);
|
||||||
|
}
|
||||||
await tableApi.query();
|
await tableApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeConfirmTitle(row: Menu) {
|
||||||
|
const menuName = $t(row.menuName);
|
||||||
|
if (!cascadingDeletion.value) {
|
||||||
|
return `是否确认删除 [${menuName}] ?`;
|
||||||
|
}
|
||||||
|
const menuAndChildren = treeToList([row], { id: 'menuId' });
|
||||||
|
if (menuAndChildren.length === 1) {
|
||||||
|
return `是否确认删除 [${menuName}] ?`;
|
||||||
|
}
|
||||||
|
return `是否确认删除 [${menuName}] 及 [${menuAndChildren.length - 1}]个子项目 ?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑/添加成功后刷新表格
|
||||||
|
*/
|
||||||
|
async function afterEditOrAdd() {
|
||||||
|
tableApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全部展开/折叠
|
* 全部展开/折叠
|
||||||
* @param expand 是否展开
|
* @param expand 是否展开
|
||||||
@@ -128,6 +161,9 @@ function setExpandOrCollapse(expand: boolean) {
|
|||||||
/**
|
/**
|
||||||
* 与后台逻辑相同
|
* 与后台逻辑相同
|
||||||
* 只有租户管理和超级管理能访问菜单管理
|
* 只有租户管理和超级管理能访问菜单管理
|
||||||
|
* 注意: 只有超管才能对菜单进行`增删改`操作
|
||||||
|
* 注意: 只有超管才能对菜单进行`增删改`操作
|
||||||
|
* 注意: 只有超管才能对菜单进行`增删改`操作
|
||||||
*/
|
*/
|
||||||
const { hasAccessByRoles } = useAccess();
|
const { hasAccessByRoles } = useAccess();
|
||||||
const isAdmin = computed(() => {
|
const isAdmin = computed(() => {
|
||||||
@@ -140,6 +176,16 @@ const isAdmin = computed(() => {
|
|||||||
<BasicTable table-title="菜单列表" table-title-help="双击展开/收起子菜单">
|
<BasicTable table-title="菜单列表" table-title-help="双击展开/收起子菜单">
|
||||||
<template #toolbar-tools>
|
<template #toolbar-tools>
|
||||||
<Space>
|
<Space>
|
||||||
|
<Tooltip title="删除菜单以及子菜单">
|
||||||
|
<div
|
||||||
|
v-access:role="['superadmin']"
|
||||||
|
v-access:code="['system:menu:remove']"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<span class="mr-2 text-sm text-[#666666]">级联删除</span>
|
||||||
|
<Switch v-model:checked="cascadingDeletion" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
<a-button @click="setExpandOrCollapse(false)">
|
<a-button @click="setExpandOrCollapse(false)">
|
||||||
{{ $t('pages.common.collapse') }}
|
{{ $t('pages.common.collapse') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -149,6 +195,7 @@ const isAdmin = computed(() => {
|
|||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
v-access:code="['system:menu:add']"
|
v-access:code="['system:menu:add']"
|
||||||
|
v-access:role="['superadmin']"
|
||||||
@click="handleAdd"
|
@click="handleAdd"
|
||||||
>
|
>
|
||||||
{{ $t('pages.common.add') }}
|
{{ $t('pages.common.add') }}
|
||||||
@@ -159,6 +206,7 @@ const isAdmin = computed(() => {
|
|||||||
<Space>
|
<Space>
|
||||||
<ghost-button
|
<ghost-button
|
||||||
v-access:code="['system:menu:edit']"
|
v-access:code="['system:menu:edit']"
|
||||||
|
v-access:role="['superadmin']"
|
||||||
@click="handleEdit(row)"
|
@click="handleEdit(row)"
|
||||||
>
|
>
|
||||||
{{ $t('pages.common.edit') }}
|
{{ $t('pages.common.edit') }}
|
||||||
@@ -168,6 +216,7 @@ const isAdmin = computed(() => {
|
|||||||
v-if="row.menuType !== 'F'"
|
v-if="row.menuType !== 'F'"
|
||||||
class="btn-success"
|
class="btn-success"
|
||||||
v-access:code="['system:menu:add']"
|
v-access:code="['system:menu:add']"
|
||||||
|
v-access:role="['superadmin']"
|
||||||
@click="handleSubAdd(row)"
|
@click="handleSubAdd(row)"
|
||||||
>
|
>
|
||||||
{{ $t('pages.common.add') }}
|
{{ $t('pages.common.add') }}
|
||||||
@@ -175,12 +224,13 @@ const isAdmin = computed(() => {
|
|||||||
<Popconfirm
|
<Popconfirm
|
||||||
:get-popup-container="getVxePopupContainer"
|
:get-popup-container="getVxePopupContainer"
|
||||||
placement="left"
|
placement="left"
|
||||||
title="确认删除?"
|
:title="removeConfirmTitle(row)"
|
||||||
@confirm="handleDelete(row)"
|
@confirm="handleDelete(row)"
|
||||||
>
|
>
|
||||||
<ghost-button
|
<ghost-button
|
||||||
danger
|
danger
|
||||||
v-access:code="['system:menu:remove']"
|
v-access:code="['system:menu:remove']"
|
||||||
|
v-access:role="['superadmin']"
|
||||||
@click.stop=""
|
@click.stop=""
|
||||||
>
|
>
|
||||||
{{ $t('pages.common.delete') }}
|
{{ $t('pages.common.delete') }}
|
||||||
@@ -189,7 +239,7 @@ const isAdmin = computed(() => {
|
|||||||
</Space>
|
</Space>
|
||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
<MenuDrawer @reload="tableApi.query()" />
|
<MenuDrawer @reload="afterEditOrAdd" />
|
||||||
</Page>
|
</Page>
|
||||||
<Fallback v-else description="您没有菜单管理的访问权限" status="403" />
|
<Fallback v-else description="您没有菜单管理的访问权限" status="403" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ export const querySchema: FormSchemaGetter = () => [
|
|||||||
{
|
{
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
options: getDictOptions(DictEnum.SYS_YES_NO),
|
options: [
|
||||||
|
{ label: '是', value: '0' },
|
||||||
|
{ label: '否', value: '1' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
fieldName: 'status',
|
fieldName: 'status',
|
||||||
label: '是否默认',
|
label: '是否默认',
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ const { hasAccessByCodes } = useAccess();
|
|||||||
v-model:value="row.status"
|
v-model:value="row.status"
|
||||||
:api="() => ossConfigChangeStatus(row)"
|
:api="() => ossConfigChangeStatus(row)"
|
||||||
:disabled="!hasAccessByCodes(['system:ossConfig:edit'])"
|
:disabled="!hasAccessByCodes(['system:ossConfig:edit'])"
|
||||||
|
checked-text="是"
|
||||||
|
un-checked-text="否"
|
||||||
@reload="tableApi.query()"
|
@reload="tableApi.query()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
11
apps/web-antd/src/views/system/oss/config.vue
Normal file
11
apps/web-antd/src/views/system/oss/config.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!--
|
||||||
|
后端版本>=5.4.0 这个从本地路由变为从后台返回
|
||||||
|
未修改文件名 而是新加了这个文件
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import OssConfigPage from '#/views/system/oss-config/index.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<OssConfigPage />
|
||||||
|
</template>
|
||||||
2
apps/web-antd/src/views/system/oss/constant.ts
Normal file
2
apps/web-antd/src/views/system/oss/constant.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/** 支持的图片列表 */
|
||||||
|
export const supportImageList = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
@@ -73,9 +73,3 @@ export const columns: VxeGridProps['columns'] = [
|
|||||||
width: 'auto',
|
width: 'auto',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片加载失败的fallback
|
|
||||||
*/
|
|
||||||
export const fallbackImageBase64 =
|
|
||||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg==';
|
|
||||||
|
|||||||
1
apps/web-antd/src/views/system/oss/fallback-image.txt
Normal file
1
apps/web-antd/src/views/system/oss/fallback-image.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
data:image/png;base64,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
|
||||||
@@ -31,7 +31,11 @@ const [BasicModal, modalApi] = useVbenModal({
|
|||||||
title="文件上传"
|
title="文件上传"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<FileUpload v-model:value="fileList" :enable-drag-upload="true" />
|
<FileUpload
|
||||||
|
v-model:value="fileList"
|
||||||
|
:enable-drag-upload="true"
|
||||||
|
:max-count="3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const [BasicModal, modalApi] = useVbenModal({
|
|||||||
title="图片上传"
|
title="图片上传"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<ImageUpload v-model:value="fileList" />
|
<ImageUpload v-model:value="fileList" :max-count="3" />
|
||||||
</div>
|
</div>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ import { ossDownload, ossList, ossRemove } from '#/api/system/oss';
|
|||||||
import { calculateFileSize } from '#/utils/file';
|
import { calculateFileSize } from '#/utils/file';
|
||||||
import { downloadByData } from '#/utils/file/download';
|
import { downloadByData } from '#/utils/file/download';
|
||||||
|
|
||||||
import { columns, fallbackImageBase64, querySchema } from './data';
|
import { supportImageList } from './constant';
|
||||||
|
import { columns, querySchema } from './data';
|
||||||
|
import fallbackImageBase64 from './fallback-image.txt?raw';
|
||||||
import fileUploadModal from './file-upload-modal.vue';
|
import fileUploadModal from './file-upload-modal.vue';
|
||||||
import imageUploadModal from './image-upload-modal.vue';
|
import imageUploadModal from './image-upload-modal.vue';
|
||||||
|
|
||||||
@@ -154,7 +156,7 @@ function handleMultiDelete() {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
function handleToSettings() {
|
function handleToSettings() {
|
||||||
router.push('/system/oss-config');
|
router.push('/system/oss-config/index');
|
||||||
}
|
}
|
||||||
|
|
||||||
const preview = ref(false);
|
const preview = ref(false);
|
||||||
@@ -163,10 +165,32 @@ onMounted(async () => {
|
|||||||
preview.value = previewStr === 'true';
|
preview.value = previewStr === 'true';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据拓展名判断是否是图片
|
||||||
|
* @param ext 拓展名
|
||||||
|
*/
|
||||||
function isImageFile(ext: string) {
|
function isImageFile(ext: string) {
|
||||||
const supportList = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
return supportImageList.some((item) =>
|
||||||
return supportList.some((item) => ext.toLocaleLowerCase().includes(item));
|
ext.toLocaleLowerCase().includes(item),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否是pdf文件
|
||||||
|
* @param ext 扩展名
|
||||||
|
*/
|
||||||
|
function isPdfFile(ext: string) {
|
||||||
|
return ext.toLocaleLowerCase().includes('pdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pdf预览 使用浏览器接管
|
||||||
|
* @param url 文件地址
|
||||||
|
*/
|
||||||
|
function pdfPreview(url: string) {
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
|
||||||
const [ImageUploadModal, imageUploadApi] = useVbenModal({
|
const [ImageUploadModal, imageUploadApi] = useVbenModal({
|
||||||
connectedComponent: imageUploadModal,
|
connectedComponent: imageUploadModal,
|
||||||
});
|
});
|
||||||
@@ -230,6 +254,12 @@ const [FileUploadModal, fileUploadApi] = useVbenModal({
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Image>
|
</Image>
|
||||||
|
<!-- pdf预览 使用浏览器开新窗口 -->
|
||||||
|
<span
|
||||||
|
v-else-if="preview && isPdfFile(row.url)"
|
||||||
|
class="icon-[vscode-icons--file-type-pdf2] size-10 cursor-pointer"
|
||||||
|
@click.stop="pdfPreview(row.url)"
|
||||||
|
></span>
|
||||||
<span v-else>{{ row.url }}</span>
|
<span v-else>{{ row.url }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||||||
fieldName: 'postSort',
|
fieldName: 'postSort',
|
||||||
label: '岗位排序',
|
label: '岗位排序',
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'RadioGroup',
|
component: 'RadioGroup',
|
||||||
|
|||||||
11
apps/web-antd/src/views/system/role/authUser.vue
Normal file
11
apps/web-antd/src/views/system/role/authUser.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!--
|
||||||
|
后端版本>=5.4.0 这个从本地路由变为从后台返回
|
||||||
|
未修改文件名 而是新加了这个文件
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import RoleAssignPage from '#/views/system/role-assign/index.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RoleAssignPage />
|
||||||
|
</template>
|
||||||
@@ -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 {
|
||||||
@@ -142,7 +135,7 @@ function handleAuthEdit(record: Role) {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
function handleAssignRole(record: Role) {
|
function handleAssignRole(record: Role) {
|
||||||
router.push(`/system/role-assign/${record.roleId}`);
|
router.push(`/system/role-auth/user/${record.roleId}`);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -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() {
|
||||||
|
|||||||
6
apps/web-antd/src/views/system/user/authRole.vue
Normal file
6
apps/web-antd/src/views/system/user/authRole.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
ele版本会使用这个文件 只是为了不报错`未找到对应组件`才新建的这个文件
|
||||||
|
无实际意义
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -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',
|
||||||
|
|||||||
10
apps/web-antd/src/views/tool/gen/editTable.vue
Normal file
10
apps/web-antd/src/views/tool/gen/editTable.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!--
|
||||||
|
后端版本>=5.4.0 这个从本地路由变为从后台返回
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import EditGenPage from './edit-gen.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EditGenPage />
|
||||||
|
</template>
|
||||||
@@ -110,7 +110,7 @@ function handlePreview(record: Recordable<any>) {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
function handleEdit(record: Recordable<any>) {
|
function handleEdit(record: Recordable<any>) {
|
||||||
router.push(`/code-gen/edit/${record.tableId}`);
|
router.push(`/tool/gen-edit/index/${record.tableId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSync(record: Recordable<any>) {
|
async function handleSync(record: Recordable<any>) {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { renderDict } from '#/utils/render';
|
|||||||
|
|
||||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
|
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
|
||||||
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';
|
||||||
|
|
||||||
@@ -442,10 +443,7 @@ async function handleCopy(text: string) {
|
|||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
<TabPane key="2" tab="审批流程图">
|
<TabPane key="2" tab="审批流程图">
|
||||||
<img
|
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
||||||
:src="`data:image/png;base64,${currentFlowInfo.image}`"
|
|
||||||
class="rounded-lg border"
|
|
||||||
/>
|
|
||||||
</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>
|
||||||
<iframe :src="url" class="size-full"></iframe>
|
<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>
|
||||||
|
</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>
|
||||||
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 };
|
||||||
|
}
|
||||||
11
apps/web-antd/src/views/workflow/leave/leaveEdit.vue
Normal file
11
apps/web-antd/src/views/workflow/leave/leaveEdit.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!--
|
||||||
|
后端版本>=5.4.0 这个从本地路由变为从后台返回
|
||||||
|
未修改文件名 而是新加了这个文件
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import LeaveFormPage from './leave-form.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LeaveFormPage />
|
||||||
|
</template>
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!--
|
||||||
|
后端版本>=5.4.0 这个从本地路由变为从后台返回
|
||||||
|
未修改文件名 而是新加了这个文件
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FlowDesignerPage from '../components/flow-designer.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FlowDesignerPage />
|
||||||
|
</template>
|
||||||
@@ -154,7 +154,7 @@ const router = useRouter();
|
|||||||
*/
|
*/
|
||||||
function handleDesign(row: any, disabled: boolean) {
|
function handleDesign(row: any, disabled: boolean) {
|
||||||
router.push({
|
router.push({
|
||||||
path: '/workflow/designer',
|
path: '/workflow/design/index',
|
||||||
query: { definitionId: row.id, disabled: String(disabled) },
|
query: { definitionId: row.id, disabled: String(disabled) },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ outline: deep
|
|||||||
|
|
||||||
<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
|
<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
|
||||||
|
|
||||||
|
## 自动显示 tooltip
|
||||||
|
|
||||||
|
通过`tooltip-when-ellipsis`设置,仅在文本长度超出导致省略号出现时才触发 tooltip。
|
||||||
|
|
||||||
|
<DemoPreview dir="demos/vben-ellipsis-text/auto-display" />
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
@@ -37,6 +43,8 @@ outline: deep
|
|||||||
| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
|
| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
|
||||||
| placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
|
| placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
|
||||||
| tooltip | 启用文本提示 | `boolean` | `true` |
|
| tooltip | 启用文本提示 | `boolean` | `true` |
|
||||||
|
| tooltipWhenEllipsis | 内容超出,自动启用文本提示 | `boolean` | `false` |
|
||||||
|
| ellipsisThreshold | 设置 tooltipWhenEllipsis 后才生效,文本截断检测的像素差异阈值,越大则判断越严格,如果碰见异常情况可以自己设置阈值 | `number` | `3` |
|
||||||
| tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
|
| tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
|
||||||
| tooltipColor | 提示文本的颜色 | `string` | - |
|
| tooltipColor | 提示文本的颜色 | `string` | - |
|
||||||
| tooltipFontSize | 提示文本的大小 | `string` | - |
|
| tooltipFontSize | 提示文本的大小 | `string` | - |
|
||||||
|
|||||||
16
docs/src/demos/vben-ellipsis-text/auto-display/index.vue
Normal file
16
docs/src/demos/vben-ellipsis-text/auto-display/index.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { EllipsisText } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const text = `
|
||||||
|
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<EllipsisText :line="2" :tooltip-when-ellipsis="true">
|
||||||
|
{{ text }}
|
||||||
|
</EllipsisText>
|
||||||
|
|
||||||
|
<EllipsisText :line="3" :tooltip-when-ellipsis="true">
|
||||||
|
{{ text }}
|
||||||
|
</EllipsisText>
|
||||||
|
</template>
|
||||||
@@ -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
|
||||||
@@ -186,6 +207,12 @@ const defaultPreferences: Preferences = {
|
|||||||
colorWeakMode: false,
|
colorWeakMode: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
contentCompact: 'wide',
|
contentCompact: 'wide',
|
||||||
|
contentCompactWidth: 1200,
|
||||||
|
contentPadding: 0,
|
||||||
|
contentPaddingBottom: 0,
|
||||||
|
contentPaddingLeft: 0,
|
||||||
|
contentPaddingRight: 0,
|
||||||
|
contentPaddingTop: 0,
|
||||||
defaultAvatar:
|
defaultAvatar:
|
||||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||||
defaultHomePath: '/analytics',
|
defaultHomePath: '/analytics',
|
||||||
@@ -200,6 +227,7 @@ const defaultPreferences: Preferences = {
|
|||||||
name: 'Vben Admin',
|
name: 'Vben Admin',
|
||||||
preferencesButtonPosition: 'auto',
|
preferencesButtonPosition: 'auto',
|
||||||
watermark: false,
|
watermark: false,
|
||||||
|
zIndex: 200,
|
||||||
},
|
},
|
||||||
breadcrumb: {
|
breadcrumb: {
|
||||||
enable: true,
|
enable: true,
|
||||||
@@ -220,15 +248,18 @@ const defaultPreferences: Preferences = {
|
|||||||
footer: {
|
footer: {
|
||||||
enable: false,
|
enable: false,
|
||||||
fixed: false,
|
fixed: false,
|
||||||
|
height: 32,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
height: 50,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
menuAlign: 'start',
|
menuAlign: 'start',
|
||||||
mode: 'fixed',
|
mode: 'fixed',
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
fit: 'contain',
|
||||||
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
@@ -248,11 +279,14 @@ const defaultPreferences: Preferences = {
|
|||||||
collapsed: false,
|
collapsed: false,
|
||||||
collapsedButton: true,
|
collapsedButton: true,
|
||||||
collapsedShowTitle: false,
|
collapsedShowTitle: false,
|
||||||
|
collapseWidth: 60,
|
||||||
enable: true,
|
enable: true,
|
||||||
expandOnHover: true,
|
expandOnHover: true,
|
||||||
extraCollapse: false,
|
extraCollapse: false,
|
||||||
|
extraCollapsedWidth: 60,
|
||||||
fixedButton: true,
|
fixedButton: true,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
mixedWidth: 80,
|
||||||
width: 224,
|
width: 224,
|
||||||
},
|
},
|
||||||
tabbar: {
|
tabbar: {
|
||||||
@@ -319,6 +353,18 @@ interface AppPreferences {
|
|||||||
compact: boolean;
|
compact: boolean;
|
||||||
/** Whether to enable content compact mode */
|
/** Whether to enable content compact mode */
|
||||||
contentCompact: ContentCompactType;
|
contentCompact: ContentCompactType;
|
||||||
|
/** Content compact width */
|
||||||
|
contentCompactWidth: number;
|
||||||
|
/** Content padding */
|
||||||
|
contentPadding: number;
|
||||||
|
/** Content bottom padding */
|
||||||
|
contentPaddingBottom: number;
|
||||||
|
/** Content left padding */
|
||||||
|
contentPaddingLeft: number;
|
||||||
|
/** Content right padding */
|
||||||
|
contentPaddingRight: number;
|
||||||
|
/** Content top padding */
|
||||||
|
contentPaddingTop: number;
|
||||||
// /** Default application avatar */
|
// /** Default application avatar */
|
||||||
defaultAvatar: string;
|
defaultAvatar: string;
|
||||||
/** Default homepage path */
|
/** Default homepage path */
|
||||||
@@ -349,6 +395,8 @@ interface AppPreferences {
|
|||||||
* @zh_CN Whether to enable watermark
|
* @zh_CN Whether to enable watermark
|
||||||
*/
|
*/
|
||||||
watermark: boolean;
|
watermark: boolean;
|
||||||
|
/** z-index */
|
||||||
|
zIndex: number;
|
||||||
}
|
}
|
||||||
interface BreadcrumbPreferences {
|
interface BreadcrumbPreferences {
|
||||||
/** Whether breadcrumbs are enabled */
|
/** Whether breadcrumbs are enabled */
|
||||||
@@ -385,11 +433,15 @@ interface FooterPreferences {
|
|||||||
enable: boolean;
|
enable: boolean;
|
||||||
/** Whether the footer is fixed */
|
/** Whether the footer is fixed */
|
||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
|
/** Footer height */
|
||||||
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HeaderPreferences {
|
interface HeaderPreferences {
|
||||||
/** Whether the header is enabled */
|
/** Whether the header is enabled */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
/** Header height */
|
||||||
|
height: number;
|
||||||
/** Whether the header is hidden, css-hidden */
|
/** Whether the header is hidden, css-hidden */
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
/** Header menu alignment */
|
/** Header menu alignment */
|
||||||
@@ -401,6 +453,8 @@ interface HeaderPreferences {
|
|||||||
interface LogoPreferences {
|
interface LogoPreferences {
|
||||||
/** Whether the logo is visible */
|
/** Whether the logo is visible */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
/** Logo image fitting method */
|
||||||
|
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||||
/** Logo URL */
|
/** Logo URL */
|
||||||
source: string;
|
source: string;
|
||||||
}
|
}
|
||||||
@@ -422,16 +476,22 @@ interface SidebarPreferences {
|
|||||||
collapsedButton: boolean;
|
collapsedButton: boolean;
|
||||||
/** Whether to show title when sidebar is collapsed */
|
/** Whether to show title when sidebar is collapsed */
|
||||||
collapsedShowTitle: boolean;
|
collapsedShowTitle: boolean;
|
||||||
|
/** Sidebar collapse width */
|
||||||
|
collapseWidth: number;
|
||||||
/** Whether the sidebar is visible */
|
/** Whether the sidebar is visible */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
/** Menu auto-expand state */
|
/** Menu auto-expand state */
|
||||||
expandOnHover: boolean;
|
expandOnHover: boolean;
|
||||||
/** Whether the sidebar extension area is collapsed */
|
/** Whether the sidebar extension area is collapsed */
|
||||||
extraCollapse: boolean;
|
extraCollapse: boolean;
|
||||||
|
/** Sidebar extension area collapse width */
|
||||||
|
extraCollapsedWidth: number;
|
||||||
/** Whether the sidebar fixed button is visible */
|
/** Whether the sidebar fixed button is visible */
|
||||||
fixedButton: boolean;
|
fixedButton: boolean;
|
||||||
/** Whether the sidebar is hidden - css */
|
/** Whether the sidebar is hidden - css */
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
|
/** Mixed sidebar width */
|
||||||
|
mixedWidth: number;
|
||||||
/** Sidebar width */
|
/** Sidebar width */
|
||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ outline: deep
|
|||||||
|
|
||||||
# Access Control
|
# Access Control
|
||||||
|
|
||||||
The framework has built-in two types of access control methods:
|
The framework has built-in three types of access control methods:
|
||||||
|
|
||||||
- Determining whether a menu or button can be accessed based on user roles
|
- Determining whether a menu or button can be accessed based on user roles
|
||||||
- Determining whether a menu or button can be accessed through an API
|
- Determining whether a menu or button can be accessed through an API
|
||||||
|
- Mixed mode: Using both frontend and backend access control simultaneously
|
||||||
|
|
||||||
## Frontend Access Control
|
## Frontend Access Control
|
||||||
|
|
||||||
@@ -151,6 +152,43 @@ const dashboardMenus = [
|
|||||||
|
|
||||||
At this point, the configuration is complete. You need to ensure that after logging in, the format of the menu returned by the interface is correct; otherwise, access will not be possible.
|
At this point, the configuration is complete. You need to ensure that after logging in, the format of the menu returned by the interface is correct; otherwise, access will not be possible.
|
||||||
|
|
||||||
|
## Mixed Access Control
|
||||||
|
|
||||||
|
**Implementation Principle**: Mixed mode combines both frontend access control and backend access control methods. The system processes frontend fixed route permissions and backend dynamic menu data in parallel, ultimately merging both parts of routes to provide a more flexible access control solution.
|
||||||
|
|
||||||
|
**Advantages**: Combines the performance advantages of frontend control with the flexibility of backend control, suitable for complex business scenarios requiring permission management.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
- Ensure the current mode is set to mixed access control
|
||||||
|
|
||||||
|
Adjust `preferences.ts` in the corresponding application directory to ensure `accessMode='mixed'`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineOverridesPreferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
export const overridesPreferences = defineOverridesPreferences({
|
||||||
|
// overrides
|
||||||
|
app: {
|
||||||
|
accessMode: 'mixed',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- Configure frontend route permissions
|
||||||
|
|
||||||
|
Same as the route permission configuration method in [Frontend Access Control](#frontend-access-control) mode.
|
||||||
|
|
||||||
|
- Configure backend menu interface
|
||||||
|
|
||||||
|
Same as the interface configuration method in [Backend Access Control](#backend-access-control) mode.
|
||||||
|
|
||||||
|
- Ensure roles and permissions match
|
||||||
|
|
||||||
|
Must satisfy both frontend route permission configuration and backend menu data return requirements, ensuring user roles match the permission configurations of both modes.
|
||||||
|
|
||||||
|
At this point, the configuration is complete. Mixed mode will automatically merge frontend and backend routes, providing complete access control functionality.
|
||||||
|
|
||||||
## Fine-grained Control of Buttons
|
## Fine-grained Control of Buttons
|
||||||
|
|
||||||
In some cases, we need to control the display of buttons with fine granularity. We can control the display of buttons through interfaces or roles.
|
In some cases, we need to control the display of buttons with fine granularity. We can control the display of buttons through interfaces or roles.
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ server {
|
|||||||
|
|
||||||
使用 nginx 处理项目部署后的跨域问题
|
使用 nginx 处理项目部署后的跨域问题
|
||||||
|
|
||||||
1. 配置前端项目接口地址,在项目目录下的``.env.production`文件中配置:
|
1. 配置前端项目接口地址,在项目目录下的`.env.production`文件中配置:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VITE_GLOB_API_URL=/api
|
VITE_GLOB_API_URL=/api
|
||||||
|
|||||||
@@ -339,6 +339,10 @@ interface RouteMeta {
|
|||||||
| 'success'
|
| 'success'
|
||||||
| 'warning'
|
| 'warning'
|
||||||
| string;
|
| string;
|
||||||
|
/**
|
||||||
|
* 路由的完整路径作为key(默认true)
|
||||||
|
*/
|
||||||
|
fullPathKey?: boolean;
|
||||||
/**
|
/**
|
||||||
* 当前路由的子级在菜单中不展现
|
* 当前路由的子级在菜单中不展现
|
||||||
* @default false
|
* @default false
|
||||||
@@ -502,6 +506,13 @@ interface RouteMeta {
|
|||||||
|
|
||||||
用于配置页面的徽标颜色。
|
用于配置页面的徽标颜色。
|
||||||
|
|
||||||
|
### fullPathKey
|
||||||
|
|
||||||
|
- 类型:`boolean`
|
||||||
|
- 默认值:`true`
|
||||||
|
|
||||||
|
是否将路由的完整路径作为tab key(默认true)
|
||||||
|
|
||||||
### activePath
|
### activePath
|
||||||
|
|
||||||
- 类型:`string`
|
- 类型:`string`
|
||||||
@@ -602,3 +613,32 @@ const { refresh } = useRefresh();
|
|||||||
refresh();
|
refresh();
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 标签页与路由控制
|
||||||
|
|
||||||
|
在某些场景下,需要单个路由打开多个标签页,或者修改路由的query不打开新的标签页
|
||||||
|
|
||||||
|
每个标签页Tab使用唯一的key标识,设置Tab key有三种方式,优先级由高到低:
|
||||||
|
|
||||||
|
- 使用路由query参数pageKey
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
// 跳转路由
|
||||||
|
const router = useRouter();
|
||||||
|
router.push({
|
||||||
|
path: 'path',
|
||||||
|
query: {
|
||||||
|
pageKey: 'key',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- 路由的完整路径作为key
|
||||||
|
|
||||||
|
`meta` 属性中的 `fullPathKey`不为false,则使用路由`fullPath`作为key
|
||||||
|
|
||||||
|
- 路由的path作为key
|
||||||
|
|
||||||
|
`meta` 属性中的 `fullPathKey`为false,则使用路由`path`作为key
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -185,6 +206,12 @@ const defaultPreferences: Preferences = {
|
|||||||
colorWeakMode: false,
|
colorWeakMode: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
contentCompact: 'wide',
|
contentCompact: 'wide',
|
||||||
|
contentCompactWidth: 1200,
|
||||||
|
contentPadding: 0,
|
||||||
|
contentPaddingBottom: 0,
|
||||||
|
contentPaddingLeft: 0,
|
||||||
|
contentPaddingRight: 0,
|
||||||
|
contentPaddingTop: 0,
|
||||||
defaultAvatar:
|
defaultAvatar:
|
||||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||||
defaultHomePath: '/analytics',
|
defaultHomePath: '/analytics',
|
||||||
@@ -199,6 +226,7 @@ const defaultPreferences: Preferences = {
|
|||||||
name: 'Vben Admin',
|
name: 'Vben Admin',
|
||||||
preferencesButtonPosition: 'auto',
|
preferencesButtonPosition: 'auto',
|
||||||
watermark: false,
|
watermark: false,
|
||||||
|
zIndex: 200,
|
||||||
},
|
},
|
||||||
breadcrumb: {
|
breadcrumb: {
|
||||||
enable: true,
|
enable: true,
|
||||||
@@ -219,15 +247,18 @@ const defaultPreferences: Preferences = {
|
|||||||
footer: {
|
footer: {
|
||||||
enable: false,
|
enable: false,
|
||||||
fixed: false,
|
fixed: false,
|
||||||
|
height: 32,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
height: 50,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
menuAlign: 'start',
|
menuAlign: 'start',
|
||||||
mode: 'fixed',
|
mode: 'fixed',
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
fit: 'contain',
|
||||||
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
@@ -247,11 +278,14 @@ const defaultPreferences: Preferences = {
|
|||||||
collapsed: false,
|
collapsed: false,
|
||||||
collapsedButton: true,
|
collapsedButton: true,
|
||||||
collapsedShowTitle: false,
|
collapsedShowTitle: false,
|
||||||
|
collapseWidth: 60,
|
||||||
enable: true,
|
enable: true,
|
||||||
expandOnHover: true,
|
expandOnHover: true,
|
||||||
extraCollapse: false,
|
extraCollapse: false,
|
||||||
|
extraCollapsedWidth: 60,
|
||||||
fixedButton: true,
|
fixedButton: true,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
mixedWidth: 80,
|
||||||
width: 224,
|
width: 224,
|
||||||
},
|
},
|
||||||
tabbar: {
|
tabbar: {
|
||||||
@@ -318,6 +352,18 @@ interface AppPreferences {
|
|||||||
compact: boolean;
|
compact: boolean;
|
||||||
/** 是否开启内容紧凑模式 */
|
/** 是否开启内容紧凑模式 */
|
||||||
contentCompact: ContentCompactType;
|
contentCompact: ContentCompactType;
|
||||||
|
/** 内容紧凑宽度 */
|
||||||
|
contentCompactWidth: number;
|
||||||
|
/** 内容内边距 */
|
||||||
|
contentPadding: number;
|
||||||
|
/** 内容底部内边距 */
|
||||||
|
contentPaddingBottom: number;
|
||||||
|
/** 内容左侧内边距 */
|
||||||
|
contentPaddingLeft: number;
|
||||||
|
/** 内容右侧内边距 */
|
||||||
|
contentPaddingRight: number;
|
||||||
|
/** 内容顶部内边距 */
|
||||||
|
contentPaddingTop: number;
|
||||||
// /** 应用默认头像 */
|
// /** 应用默认头像 */
|
||||||
defaultAvatar: string;
|
defaultAvatar: string;
|
||||||
/** 默认首页地址 */
|
/** 默认首页地址 */
|
||||||
@@ -348,6 +394,8 @@ interface AppPreferences {
|
|||||||
* @zh_CN 是否开启水印
|
* @zh_CN 是否开启水印
|
||||||
*/
|
*/
|
||||||
watermark: boolean;
|
watermark: boolean;
|
||||||
|
/** z-index */
|
||||||
|
zIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BreadcrumbPreferences {
|
interface BreadcrumbPreferences {
|
||||||
@@ -385,11 +433,15 @@ interface FooterPreferences {
|
|||||||
enable: boolean;
|
enable: boolean;
|
||||||
/** 底栏是否固定 */
|
/** 底栏是否固定 */
|
||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
|
/** 底栏高度 */
|
||||||
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HeaderPreferences {
|
interface HeaderPreferences {
|
||||||
/** 顶栏是否启用 */
|
/** 顶栏是否启用 */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
/** 顶栏高度 */
|
||||||
|
height: number;
|
||||||
/** 顶栏是否隐藏,css-隐藏 */
|
/** 顶栏是否隐藏,css-隐藏 */
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
/** 顶栏菜单位置 */
|
/** 顶栏菜单位置 */
|
||||||
@@ -401,6 +453,8 @@ interface HeaderPreferences {
|
|||||||
interface LogoPreferences {
|
interface LogoPreferences {
|
||||||
/** logo是否可见 */
|
/** logo是否可见 */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
/** logo图片适应方式 */
|
||||||
|
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||||
/** logo地址 */
|
/** logo地址 */
|
||||||
source: string;
|
source: string;
|
||||||
}
|
}
|
||||||
@@ -423,16 +477,22 @@ interface SidebarPreferences {
|
|||||||
collapsedButton: boolean;
|
collapsedButton: boolean;
|
||||||
/** 侧边栏折叠时,是否显示title */
|
/** 侧边栏折叠时,是否显示title */
|
||||||
collapsedShowTitle: boolean;
|
collapsedShowTitle: boolean;
|
||||||
|
/** 侧边栏折叠宽度 */
|
||||||
|
collapseWidth: number;
|
||||||
/** 侧边栏是否可见 */
|
/** 侧边栏是否可见 */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
/** 菜单自动展开状态 */
|
/** 菜单自动展开状态 */
|
||||||
expandOnHover: boolean;
|
expandOnHover: boolean;
|
||||||
/** 侧边栏扩展区域是否折叠 */
|
/** 侧边栏扩展区域是否折叠 */
|
||||||
extraCollapse: boolean;
|
extraCollapse: boolean;
|
||||||
|
/** 侧边栏扩展区域折叠宽度 */
|
||||||
|
extraCollapsedWidth: number;
|
||||||
/** 侧边栏固定按钮是否可见 */
|
/** 侧边栏固定按钮是否可见 */
|
||||||
fixedButton: boolean;
|
fixedButton: boolean;
|
||||||
/** 侧边栏是否隐藏 - css */
|
/** 侧边栏是否隐藏 - css */
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
|
/** 混合侧边栏宽度 */
|
||||||
|
mixedWidth: number;
|
||||||
/** 侧边栏宽度 */
|
/** 侧边栏宽度 */
|
||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ outline: deep
|
|||||||
|
|
||||||
# 权限
|
# 权限
|
||||||
|
|
||||||
框架内置了两种权限控制方式:
|
框架内置了三种权限控制方式:
|
||||||
|
|
||||||
- 通过用户角色来判断菜单或者按钮是否可以访问
|
- 通过用户角色来判断菜单或者按钮是否可以访问
|
||||||
- 通过接口来判断菜单或者按钮是否可以访问
|
- 通过接口来判断菜单或者按钮是否可以访问
|
||||||
|
- 混合模式:同时使用前端和后端权限控制
|
||||||
|
|
||||||
## 前端访问控制
|
## 前端访问控制
|
||||||
|
|
||||||
@@ -159,6 +160,43 @@ const dashboardMenus = [
|
|||||||
|
|
||||||
到这里,就已经配置完成,你需要确保登录后,接口返回的菜单格式正确,否则无法访问。
|
到这里,就已经配置完成,你需要确保登录后,接口返回的菜单格式正确,否则无法访问。
|
||||||
|
|
||||||
|
## 混合访问控制
|
||||||
|
|
||||||
|
**实现原理**: 混合模式同时结合了前端访问控制和后端访问控制两种方式。系统会并行处理前端固定路由权限和后端动态菜单数据,最终将两部分路由合并,提供更灵活的权限控制方案。
|
||||||
|
|
||||||
|
**优点**: 兼具前端控制的性能优势和后端控制的灵活性,适合复杂业务场景下的权限管理。
|
||||||
|
|
||||||
|
### 步骤
|
||||||
|
|
||||||
|
- 确保当前模式为混合访问控制模式
|
||||||
|
|
||||||
|
调整对应应用目录下的`preferences.ts`,确保`accessMode='mixed'`。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineOverridesPreferences } from '@vben/preferences';
|
||||||
|
|
||||||
|
export const overridesPreferences = defineOverridesPreferences({
|
||||||
|
// overrides
|
||||||
|
app: {
|
||||||
|
accessMode: 'mixed',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- 配置前端路由权限
|
||||||
|
|
||||||
|
同[前端访问控制](#前端访问控制)模式的路由权限配置方式。
|
||||||
|
|
||||||
|
- 配置后端菜单接口
|
||||||
|
|
||||||
|
同[后端访问控制](#后端访问控制)模式的接口配置方式。
|
||||||
|
|
||||||
|
- 确保角色和权限匹配
|
||||||
|
|
||||||
|
需要同时满足前端路由权限配置和后端菜单数据返回的要求,确保用户角色与两种模式的权限配置都匹配。
|
||||||
|
|
||||||
|
到这里,就已经配置完成,混合模式会自动合并前端和后端的路由,提供完整的权限控制功能。
|
||||||
|
|
||||||
## 按钮细粒度控制
|
## 按钮细粒度控制
|
||||||
|
|
||||||
在某些情况下,我们需要对按钮进行细粒度的控制,我们可以借助接口或者角色来控制按钮的显示。
|
在某些情况下,我们需要对按钮进行细粒度的控制,我们可以借助接口或者角色来控制按钮的显示。
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"stub": "pnpm unbuild"
|
"stub": "pnpm unbuild --stub"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
3
packages/@core/base/typings/src/app.d.ts
vendored
3
packages/@core/base/typings/src/app.d.ts
vendored
@@ -60,8 +60,9 @@ type BreadcrumbStyleType = 'background' | 'normal';
|
|||||||
* 权限模式
|
* 权限模式
|
||||||
* backend 后端权限模式
|
* backend 后端权限模式
|
||||||
* frontend 前端权限模式
|
* frontend 前端权限模式
|
||||||
|
* mixed 混合权限模式
|
||||||
*/
|
*/
|
||||||
type AccessModeType = 'backend' | 'frontend';
|
type AccessModeType = 'backend' | 'frontend' | 'mixed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导航风格
|
* 导航风格
|
||||||
|
|||||||
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,3 +1,8 @@
|
|||||||
import type { RouteLocationNormalized } from 'vue-router';
|
import type { RouteLocationNormalized } from 'vue-router';
|
||||||
|
|
||||||
export type TabDefinition = RouteLocationNormalized;
|
export interface TabDefinition extends RouteLocationNormalized {
|
||||||
|
/**
|
||||||
|
* 标签页的key
|
||||||
|
*/
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ interface RouteMeta {
|
|||||||
| 'success'
|
| 'success'
|
||||||
| 'warning'
|
| 'warning'
|
||||||
| string;
|
| string;
|
||||||
|
/**
|
||||||
|
* 路由的完整路径作为key(默认true)
|
||||||
|
*/
|
||||||
|
fullPathKey?: boolean;
|
||||||
/**
|
/**
|
||||||
* 当前路由的子级在菜单中不展现
|
* 当前路由的子级在菜单中不展现
|
||||||
* @default false
|
* @default false
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
"colorWeakMode": false,
|
"colorWeakMode": false,
|
||||||
"compact": false,
|
"compact": false,
|
||||||
"contentCompact": "wide",
|
"contentCompact": "wide",
|
||||||
|
"contentCompactWidth": 1200,
|
||||||
|
"contentPadding": 0,
|
||||||
|
"contentPaddingBottom": 0,
|
||||||
|
"contentPaddingLeft": 0,
|
||||||
|
"contentPaddingRight": 0,
|
||||||
|
"contentPaddingTop": 0,
|
||||||
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
|
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
|
||||||
"defaultHomePath": "/analytics",
|
"defaultHomePath": "/analytics",
|
||||||
"dynamicTitle": true,
|
"dynamicTitle": true,
|
||||||
@@ -23,6 +29,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
"name": "Vben Admin",
|
"name": "Vben Admin",
|
||||||
"preferencesButtonPosition": "auto",
|
"preferencesButtonPosition": "auto",
|
||||||
"watermark": false,
|
"watermark": false,
|
||||||
|
"zIndex": 200,
|
||||||
},
|
},
|
||||||
"breadcrumb": {
|
"breadcrumb": {
|
||||||
"enable": true,
|
"enable": true,
|
||||||
@@ -43,15 +50,18 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
"footer": {
|
"footer": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
"fixed": false,
|
"fixed": false,
|
||||||
|
"height": 32,
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"enable": true,
|
"enable": true,
|
||||||
|
"height": 50,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"menuAlign": "start",
|
"menuAlign": "start",
|
||||||
"mode": "fixed",
|
"mode": "fixed",
|
||||||
},
|
},
|
||||||
"logo": {
|
"logo": {
|
||||||
"enable": true,
|
"enable": true,
|
||||||
|
"fit": "contain",
|
||||||
"source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp",
|
"source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp",
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
@@ -68,14 +78,17 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"autoActivateChild": false,
|
"autoActivateChild": false,
|
||||||
|
"collapseWidth": 60,
|
||||||
"collapsed": false,
|
"collapsed": false,
|
||||||
"collapsedButton": true,
|
"collapsedButton": true,
|
||||||
"collapsedShowTitle": false,
|
"collapsedShowTitle": false,
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"expandOnHover": true,
|
"expandOnHover": true,
|
||||||
"extraCollapse": false,
|
"extraCollapse": false,
|
||||||
|
"extraCollapsedWidth": 60,
|
||||||
"fixedButton": true,
|
"fixedButton": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
|
"mixedWidth": 80,
|
||||||
"width": 224,
|
"width": 224,
|
||||||
},
|
},
|
||||||
"tabbar": {
|
"tabbar": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ const defaultPreferences: Preferences = {
|
|||||||
colorWeakMode: false,
|
colorWeakMode: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
contentCompact: 'wide',
|
contentCompact: 'wide',
|
||||||
|
contentCompactWidth: 1200,
|
||||||
|
contentPadding: 0,
|
||||||
|
contentPaddingBottom: 0,
|
||||||
|
contentPaddingLeft: 0,
|
||||||
|
contentPaddingRight: 0,
|
||||||
|
contentPaddingTop: 0,
|
||||||
defaultAvatar:
|
defaultAvatar:
|
||||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||||
defaultHomePath: '/analytics',
|
defaultHomePath: '/analytics',
|
||||||
@@ -23,6 +29,7 @@ const defaultPreferences: Preferences = {
|
|||||||
name: 'Vben Admin',
|
name: 'Vben Admin',
|
||||||
preferencesButtonPosition: 'auto',
|
preferencesButtonPosition: 'auto',
|
||||||
watermark: false,
|
watermark: false,
|
||||||
|
zIndex: 200,
|
||||||
},
|
},
|
||||||
breadcrumb: {
|
breadcrumb: {
|
||||||
enable: true,
|
enable: true,
|
||||||
@@ -43,15 +50,19 @@ const defaultPreferences: Preferences = {
|
|||||||
footer: {
|
footer: {
|
||||||
enable: false,
|
enable: false,
|
||||||
fixed: false,
|
fixed: false,
|
||||||
|
height: 32,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
height: 50,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
menuAlign: 'start',
|
menuAlign: 'start',
|
||||||
mode: 'fixed',
|
mode: 'fixed',
|
||||||
},
|
},
|
||||||
|
|
||||||
logo: {
|
logo: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
fit: 'contain',
|
||||||
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
@@ -71,11 +82,14 @@ const defaultPreferences: Preferences = {
|
|||||||
collapsed: false,
|
collapsed: false,
|
||||||
collapsedButton: true,
|
collapsedButton: true,
|
||||||
collapsedShowTitle: false,
|
collapsedShowTitle: false,
|
||||||
|
collapseWidth: 60,
|
||||||
enable: true,
|
enable: true,
|
||||||
expandOnHover: true,
|
expandOnHover: true,
|
||||||
extraCollapse: false,
|
extraCollapse: false,
|
||||||
|
extraCollapsedWidth: 60,
|
||||||
fixedButton: true,
|
fixedButton: true,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
mixedWidth: 80,
|
||||||
width: 224,
|
width: 224,
|
||||||
},
|
},
|
||||||
tabbar: {
|
tabbar: {
|
||||||
|
|||||||
@@ -33,6 +33,18 @@ interface AppPreferences {
|
|||||||
compact: boolean;
|
compact: boolean;
|
||||||
/** 是否开启内容紧凑模式 */
|
/** 是否开启内容紧凑模式 */
|
||||||
contentCompact: ContentCompactType;
|
contentCompact: ContentCompactType;
|
||||||
|
/** 内容紧凑宽度 */
|
||||||
|
contentCompactWidth: number;
|
||||||
|
/** 内容内边距 */
|
||||||
|
contentPadding: number;
|
||||||
|
/** 内容底部内边距 */
|
||||||
|
contentPaddingBottom: number;
|
||||||
|
/** 内容左侧内边距 */
|
||||||
|
contentPaddingLeft: number;
|
||||||
|
/** 内容右侧内边距 */
|
||||||
|
contentPaddingRight: number;
|
||||||
|
/** 内容顶部内边距 */
|
||||||
|
contentPaddingTop: number;
|
||||||
// /** 应用默认头像 */
|
// /** 应用默认头像 */
|
||||||
defaultAvatar: string;
|
defaultAvatar: string;
|
||||||
/** 默认首页地址 */
|
/** 默认首页地址 */
|
||||||
@@ -63,6 +75,8 @@ interface AppPreferences {
|
|||||||
* @zh_CN 是否开启水印
|
* @zh_CN 是否开启水印
|
||||||
*/
|
*/
|
||||||
watermark: boolean;
|
watermark: boolean;
|
||||||
|
/** z-index */
|
||||||
|
zIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BreadcrumbPreferences {
|
interface BreadcrumbPreferences {
|
||||||
@@ -100,11 +114,15 @@ interface FooterPreferences {
|
|||||||
enable: boolean;
|
enable: boolean;
|
||||||
/** 底栏是否固定 */
|
/** 底栏是否固定 */
|
||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
|
/** 底栏高度 */
|
||||||
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HeaderPreferences {
|
interface HeaderPreferences {
|
||||||
/** 顶栏是否启用 */
|
/** 顶栏是否启用 */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
/** 顶栏高度 */
|
||||||
|
height: number;
|
||||||
/** 顶栏是否隐藏,css-隐藏 */
|
/** 顶栏是否隐藏,css-隐藏 */
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
/** 顶栏菜单位置 */
|
/** 顶栏菜单位置 */
|
||||||
@@ -116,6 +134,8 @@ interface HeaderPreferences {
|
|||||||
interface LogoPreferences {
|
interface LogoPreferences {
|
||||||
/** logo是否可见 */
|
/** logo是否可见 */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
/** logo图片适应方式 */
|
||||||
|
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||||
/** logo地址 */
|
/** logo地址 */
|
||||||
source: string;
|
source: string;
|
||||||
}
|
}
|
||||||
@@ -138,16 +158,22 @@ interface SidebarPreferences {
|
|||||||
collapsedButton: boolean;
|
collapsedButton: boolean;
|
||||||
/** 侧边栏折叠时,是否显示title */
|
/** 侧边栏折叠时,是否显示title */
|
||||||
collapsedShowTitle: boolean;
|
collapsedShowTitle: boolean;
|
||||||
|
/** 侧边栏折叠宽度 */
|
||||||
|
collapseWidth: number;
|
||||||
/** 侧边栏是否可见 */
|
/** 侧边栏是否可见 */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
/** 菜单自动展开状态 */
|
/** 菜单自动展开状态 */
|
||||||
expandOnHover: boolean;
|
expandOnHover: boolean;
|
||||||
/** 侧边栏扩展区域是否折叠 */
|
/** 侧边栏扩展区域是否折叠 */
|
||||||
extraCollapse: boolean;
|
extraCollapse: boolean;
|
||||||
|
/** 侧边栏扩展区域折叠宽度 */
|
||||||
|
extraCollapsedWidth: number;
|
||||||
/** 侧边栏固定按钮是否可见 */
|
/** 侧边栏固定按钮是否可见 */
|
||||||
fixedButton: boolean;
|
fixedButton: boolean;
|
||||||
/** 侧边栏是否隐藏 - css */
|
/** 侧边栏是否隐藏 - css */
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
|
/** 混合侧边栏宽度 */
|
||||||
|
mixedWidth: number;
|
||||||
/** 侧边栏宽度 */
|
/** 侧边栏宽度 */
|
||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { Recordable } from '@vben-core/typings';
|
|||||||
|
|
||||||
import type { FormActions, FormSchema, VbenFormProps } from './types';
|
import type { FormActions, FormSchema, VbenFormProps } from './types';
|
||||||
|
|
||||||
import { toRaw } from 'vue';
|
import { isRef, toRaw } from 'vue';
|
||||||
|
|
||||||
import { Store } from '@vben-core/shared/store';
|
import { Store } from '@vben-core/shared/store';
|
||||||
import {
|
import {
|
||||||
@@ -100,9 +100,26 @@ export class FormApi {
|
|||||||
getFieldComponentRef<T = ComponentPublicInstance>(
|
getFieldComponentRef<T = ComponentPublicInstance>(
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
): T | undefined {
|
): T | undefined {
|
||||||
return this.componentRefMap.has(fieldName)
|
let target = this.componentRefMap.has(fieldName)
|
||||||
? (this.componentRefMap.get(fieldName) as T)
|
? (this.componentRefMap.get(fieldName) as ComponentPublicInstance)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
if (
|
||||||
|
target &&
|
||||||
|
target.$.type.name === 'AsyncComponentWrapper' &&
|
||||||
|
target.$.subTree.ref
|
||||||
|
) {
|
||||||
|
if (Array.isArray(target.$.subTree.ref)) {
|
||||||
|
if (
|
||||||
|
target.$.subTree.ref.length > 0 &&
|
||||||
|
isRef(target.$.subTree.ref[0]?.r)
|
||||||
|
) {
|
||||||
|
target = target.$.subTree.ref[0]?.r.value as ComponentPublicInstance;
|
||||||
|
}
|
||||||
|
} else if (isRef(target.$.subTree.ref.r)) {
|
||||||
|
target = target.$.subTree.ref.r.value as ComponentPublicInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { createContext } from '@vben-core/shadcn-ui';
|
|||||||
import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils';
|
import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { useForm } from 'vee-validate';
|
import { useForm } from 'vee-validate';
|
||||||
import { object } from 'zod';
|
import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';
|
||||||
import { getDefaultsForSchema } from 'zod-defaults';
|
import { getDefaultsForSchema } from 'zod-defaults';
|
||||||
|
|
||||||
type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
|
type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi };
|
||||||
@@ -52,7 +52,12 @@ export function useFormInitial(
|
|||||||
if (Reflect.has(item, 'defaultValue')) {
|
if (Reflect.has(item, 'defaultValue')) {
|
||||||
set(initialValues, item.fieldName, item.defaultValue);
|
set(initialValues, item.fieldName, item.defaultValue);
|
||||||
} else if (item.rules && !isString(item.rules)) {
|
} else if (item.rules && !isString(item.rules)) {
|
||||||
|
// 检查规则是否适合提取默认值
|
||||||
|
const customDefaultValue = getCustomDefaultValue(item.rules);
|
||||||
zodObject[item.fieldName] = item.rules;
|
zodObject[item.fieldName] = item.rules;
|
||||||
|
if (customDefaultValue !== undefined) {
|
||||||
|
initialValues[item.fieldName] = customDefaultValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,6 +69,38 @@ export function useFormInitial(
|
|||||||
}
|
}
|
||||||
return mergeWithArrayOverride(initialValues, zodDefaults);
|
return mergeWithArrayOverride(initialValues, zodDefaults);
|
||||||
}
|
}
|
||||||
|
// 自定义默认值提取逻辑
|
||||||
|
function getCustomDefaultValue(rule: any): any {
|
||||||
|
if (rule instanceof ZodString) {
|
||||||
|
return ''; // 默认为空字符串
|
||||||
|
} else if (rule instanceof ZodNumber) {
|
||||||
|
return null; // 默认为 null(避免显示 0)
|
||||||
|
} else if (rule instanceof ZodObject) {
|
||||||
|
// 递归提取嵌套对象的默认值
|
||||||
|
const defaultValues: Record<string, any> = {};
|
||||||
|
for (const [key, valueSchema] of Object.entries(rule.shape)) {
|
||||||
|
defaultValues[key] = getCustomDefaultValue(valueSchema);
|
||||||
|
}
|
||||||
|
return defaultValues;
|
||||||
|
} else if (rule instanceof ZodIntersection) {
|
||||||
|
// 对于交集类型,从schema 提取默认值
|
||||||
|
const leftDefaultValue = getCustomDefaultValue(rule._def.left);
|
||||||
|
const rightDefaultValue = getCustomDefaultValue(rule._def.right);
|
||||||
|
|
||||||
|
// 如果左右两边都能提取默认值,合并它们
|
||||||
|
if (
|
||||||
|
typeof leftDefaultValue === 'object' &&
|
||||||
|
typeof rightDefaultValue === 'object'
|
||||||
|
) {
|
||||||
|
return { ...leftDefaultValue, ...rightDefaultValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则优先使用左边的默认值
|
||||||
|
return leftDefaultValue ?? rightDefaultValue;
|
||||||
|
} else {
|
||||||
|
return undefined; // 其他类型不提供默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
delegatedSlots,
|
delegatedSlots,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user