Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fee909fa3 | ||
|
|
4c39bef181 | ||
|
|
86bcceaa84 | ||
|
|
1980a2482d | ||
|
|
a38cf80ea4 | ||
|
|
a986e1a2ab | ||
|
|
9822d2af8a | ||
|
|
b51f5d1fa6 | ||
|
|
56104b2abf | ||
|
|
b4ca3f43a9 | ||
|
|
0666483c58 | ||
|
|
77c45d855b | ||
|
|
8ce52eef51 | ||
|
|
738a918df6 | ||
|
|
d9131cbe22 | ||
|
|
968a2eb7b6 | ||
|
|
9b59a8acdb | ||
|
|
ab756b3434 | ||
|
|
e23e5cd5a8 | ||
|
|
c3033d66bd | ||
|
|
4291d59b97 | ||
|
|
4fee98ea58 | ||
|
|
7844a3c7e6 | ||
|
|
def245f56a | ||
|
|
03ce030e7c | ||
|
|
59097e2466 | ||
|
|
80328e7565 | ||
|
|
2ce161e585 | ||
|
|
33306a5aff | ||
|
|
6e03de5011 | ||
|
|
713281a8ba | ||
|
|
42e3de9e2c | ||
|
|
1e029dbc9a | ||
|
|
c12a6e4bdd | ||
|
|
81bb7456f8 | ||
|
|
26f8d2aa30 | ||
|
|
a8b848d367 |
33
CHANGELOG.md
33
CHANGELOG.md
@ -1,3 +1,36 @@
|
||||
# 1.5.2
|
||||
|
||||
对应后端版本 单体/微服务: 5.5.1/2.5.1
|
||||
|
||||
该版本后端功能值包含一个`同步租户参数配置`功能 旧版本也能升级(使用)
|
||||
|
||||
**REFACTOR**
|
||||
|
||||
- 流程相关代码重构 移除之前的历史代码
|
||||
|
||||
**FEATURES**
|
||||
|
||||
- 修改流程变量
|
||||
- 租户管理 同步租户参数配置
|
||||
|
||||
**BUG FIX**
|
||||
|
||||
- 菜单管理 新增没有加载下拉选择api
|
||||
- v-access:role指令错误判断code而非role
|
||||
- modal/drawer里使用列配置 重置列弹窗被遮挡
|
||||
|
||||
# 1.5.1
|
||||
|
||||
对应后端版本 单体/微服务: 5.5.0/2.5.0
|
||||
|
||||
**STYLE**
|
||||
|
||||
- 拖拽列宽时的颜色与primary保持一致
|
||||
|
||||
**OTHERS**
|
||||
|
||||
- 调整不同环境打包命令 兼容windows系统
|
||||
|
||||
# 1.5.0
|
||||
|
||||
对应后端版本 单体/微服务: 5.5.0/2.5.0
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
|
||||
|
||||
目前对应后端版本: **分布式5.5.0/微服务2.5.0**
|
||||
目前对应后端版本: **分布式5.5.1/微服务2.5.1**
|
||||
|
||||
V1.1.0版本已支持离线图标
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@ -16,7 +16,8 @@
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm vite build",
|
||||
"build:prod": "pnpm vite build",
|
||||
"build:test": "pnpm vite build --mode test",
|
||||
"build:analyze": "pnpm vite build --mode analyze",
|
||||
"dev": "pnpm vite --mode development",
|
||||
"preview": "vite preview",
|
||||
|
||||
@ -125,3 +125,13 @@ export function dictSyncTenant(tenantId?: string) {
|
||||
successMessageMode: 'message',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步租户配置
|
||||
* @returns void
|
||||
*/
|
||||
export function syncTenantConfig() {
|
||||
return requestClient.get<void>('/system/tenant/syncTenantConfig', {
|
||||
successMessageMode: 'message',
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { TaskInfo } from '../task/model';
|
||||
import type { FlowInfoResponse } from './model';
|
||||
import type { FlowInfoResponse, FlowInstanceVariableResp } from './model';
|
||||
|
||||
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
|
||||
|
||||
@ -104,8 +104,8 @@ export function flowInfo(businessId: string) {
|
||||
* @returns Map<string,any>
|
||||
*/
|
||||
export function instanceVariable(instanceId: string) {
|
||||
return requestClient.get<Record<string, any>>(
|
||||
`/workflow/instance/variable/${instanceId}`,
|
||||
return requestClient.get<FlowInstanceVariableResp>(
|
||||
`/workflow/instance/instanceVariable/${instanceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -118,3 +118,22 @@ export function workflowInstanceInvalid(data: {
|
||||
}) {
|
||||
return requestClient.postWithMsg<void>('/workflow/instance/invalid', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改流程参数
|
||||
* @param data 参数
|
||||
* @param data.instanceId 实例ID
|
||||
* @param data.key 参数key
|
||||
* @param data.value 值
|
||||
* @returns void
|
||||
*/
|
||||
export function updateFlowVariable(data: {
|
||||
instanceId: string;
|
||||
key: string;
|
||||
value: any;
|
||||
}) {
|
||||
return requestClient.putWithMsg<void>(
|
||||
'/workflow/instance/updateVariable',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
export {};
|
||||
|
||||
export interface Flow {
|
||||
id: string;
|
||||
createTime: string;
|
||||
@ -39,3 +41,14 @@ export interface FlowInfoResponse {
|
||||
instanceId: string;
|
||||
list: Flow[];
|
||||
}
|
||||
|
||||
export interface FlowInstanceVariableResp {
|
||||
/**
|
||||
* json字符串 流程变量
|
||||
*/
|
||||
variable: string;
|
||||
variableList: {
|
||||
key: string;
|
||||
value: any;
|
||||
}[];
|
||||
}
|
||||
|
||||
27
apps/web-antd/src/components/global/slot.ts
Normal file
27
apps/web-antd/src/components/global/slot.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { defineComponent, h } from 'vue';
|
||||
|
||||
/**
|
||||
* 使用默认插槽来自定义组件
|
||||
* 给vbenForm的components使用
|
||||
*/
|
||||
export const DefaultSlot = defineComponent({
|
||||
name: 'DefaultSlot',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
/**
|
||||
* 绑定到根节点的div上的属性
|
||||
*/
|
||||
rootDivAttrs: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
render() {
|
||||
/**
|
||||
* 获取属性 传递给作用域插槽供外部使用
|
||||
*/
|
||||
const attrs = this.$attrs;
|
||||
|
||||
return h('div', { ...this.rootDivAttrs }, this.$slots.default?.(attrs));
|
||||
},
|
||||
});
|
||||
@ -58,6 +58,9 @@ const content = defineModel<string>('modelValue', {
|
||||
|
||||
const editorRef = shallowRef<EditorType | null>(null);
|
||||
|
||||
// 存储上传图片的 url -> ossId 映射,用于后续附加 data-oss-id
|
||||
const pendingImageMap = new Map<string, string>();
|
||||
|
||||
const { isDark, locale } = usePreferences();
|
||||
const skinName = computed(() => {
|
||||
return isDark.value ? 'oxide-dark' : 'oxide';
|
||||
@ -130,6 +133,11 @@ const initOptions = computed((): InitOptions => {
|
||||
toolbar_mode: 'sliding',
|
||||
// 隐藏下面的 按xxx获取帮助
|
||||
help_accessibility: false,
|
||||
// https://blog.csdn.net/qq_46380656/article/details/122171418
|
||||
// 避免图片地址和链接地址转换成相对路径
|
||||
relative_urls: false,
|
||||
remove_script_host: false,
|
||||
convert_urls: false,
|
||||
...options,
|
||||
/**
|
||||
* 覆盖默认的base64行为
|
||||
@ -151,16 +159,9 @@ const initOptions = computed((): InitOptions => {
|
||||
.then((response) => {
|
||||
const { url, ossId } = response as unknown as UploadResult;
|
||||
console.log('tinymce上传图片:', url);
|
||||
// 将 url -> ossId 映射存储起来,等待图片插入后再附加
|
||||
pendingImageMap.set(url, ossId);
|
||||
resolve(url);
|
||||
// 放在宏队列才能获取
|
||||
setTimeout(() => {
|
||||
const imgDom = editorRef.value?.dom.select(`img[src="${url}"]`);
|
||||
if (imgDom) {
|
||||
editorRef.value?.dom.setAttrib(imgDom, 'data-oss-id', ossId);
|
||||
} else {
|
||||
console.warn('无法获取图片dom, 存储数据可能会出现问题');
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('tinymce上传图片失败:', error);
|
||||
@ -175,6 +176,27 @@ const initOptions = computed((): InitOptions => {
|
||||
emit('mounted');
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
// 监听内容变化,处理待附加 data-oss-id 的图片
|
||||
editor.on('change', () => {
|
||||
if (pendingImageMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingImageMap.forEach((ossId, url) => {
|
||||
const imgDoms = editor.dom.select(`img[src="${url}"]`);
|
||||
if (imgDoms && imgDoms.length > 0) {
|
||||
imgDoms.forEach((imgDom) => {
|
||||
// 只处理还没有 data-oss-id 属性的图片
|
||||
if (!editor.dom.getAttrib(imgDom, 'data-oss-id')) {
|
||||
editor.dom.setAttrib(imgDom, 'data-oss-id', ossId);
|
||||
console.log('已附加 data-oss-id:', url, ossId);
|
||||
}
|
||||
});
|
||||
pendingImageMap.delete(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,215 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
|
||||
import type { DataNode } from 'ant-design-vue/es/tree';
|
||||
import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
|
||||
|
||||
import type { PropType, SetupContext } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, useSlots, watch } from 'vue';
|
||||
|
||||
import { findGroupParentIds, treeToList } from '@vben/utils';
|
||||
import { treeToList } from '@vben/utils';
|
||||
|
||||
import { Checkbox, Tree } from 'ant-design-vue';
|
||||
import { uniq } from 'lodash-es';
|
||||
|
||||
/** 需要禁止透传 */
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
checkStrictly: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
expandAllOnInit: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
fieldNames: {
|
||||
default: () => ({ key: 'id', title: 'label' }),
|
||||
type: Object as PropType<{ key: string; title: string }>,
|
||||
},
|
||||
/** 点击节点关联/独立时 清空已勾选的节点 */
|
||||
resetOnStrictlyChange: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
treeData: {
|
||||
default: () => [],
|
||||
type: Array as PropType<DataNode[]>,
|
||||
},
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
expandAllOnInit: false,
|
||||
fieldNames: () => ({ key: 'id', title: 'label' }),
|
||||
resetOnStrictlyChange: true,
|
||||
treeData: () => [],
|
||||
});
|
||||
const emit = defineEmits<{ checkStrictlyChange: [boolean] }>();
|
||||
|
||||
const expandStatus = ref(false);
|
||||
const selectAllStatus = ref(false);
|
||||
interface Props {
|
||||
/**
|
||||
* 是否展开所有节点 mount
|
||||
*/
|
||||
expandAllOnInit?: boolean;
|
||||
/**
|
||||
* 自定义字段
|
||||
*/
|
||||
fieldNames?: { key: string; title: string };
|
||||
/**
|
||||
* 点击节点关联/独立时 清空已勾选的节点
|
||||
*/
|
||||
resetOnStrictlyChange?: boolean;
|
||||
/**
|
||||
* 树结构数据
|
||||
*/
|
||||
treeData?: DataNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台的这个字段跟antd/ele是反的
|
||||
* 组件库这个字段代表不关联
|
||||
* 后台这个代表关联
|
||||
* 展开的状态
|
||||
*/
|
||||
const innerCheckedStrictly = computed(() => {
|
||||
return !props.checkStrictly;
|
||||
});
|
||||
const expandStatus = ref(false);
|
||||
/**
|
||||
* 全选状态
|
||||
*/
|
||||
const selectAllStatus = ref(false);
|
||||
|
||||
const associationText = computed(() => {
|
||||
return props.checkStrictly ? '父子节点关联' : '父子节点独立';
|
||||
return checkStrictly.value ? '父子节点关联' : '父子节点独立';
|
||||
});
|
||||
|
||||
/**
|
||||
* 这个只用于界面显示
|
||||
* 关联情况下 只会有最末尾的节点被选中
|
||||
*/
|
||||
const checkedKeys = defineModel('value', {
|
||||
const checkedKeys = defineModel<(number | string)[]>('value', {
|
||||
default: () => [],
|
||||
type: Array as PropType<(number | string)[]>,
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否节点关联 后端字段跟前端字段是反的
|
||||
*/
|
||||
const checkStrictly = defineModel<boolean>('checkStrictly', {
|
||||
default: () => true,
|
||||
});
|
||||
|
||||
const computedCheckedKeys = computed<any>({
|
||||
get() {
|
||||
/**
|
||||
* 严格模式(节点不关联) 需要返回{checked: string[] | number[], halfChecked: string[]}
|
||||
* @see https://www.antdv.com/components/tree-cn#tree-props
|
||||
*/
|
||||
if (!checkStrictly.value) {
|
||||
return {
|
||||
checked: [...checkedKeys.value],
|
||||
halfChecked: [],
|
||||
};
|
||||
}
|
||||
return checkedKeys.value;
|
||||
},
|
||||
set(v) {
|
||||
if (!checkStrictly.value) {
|
||||
checkedKeys.value = [...v.checked, ...v.halfChecked];
|
||||
return;
|
||||
}
|
||||
checkedKeys.value = v;
|
||||
},
|
||||
});
|
||||
|
||||
// 所有节点的ID
|
||||
const allKeys = computed(() => {
|
||||
const idField = props.fieldNames.key;
|
||||
return treeToList(props.treeData).map((item: any) => item[idField]);
|
||||
});
|
||||
|
||||
/** 已经选择的所有节点 包括子/父节点 用于提交 */
|
||||
const checkedRealKeys = ref<(number | string)[]>([]);
|
||||
|
||||
/**
|
||||
* 取第一次的menuTree id 设置到checkedMenuKeys
|
||||
* 主要为了解决没有任何修改 直接点击保存的情况
|
||||
*
|
||||
* length为0情况(即新增时候没有勾选节点) 勾选这里会延迟触发 节点会拼接上父节点 导致ID重复
|
||||
*/
|
||||
const stop = watch([checkedKeys, () => props.treeData], () => {
|
||||
if (
|
||||
props.checkStrictly &&
|
||||
checkedKeys.value.length > 0 &&
|
||||
props.treeData.length > 0
|
||||
) {
|
||||
/** 找到父节点 添加上 */
|
||||
const parentIds = findGroupParentIds(
|
||||
props.treeData,
|
||||
checkedKeys.value as any,
|
||||
{ id: props.fieldNames.key },
|
||||
);
|
||||
/**
|
||||
* uniq 解决上面的id重复问题
|
||||
*/
|
||||
checkedRealKeys.value = uniq([...parentIds, ...checkedKeys.value]);
|
||||
stop();
|
||||
}
|
||||
if (!props.checkStrictly && checkedKeys.value.length > 0) {
|
||||
/** 节点独立 这里是全部的节点 */
|
||||
checkedRealKeys.value = checkedKeys.value;
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param checkedStateKeys 已经选中的子节点的ID
|
||||
* @param info info.halfCheckedKeys为父节点的ID
|
||||
*/
|
||||
type CheckedState<T = number | string> =
|
||||
| T[]
|
||||
| { checked: T[]; halfChecked: T[] };
|
||||
function handleChecked(checkedStateKeys: CheckedState, info: CheckInfo) {
|
||||
// 数组的话为节点关联
|
||||
if (Array.isArray(checkedStateKeys)) {
|
||||
const halfCheckedKeys: number[] = (info.halfCheckedKeys || []) as number[];
|
||||
checkedRealKeys.value = [...halfCheckedKeys, ...checkedStateKeys];
|
||||
} else {
|
||||
checkedRealKeys.value = [...checkedStateKeys.checked];
|
||||
// fix: Invalid prop: type check failed for prop "value". Expected Array, got Object
|
||||
checkedKeys.value = [...checkedStateKeys.checked];
|
||||
}
|
||||
}
|
||||
|
||||
function handleExpandChange(e: CheckboxChangeEvent) {
|
||||
function handleCheckedAllChange(e: CheckboxChangeEvent) {
|
||||
// 这个用于展示
|
||||
checkedKeys.value = e.target.checked ? allKeys.value : [];
|
||||
// 这个用于提交
|
||||
checkedRealKeys.value = e.target.checked ? allKeys.value : [];
|
||||
}
|
||||
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
function handleExpandOrCollapseAll(e: CheckboxChangeEvent) {
|
||||
const expand = e.target.checked;
|
||||
expandedKeys.value = expand ? allKeys.value : [];
|
||||
function handleExpandOrCollapseAll() {
|
||||
expandStatus.value = !expandStatus.value;
|
||||
expandedKeys.value = expandStatus.value ? allKeys.value : [];
|
||||
}
|
||||
|
||||
function handleCheckStrictlyChange(e: CheckboxChangeEvent) {
|
||||
emit('checkStrictlyChange', e.target.checked);
|
||||
function handleCheckStrictlyChange() {
|
||||
if (props.resetOnStrictlyChange) {
|
||||
checkedKeys.value = [];
|
||||
checkedRealKeys.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露方法来获取用于提交的全部节点
|
||||
* uniq去重(保险方案)
|
||||
*/
|
||||
defineExpose({
|
||||
getCheckedKeys: () => uniq(checkedRealKeys.value),
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.expandAllOnInit) {
|
||||
await nextTick();
|
||||
expandedKeys.value = allKeys.value;
|
||||
}
|
||||
});
|
||||
|
||||
const slots = useSlots() as SetupContext['slots'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-background w-full rounded-lg border-[1px] p-[12px]">
|
||||
<!-- <div class="flex flex-col gap-6 text-[13px]">
|
||||
<div>computedCheckedKeys {{ computedCheckedKeys }}</div>
|
||||
<div>checkedKeys {{ checkedKeys }}</div>
|
||||
</div> -->
|
||||
|
||||
<div class="flex items-center justify-between gap-2 border-b-[1px] pb-2">
|
||||
<div>
|
||||
<div class="opacity-75">
|
||||
<span>节点状态: </span>
|
||||
<span :class="[props.checkStrictly ? 'text-primary' : 'text-red-500']">
|
||||
<span :class="[checkStrictly ? 'text-primary' : 'text-red-500']">
|
||||
{{ associationText }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
已选中
|
||||
<span class="text-primary mx-1 font-semibold">
|
||||
{{ checkedRealKeys.length }}
|
||||
</span>
|
||||
个节点
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between border-b-[1px] py-2"
|
||||
>
|
||||
<Checkbox
|
||||
v-model:checked="expandStatus"
|
||||
@change="handleExpandOrCollapseAll"
|
||||
>
|
||||
<a-button size="small" @click="handleExpandOrCollapseAll">
|
||||
展开/折叠全部
|
||||
</Checkbox>
|
||||
<Checkbox v-model:checked="selectAllStatus" @change="handleExpandChange">
|
||||
</a-button>
|
||||
<Checkbox
|
||||
v-model:checked="selectAllStatus"
|
||||
@change="handleCheckedAllChange"
|
||||
>
|
||||
全选/取消全选
|
||||
</Checkbox>
|
||||
<Checkbox :checked="checkStrictly" @change="handleCheckStrictlyChange">
|
||||
<Checkbox
|
||||
v-model:checked="checkStrictly"
|
||||
@change="handleCheckStrictlyChange"
|
||||
>
|
||||
父子节点关联
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<Tree
|
||||
v-if="treeData.length > 0"
|
||||
v-model:check-strictly="innerCheckedStrictly"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
:check-strictly="!checkStrictly"
|
||||
v-model:checked-keys="computedCheckedKeys"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
:checkable="true"
|
||||
:field-names="fieldNames"
|
||||
:selectable="false"
|
||||
:tree-data="treeData"
|
||||
@check="handleChecked"
|
||||
>
|
||||
<template
|
||||
v-for="slotName in Object.keys(slots)"
|
||||
v-for="slotName in Object.keys($slots)"
|
||||
:key="slotName"
|
||||
#[slotName]="data"
|
||||
>
|
||||
@ -219,3 +174,20 @@ const slots = useSlots() as SetupContext['slots'];
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.ant-tree) {
|
||||
// 勾选框居中
|
||||
& .ant-tree-checkbox {
|
||||
margin: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
// 展开图标居中
|
||||
& .ant-tree-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -113,7 +113,7 @@ Upload.Dragger只会影响样式
|
||||
scope="global"
|
||||
keypath="component.upload.uploadHelpMessage"
|
||||
tag="div"
|
||||
class="mt-2 text-[14px] leading-[1.5] text-black/45"
|
||||
class="mt-2 text-[14px] leading-[1.5] text-black/45 dark:text-white/45"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
<template #size>
|
||||
|
||||
@ -119,7 +119,7 @@ function currentPreview(file: UploadFile) {
|
||||
scope="global"
|
||||
keypath="component.upload.uploadHelpMessage"
|
||||
tag="div"
|
||||
class="text-[14px] leading-[1.5] text-black/45"
|
||||
class="text-[14px] leading-[1.5] text-black/45 dark:text-white/45"
|
||||
:class="{
|
||||
'upload-text__disabled': disabled,
|
||||
'mt-2': listType !== 'picture-card',
|
||||
|
||||
@ -112,11 +112,16 @@ function handleViewAll() {
|
||||
message.warning('暂未开放');
|
||||
}
|
||||
watch(
|
||||
() => preferences.app.watermark,
|
||||
async (enable) => {
|
||||
() => ({
|
||||
enable: preferences.app.watermark,
|
||||
content: preferences.app.watermarkContent,
|
||||
}),
|
||||
async ({ enable, content }) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||
content:
|
||||
content ||
|
||||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
|
||||
@ -115,9 +115,14 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
|
||||
if (id) {
|
||||
await formApi.setFieldValue('parentId', id);
|
||||
if (update) {
|
||||
// 没有依赖关系 同时加载
|
||||
const [record] = await Promise.all([menuInfo(id), setupMenuSelect()]);
|
||||
// 创建元组(不是数组 元素位置固定)
|
||||
const promise = [
|
||||
update ? menuInfo(id) : null,
|
||||
setupMenuSelect(),
|
||||
] as const;
|
||||
// 并行获取菜单树选择和菜单信息
|
||||
const [record] = await Promise.all(promise);
|
||||
if (record) {
|
||||
await formApi.setValues(record);
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -97,11 +97,19 @@ async function handleClosed() {
|
||||
<BasicForm>
|
||||
<template #tip>
|
||||
<div class="ml-7 w-full">
|
||||
<Alert
|
||||
message="私有桶使用自定义域名无法预览, 但可以正常上传/下载"
|
||||
show-icon
|
||||
type="warning"
|
||||
/>
|
||||
<Alert show-icon type="warning">
|
||||
<template #message>
|
||||
私有桶(minio)使用自定义域名需要参考
|
||||
<a
|
||||
href="https://gitee.com/dromara/RuoYi-Vue-Plus/issues/IBQIKC"
|
||||
target="_blank"
|
||||
class="text-primary"
|
||||
>
|
||||
支持minio预览私有桶
|
||||
</a>
|
||||
, 否则无法预览
|
||||
</template>
|
||||
</Alert>
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
import { DefaultSlot } from '#/components/global/slot';
|
||||
import { TreeSelectPanel } from '#/components/tree';
|
||||
import { getDictOptions } from '#/utils/dict';
|
||||
|
||||
/**
|
||||
@ -177,15 +181,6 @@ export const authModalSchemas: FormSchemaGetter = () => [
|
||||
fieldName: 'roleId',
|
||||
label: '角色ID',
|
||||
},
|
||||
{
|
||||
component: 'Radio',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'deptCheckStrictly',
|
||||
label: 'deptCheckStrictly',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
@ -214,12 +209,39 @@ export const authModalSchemas: FormSchemaGetter = () => [
|
||||
label: '权限范围',
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
component: 'Radio',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'deptCheckStrictly',
|
||||
label: 'deptCheckStrictly',
|
||||
},
|
||||
{
|
||||
// 这种的场景基本上是一个组件需要绑定两个或以上的场景
|
||||
component: markRaw(DefaultSlot),
|
||||
defaultValue: [],
|
||||
componentProps: {
|
||||
rootDivAttrs: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
show: (values) => values.dataScope === '2',
|
||||
triggerFields: ['dataScope'],
|
||||
},
|
||||
renderComponentContent: (model) => ({
|
||||
default: (attrs: any) => {
|
||||
return (
|
||||
<TreeSelectPanel
|
||||
expand-all-on-init={true}
|
||||
treeData={attrs.treeData}
|
||||
v-model:checkStrictly={model.deptCheckStrictly}
|
||||
v-model:value={model.deptIds}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
fieldName: 'deptIds',
|
||||
help: '更改后立即生效',
|
||||
label: '部门权限',
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { DeptOption } from '#/api/system/role/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
import { cloneDeep, findGroupParentIds } from '@vben/utils';
|
||||
|
||||
import { uniq } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { roleDataScope, roleDeptTree, roleInfo } from '#/api/system/role';
|
||||
import { TreeSelectPanel } from '#/components/tree';
|
||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||
|
||||
import { authModalSchemas } from './data';
|
||||
@ -26,26 +25,32 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const deptTree = ref<DeptOption[]>([]);
|
||||
/**
|
||||
* 保存部门数据 用于获取祖先节点
|
||||
*/
|
||||
let treeData: DeptOption[] = [];
|
||||
async function setupDeptTree(id: number | string) {
|
||||
const resp = await roleDeptTree(id);
|
||||
formApi.setFieldValue('deptIds', resp.checkedKeys);
|
||||
// 设置菜单信息
|
||||
deptTree.value = resp.depts;
|
||||
}
|
||||
const { checkedKeys, depts } = resp;
|
||||
|
||||
async function customFormValueGetter() {
|
||||
const v = await defaultFormValueGetter(formApi)();
|
||||
// 获取勾选信息
|
||||
const menuIds = deptSelectRef.value?.[0]?.getCheckedKeys() ?? [];
|
||||
const mixStr = v + menuIds.join(',');
|
||||
return mixStr;
|
||||
/**
|
||||
* 设置部门树数据
|
||||
*/
|
||||
formApi.updateSchema([
|
||||
{ fieldName: 'deptIds', componentProps: { treeData: depts } },
|
||||
]);
|
||||
/**
|
||||
* 设置选中 必须先传递treeData
|
||||
* Note: Tree missing follow keys: '1981565541727186945'
|
||||
*/
|
||||
await formApi.setFieldValue('deptIds', checkedKeys);
|
||||
treeData = depts;
|
||||
}
|
||||
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
{
|
||||
initializedGetter: customFormValueGetter,
|
||||
currentGetter: customFormValueGetter,
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
},
|
||||
);
|
||||
|
||||
@ -56,14 +61,14 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
onConfirm: handleConfirm,
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
treeData = [];
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { id } = modalApi.getData() as { id: number | string };
|
||||
|
||||
setupDeptTree(id);
|
||||
const record = await roleInfo(id);
|
||||
const [record] = await Promise.all([roleInfo(id), setupDeptTree(id)]);
|
||||
await formApi.setValues(record);
|
||||
markInitialized();
|
||||
|
||||
@ -71,11 +76,6 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 这里拿到的是一个数组ref
|
||||
*/
|
||||
const deptSelectRef = ref();
|
||||
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
modalApi.lock(true);
|
||||
@ -87,7 +87,15 @@ async function handleConfirm() {
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
// 不为自定义权限的话 删除部门id
|
||||
if (data.dataScope === '2') {
|
||||
const deptIds = deptSelectRef.value?.[0]?.getCheckedKeys() ?? [];
|
||||
let { deptIds, deptCheckStrictly } = data;
|
||||
// 节点关联 需要拼接上祖级ID(获取的是不带的)
|
||||
if (deptCheckStrictly) {
|
||||
// 找到所有父级ID
|
||||
const parentIds = findGroupParentIds(treeData, deptIds, { id: 'id' });
|
||||
// 去重
|
||||
deptIds = uniq([...parentIds, ...deptIds]);
|
||||
}
|
||||
// 赋值
|
||||
data.deptIds = deptIds;
|
||||
} else {
|
||||
data.deptIds = [];
|
||||
@ -107,29 +115,10 @@ async function handleClosed() {
|
||||
await formApi.resetForm();
|
||||
resetInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过回调更新 无法通过v-model
|
||||
* @param value 菜单选择是否严格模式
|
||||
*/
|
||||
function handleCheckStrictlyChange(value: boolean) {
|
||||
formApi.setFieldValue('deptCheckStrictly', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal class="min-h-[600px] w-[550px]" title="分配权限">
|
||||
<BasicForm>
|
||||
<template #deptIds="slotProps">
|
||||
<TreeSelectPanel
|
||||
ref="deptSelectRef"
|
||||
v-bind="slotProps"
|
||||
:check-strictly="formApi.form.values.deptCheckStrictly"
|
||||
:expand-all-on-init="true"
|
||||
:tree-data="deptTree"
|
||||
@check-strictly-change="handleCheckStrictlyChange"
|
||||
/>
|
||||
</template>
|
||||
</BasicForm>
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
@ -15,6 +15,7 @@ import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
dictSyncTenant,
|
||||
syncTenantConfig,
|
||||
tenantExport,
|
||||
tenantList,
|
||||
tenantRemove,
|
||||
@ -144,6 +145,18 @@ function handleSyncTenantDict() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleSyncTenantConfig() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
iconType: 'warning',
|
||||
content: '确认同步租户参数配置?',
|
||||
onOk: async () => {
|
||||
await syncTenantConfig();
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -157,6 +170,12 @@ function handleSyncTenantDict() {
|
||||
>
|
||||
同步租户字典
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['system:tenant:edit']"
|
||||
@click="handleSyncTenantConfig"
|
||||
>
|
||||
同步租户参数配置
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['system:tenant:export']"
|
||||
@click="handleDownloadExcel"
|
||||
|
||||
@ -0,0 +1,420 @@
|
||||
<script setup lang="ts">
|
||||
import type { ApprovalType } from '../type';
|
||||
|
||||
import type { User } from '#/api/core/user';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, h } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cn, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CheckOutlined,
|
||||
EditOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
MenuOutlined,
|
||||
RollbackOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UsergroupDeleteOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { Dropdown, Menu, MenuItem, Modal, Space } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
cancelProcessApply,
|
||||
deleteByInstanceIds,
|
||||
} from '#/api/workflow/instance';
|
||||
import {
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
updateAssignee,
|
||||
} from '#/api/workflow/task';
|
||||
|
||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '..';
|
||||
import { approveWithReasonModal } from '../helper';
|
||||
import userSelectModal from '../user-select-modal.vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 行数据的taskInfo?
|
||||
*/
|
||||
task?: TaskInfo;
|
||||
/**
|
||||
* 审批类型 根据不同类型显示按钮
|
||||
*/
|
||||
type: ApprovalType;
|
||||
/**
|
||||
* 为审批类型时候 显示的按钮(按钮权限)
|
||||
*/
|
||||
buttonPermissions: Record<string, boolean>;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
reload: [];
|
||||
}>();
|
||||
|
||||
// 是否显示 `其他` 按钮
|
||||
const showButtonOther = computed(() => {
|
||||
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
|
||||
return Object.keys(props.buttonPermissions).some(
|
||||
(key) => moreCollections.has(key) && props.buttonPermissions[key],
|
||||
);
|
||||
});
|
||||
|
||||
// 进行中 可以撤销
|
||||
const revocable = computed(() => props.task?.flowStatus === 'waiting');
|
||||
async function handleCancel() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要撤销该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await cancelProcessApply({
|
||||
businessId: props.task!.businessId,
|
||||
message: '申请人撤销流程!',
|
||||
});
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可编辑/删除
|
||||
*/
|
||||
const editableAndRemoveable = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
return ['back', 'cancel', 'draft'].includes(props.task.flowStatus);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function handleEdit() {
|
||||
const path = props.task?.formPath;
|
||||
if (path) {
|
||||
router.push({ path, query: { id: props.task!.businessId } });
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定删除该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await deleteByInstanceIds([props.task!.id]);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
const [RejectionModal, rejectionModalApi] = useVbenModal({
|
||||
connectedComponent: approvalRejectionModal,
|
||||
});
|
||||
function handleRejection() {
|
||||
rejectionModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
definitionId: props.task?.definitionId,
|
||||
nodeCode: props.task?.nodeCode,
|
||||
});
|
||||
rejectionModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
approveWithReasonModal({
|
||||
title: '审批终止',
|
||||
description: '确定终止当前审批流程吗?',
|
||||
onOk: async (reason) => {
|
||||
await terminationTask({ taskId: props.task!.id, comment: reason });
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
const [ApprovalModal, approvalModalApi] = useVbenModal({
|
||||
connectedComponent: approvalModal,
|
||||
});
|
||||
function handleApproval() {
|
||||
const { buttonPermissions } = props;
|
||||
// 是否具有抄送权限
|
||||
const copyPermission = buttonPermissions?.copy ?? false;
|
||||
// 是否具有选人权限
|
||||
const assignPermission = buttonPermissions?.pop ?? false;
|
||||
approvalModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
copyPermission,
|
||||
assignPermission,
|
||||
});
|
||||
approvalModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 委托
|
||||
*/
|
||||
const [DelegationModal, delegationModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleDelegation(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '委托',
|
||||
description: `确定委托给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'delegateTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '转办',
|
||||
description: `确定转办给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'transferTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation({ taskId: props.task!.id, userIds }, 'addSignature');
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 流程干预
|
||||
const [FlowInterfereModal, flowInterfereModalApi] = useVbenModal({
|
||||
connectedComponent: flowInterfereModal,
|
||||
});
|
||||
function handleFlowInterfere() {
|
||||
flowInterfereModalApi.setData({ taskId: props.task?.id });
|
||||
flowInterfereModalApi.open();
|
||||
}
|
||||
|
||||
// 修改办理人
|
||||
const [UpdateAssigneeModal, updateAssigneeModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleUpdateAssignee(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
if (!current) return;
|
||||
Modal.confirm({
|
||||
title: '修改办理人',
|
||||
content: `确定修改办理人为${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await updateAssignee([props.task!.id], current.userId);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
if (Number(props.task.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'absolute bottom-0 left-0',
|
||||
'border-t-solid border-t-[1px]',
|
||||
'bg-background w-full p-3',
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space v-if="type === 'myself'">
|
||||
<a-button
|
||||
v-if="revocable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(RollbackOutlined)"
|
||||
@click="handleCancel"
|
||||
>
|
||||
撤销申请
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
v-if="editableAndRemoveable"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleEdit"
|
||||
>
|
||||
重新编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="editableAndRemoveable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleRemove"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</Space>
|
||||
<Space v-if="type === 'approve'">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
:icon="h(CheckOutlined)"
|
||||
@click="handleApproval"
|
||||
>
|
||||
通过
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.termination"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ExclamationCircleOutlined)"
|
||||
@click="handleTermination"
|
||||
>
|
||||
终止
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.back"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ArrowLeftOutlined)"
|
||||
@click="handleRejection"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
<Dropdown
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.trust"
|
||||
key="1"
|
||||
@click="() => delegationModalApi.open()"
|
||||
>
|
||||
<UserOutlined class="mr-2" />委托
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.transfer"
|
||||
key="2"
|
||||
@click="() => transferModalApi.open()"
|
||||
>
|
||||
<RollbackOutlined class="mr-2" /> 转办
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.addSign"
|
||||
key="3"
|
||||
@click="() => addSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupAddOutlined class="mr-2" /> 加签
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.subSign"
|
||||
key="4"
|
||||
@click="() => reductionSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupDeleteOutlined class="mr-2" /> 减签
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button v-if="showButtonOther" :icon="h(MenuOutlined)">
|
||||
其他
|
||||
</a-button>
|
||||
</Dropdown>
|
||||
<ApprovalModal @complete="$emit('reload')" />
|
||||
<RejectionModal @complete="$emit('reload')" />
|
||||
<DelegationModal mode="single" @finish="handleDelegation" />
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
</Space>
|
||||
<Space v-if="type === 'admin'">
|
||||
<a-button @click="handleFlowInterfere"> 流程干预 </a-button>
|
||||
<a-button @click="() => updateAssigneeModalApi.open()">
|
||||
修改办理人
|
||||
</a-button>
|
||||
<FlowInterfereModal @complete="$emit('reload')" />
|
||||
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1 @@
|
||||
export { default as FlowActions } from './flow-actions.vue';
|
||||
@ -43,6 +43,8 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { taskId } = modalApi.getData() as ModalProps;
|
||||
|
||||
// 查询是否有按钮权限
|
||||
@ -63,6 +65,8 @@ const [BasicModal, modalApi] = useVbenModal({
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
@ -108,6 +112,11 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
component: 'Input',
|
||||
defaultValue: [],
|
||||
label: '抄送人',
|
||||
// 默认不显示
|
||||
dependencies: {
|
||||
if: false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
|
||||
@ -20,8 +20,6 @@ defineOptions({
|
||||
|
||||
defineProps<{
|
||||
currentFlowInfo: FlowInfoResponse;
|
||||
iframeHeight: number;
|
||||
iframeLoaded: boolean;
|
||||
task: TaskInfo;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@ -1,117 +1,72 @@
|
||||
<!-- 该文件需要重构 但我没空 -->
|
||||
<!--
|
||||
TODO: 优化项
|
||||
会先加载流程信息 再加载业务表单信息
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/core/user';
|
||||
import type { ApprovalType } from './type';
|
||||
|
||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, h, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Fallback, useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
import { Fallback, VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { cn, getPopupContainer } from '@vben/utils';
|
||||
import { cn } from '@vben/utils';
|
||||
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
EditOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
MenuOutlined,
|
||||
RollbackOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UsergroupDeleteOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { useClipboard, useEventListener } from '@vueuse/core';
|
||||
import {
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItem,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
TabPane,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { CopyOutlined } from '@ant-design/icons-vue';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Card, Divider, message, TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
cancelProcessApply,
|
||||
deleteByInstanceIds,
|
||||
flowInfo,
|
||||
} from '#/api/workflow/instance';
|
||||
import {
|
||||
getTaskByTaskId,
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
updateAssignee,
|
||||
} from '#/api/workflow/task';
|
||||
import { flowInfo } from '#/api/workflow/instance';
|
||||
import { getTaskByTaskId } from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
|
||||
import { FlowActions } from './actions';
|
||||
import ApprovalDetails from './approval-details.vue';
|
||||
import FlowPreview from './flow-preview.vue';
|
||||
import { approveWithReasonModal } from './helper';
|
||||
import userSelectModal from './user-select-modal.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalPanel',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<{ task?: TaskInfo; type: ApprovalType }>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
/**
|
||||
* 下面按钮点击后会触发的事件
|
||||
*/
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
defineEmits<{ reload: [] }>();
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 行数据(list)的info
|
||||
*/
|
||||
task?: TaskInfo;
|
||||
/**
|
||||
* 审批类型
|
||||
*/
|
||||
type: ApprovalType;
|
||||
}
|
||||
|
||||
const currentTask = ref<TaskInfo>();
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
* 目前的作用只为了获取按钮权限 因为list接口(行数据)获取为空
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!currentTask.value) {
|
||||
return false;
|
||||
}
|
||||
if (Number(currentTask.value.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const onlyForBtnPermissionTask = ref<TaskInfo>();
|
||||
/**
|
||||
* 按钮权限
|
||||
*/
|
||||
const buttonPermissions = computed(() => {
|
||||
const record: Record<string, boolean> = {};
|
||||
if (!currentTask.value) {
|
||||
if (!onlyForBtnPermissionTask.value) {
|
||||
return record;
|
||||
}
|
||||
currentTask.value.buttonList.forEach((item) => {
|
||||
onlyForBtnPermissionTask.value.buttonList.forEach((item) => {
|
||||
record[item.code] = item.show;
|
||||
});
|
||||
return record;
|
||||
});
|
||||
|
||||
// 是否显示 `其他` 按钮
|
||||
const showButtonOther = computed(() => {
|
||||
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
|
||||
return Object.keys(buttonPermissions.value).some(
|
||||
(key) => moreCollections.has(key) && buttonPermissions.value[key],
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* myself 我发起的
|
||||
* readonly 只读 只用于查看
|
||||
* approve 审批
|
||||
* admin 流程监控 - 待办任务使用
|
||||
*/
|
||||
type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
||||
const showFooter = computed(() => {
|
||||
if (props.type === 'readonly') {
|
||||
return false;
|
||||
@ -131,36 +86,34 @@ const currentFlowInfo = ref<FlowInfoResponse>();
|
||||
* card的loading状态
|
||||
*/
|
||||
const loading = ref(false);
|
||||
const iframeLoaded = ref(false);
|
||||
const iframeHeight = ref(300);
|
||||
useEventListener('message', (event) => {
|
||||
const data = event.data as { [key: string]: any; type: string };
|
||||
if (!isObject(data)) return;
|
||||
/**
|
||||
* iframe通信 加载完毕后才显示表单 解决卡顿问题
|
||||
*/
|
||||
if (data.type === 'mounted') {
|
||||
iframeLoaded.value = true;
|
||||
}
|
||||
/**
|
||||
* 高度与表单高度保持一致
|
||||
*/
|
||||
if (data.type === 'height') {
|
||||
const height = data.height;
|
||||
iframeHeight.value = height;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleLoadInfo(task: TaskInfo | undefined) {
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (!task) return null;
|
||||
loading.value = true;
|
||||
iframeLoaded.value = false;
|
||||
const resp = await flowInfo(task.businessId);
|
||||
currentFlowInfo.value = resp;
|
||||
|
||||
const taskResp = await getTaskByTaskId(props.task!.id);
|
||||
currentTask.value = taskResp;
|
||||
/**
|
||||
* 不为审批不需要调用`getTaskByTaskId`接口
|
||||
*/
|
||||
if (props.type !== 'approve') {
|
||||
const flowResp = await flowInfo(task.businessId);
|
||||
currentFlowInfo.value = flowResp;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* getTaskByTaskId主要为了获取按钮权限 目前没有其他功能
|
||||
* 行数据(即props.task)获取的是没有按钮权限的
|
||||
*/
|
||||
const [flowResp, taskResp] = await Promise.all([
|
||||
flowInfo(task.businessId),
|
||||
getTaskByTaskId(task.id),
|
||||
]);
|
||||
|
||||
currentFlowInfo.value = flowResp;
|
||||
onlyForBtnPermissionTask.value = taskResp;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
@ -170,217 +123,6 @@ async function handleLoadInfo(task: TaskInfo | undefined) {
|
||||
|
||||
watch(() => props.task, handleLoadInfo);
|
||||
|
||||
onUnmounted(() => (currentFlowInfo.value = undefined));
|
||||
|
||||
// 进行中 可以撤销
|
||||
const revocable = computed(() => props.task?.flowStatus === 'waiting');
|
||||
async function handleCancel() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要撤销该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await cancelProcessApply({
|
||||
businessId: props.task!.businessId,
|
||||
message: '申请人撤销流程!',
|
||||
});
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可编辑/删除
|
||||
*/
|
||||
const editableAndRemoveable = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
return ['back', 'cancel', 'draft'].includes(props.task.flowStatus);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function handleEdit() {
|
||||
const path = props.task?.formPath;
|
||||
if (path) {
|
||||
router.push({ path, query: { id: props.task!.businessId } });
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定删除该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await deleteByInstanceIds([props.task!.id]);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
const [RejectionModal, rejectionModalApi] = useVbenModal({
|
||||
connectedComponent: approvalRejectionModal,
|
||||
});
|
||||
function handleRejection() {
|
||||
rejectionModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
definitionId: props.task?.definitionId,
|
||||
nodeCode: props.task?.nodeCode,
|
||||
});
|
||||
rejectionModalApi.open();
|
||||
}
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
approveWithReasonModal({
|
||||
title: '审批终止',
|
||||
description: '确定终止当前审批流程吗?',
|
||||
onOk: async (reason) => {
|
||||
await terminationTask({ taskId: props.task!.id, comment: reason });
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
const [ApprovalModal, approvalModalApi] = useVbenModal({
|
||||
connectedComponent: approvalModal,
|
||||
});
|
||||
function handleApproval() {
|
||||
// 是否具有抄送权限
|
||||
const copyPermission = buttonPermissions.value?.copy ?? false;
|
||||
// 是否具有选人权限
|
||||
const assignPermission = buttonPermissions.value?.pop ?? false;
|
||||
approvalModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
copyPermission,
|
||||
assignPermission,
|
||||
});
|
||||
approvalModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 1提取公共函数 2原版是可以填写意见的(message参数)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 委托
|
||||
*/
|
||||
const [DelegationModal, delegationModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleDelegation(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '委托',
|
||||
description: `确定委托给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'delegateTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '转办',
|
||||
description: `确定转办给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'transferTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation({ taskId: props.task!.id, userIds }, 'addSignature');
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 流程干预
|
||||
const [FlowInterfereModal, flowInterfereModalApi] = useVbenModal({
|
||||
connectedComponent: flowInterfereModal,
|
||||
});
|
||||
function handleFlowInterfere() {
|
||||
flowInterfereModalApi.setData({ taskId: props.task?.id });
|
||||
flowInterfereModalApi.open();
|
||||
}
|
||||
|
||||
// 修改办理人
|
||||
const [UpdateAssigneeModal, updateAssigneeModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleUpdateAssignee(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
if (!current) return;
|
||||
Modal.confirm({
|
||||
title: '修改办理人',
|
||||
content: `确定修改办理人为${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await updateAssignee([props.task!.id], current.userId);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 不加legacy在本地开发没有问题
|
||||
* 打包后在一些设备会无法复制 使用legacy来保证兼容性
|
||||
@ -407,6 +149,7 @@ async function handleCopy(text: string) {
|
||||
<CopyOutlined class="cursor-pointer" @click="handleCopy(task.id)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-button size="small" @click="() => handleLoadInfo(task)">
|
||||
<div class="flex items-center justify-center">
|
||||
@ -414,6 +157,7 @@ async function handleCopy(text: string) {
|
||||
</div>
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-5 p-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
@ -426,19 +170,24 @@ async function handleCopy(text: string) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<VbenAvatar
|
||||
:alt="task?.createByName ?? ''"
|
||||
class="bg-primary size-[28px] rounded-full text-white"
|
||||
src=""
|
||||
/>
|
||||
|
||||
<span>{{ task.createByName }}</span>
|
||||
|
||||
<div class="flex items-center opacity-50">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[bxs--category-alt] size-[16px]"></span>
|
||||
流程分类: {{ task.categoryName }}
|
||||
</div>
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||
提交时间: {{ task.createTime }}
|
||||
@ -446,154 +195,32 @@ async function handleCopy(text: string) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs v-if="currentFlowInfo" class="flex-1">
|
||||
<TabPane key="1" tab="审批详情">
|
||||
<ApprovalDetails
|
||||
:current-flow-info="currentFlowInfo"
|
||||
:iframe-loaded="iframeLoaded"
|
||||
:iframe-height="iframeHeight"
|
||||
:task="task"
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<TabPane key="2" tab="审批流程图">
|
||||
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<!-- 固定底部 -->
|
||||
|
||||
<!-- 固定底部 占位高度 -->
|
||||
<div class="h-[58px]"></div>
|
||||
<div
|
||||
<FlowActions
|
||||
v-if="showFooter"
|
||||
:class="
|
||||
cn(
|
||||
'absolute bottom-0 left-0',
|
||||
'border-t-solid border-t-[1px]',
|
||||
'bg-background w-full p-3',
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space v-if="type === 'myself'">
|
||||
<a-button
|
||||
v-if="revocable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(RollbackOutlined)"
|
||||
@click="handleCancel"
|
||||
>
|
||||
撤销申请
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
v-if="editableAndRemoveable"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleEdit"
|
||||
>
|
||||
重新编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="editableAndRemoveable"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(EditOutlined)"
|
||||
@click="handleRemove"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</Space>
|
||||
<Space v-if="type === 'approve'">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
:icon="h(CheckOutlined)"
|
||||
@click="handleApproval"
|
||||
>
|
||||
通过
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.termination"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ExclamationCircleOutlined)"
|
||||
@click="handleTermination"
|
||||
>
|
||||
终止
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="buttonPermissions?.back"
|
||||
danger
|
||||
ghost
|
||||
type="primary"
|
||||
:icon="h(ArrowLeftOutlined)"
|
||||
@click="handleRejection"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
<Dropdown
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.trust"
|
||||
key="1"
|
||||
@click="() => delegationModalApi.open()"
|
||||
>
|
||||
<UserOutlined class="mr-2" />委托
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="buttonPermissions?.transfer"
|
||||
key="2"
|
||||
@click="() => transferModalApi.open()"
|
||||
>
|
||||
<RollbackOutlined class="mr-2" /> 转办
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.addSign"
|
||||
key="3"
|
||||
@click="() => addSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupAddOutlined class="mr-2" /> 加签
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions && buttonPermissions?.subSign"
|
||||
key="4"
|
||||
@click="() => reductionSignatureModalApi.open()"
|
||||
>
|
||||
<UsergroupDeleteOutlined class="mr-2" /> 减签
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button v-if="showButtonOther" :icon="h(MenuOutlined)">
|
||||
其他
|
||||
</a-button>
|
||||
</Dropdown>
|
||||
<ApprovalModal @complete="$emit('reload')" />
|
||||
<RejectionModal @complete="$emit('reload')" />
|
||||
<DelegationModal mode="single" @finish="handleDelegation" />
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
</Space>
|
||||
<Space v-if="type === 'admin'">
|
||||
<a-button @click="handleFlowInterfere"> 流程干预 </a-button>
|
||||
<a-button @click="() => updateAssigneeModalApi.open()">
|
||||
修改办理人
|
||||
</a-button>
|
||||
<FlowInterfereModal @complete="$emit('reload')" />
|
||||
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
:type="type"
|
||||
:task="task"
|
||||
:button-permissions="buttonPermissions"
|
||||
@reload="$emit('reload')"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<slot v-else name="empty">
|
||||
<Fallback title="点击左侧选择" />
|
||||
</slot>
|
||||
|
||||
@ -45,11 +45,8 @@ onMounted(async () => {
|
||||
}));
|
||||
});
|
||||
|
||||
/**
|
||||
* 这里无法处理昵称中带,的情况
|
||||
*/
|
||||
const isMultiplePerson = computed(
|
||||
() => props.item.approveName?.split(',').length > 1,
|
||||
() => props.item.approver?.split(',').length > 1,
|
||||
);
|
||||
</script>
|
||||
|
||||
@ -87,6 +84,7 @@ const isMultiplePerson = computed(
|
||||
</div>
|
||||
|
||||
<div :class="cn('mt-2 flex flex-wrap gap-2')" v-if="isMultiplePerson">
|
||||
<!-- 如果昵称中带, 这里的处理是不准确的 -->
|
||||
<div
|
||||
:class="cn('bg-foreground/5 flex items-center rounded-full', 'p-1')"
|
||||
v-for="(name, index) in item.approveName.split(',')"
|
||||
|
||||
@ -1,21 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { Flow } from '#/api/workflow/instance/model';
|
||||
|
||||
import { Timeline } from 'ant-design-vue';
|
||||
import { Empty, Timeline } from 'ant-design-vue';
|
||||
|
||||
import ApprovalTimelineItem from './approval-timeline-item.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
list: Flow[];
|
||||
}>();
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Timeline v-if="props.list.length > 0">
|
||||
<ApprovalTimelineItem
|
||||
v-for="item in props.list"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
/>
|
||||
<Timeline v-if="list.length > 0">
|
||||
<ApprovalTimelineItem v-for="item in list" :key="item.id" :item="item" />
|
||||
</Timeline>
|
||||
<Empty v-else />
|
||||
</template>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
<!-- 流程图预览组件 -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { stringify } from '@vben/request';
|
||||
@ -7,7 +9,14 @@ import { useWarmflowIframe } from './hook';
|
||||
|
||||
defineOptions({ name: 'FlowPreview' });
|
||||
|
||||
const props = defineProps<{ instanceId: string }>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 流程实例ID
|
||||
*/
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
@ -21,6 +30,7 @@ const params = {
|
||||
|
||||
/**
|
||||
* iframe地址
|
||||
* 后端地址 + 固定flow地址拼接
|
||||
*/
|
||||
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
||||
|
||||
@ -28,5 +38,9 @@ const { iframeRef } = useWarmflowIframe();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe ref="iframeRef" :src="url" class="h-[500px] w-full border"></iframe>
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
:src="url"
|
||||
class="h-[600px] w-full rounded-[6px] border"
|
||||
></iframe>
|
||||
</template>
|
||||
|
||||
@ -10,20 +10,26 @@ export function useWarmflowIframe() {
|
||||
const iframeRef = useTemplateRef<HTMLIFrameElement>('iframeRef');
|
||||
const { isDark } = usePreferences();
|
||||
|
||||
async function iframeLoadEvent() {
|
||||
/**
|
||||
* TODO: 这里可以优化 因为拿不到内部vue的mount状态
|
||||
*/
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const theme = isDark.value ? 'theme-dark' : 'theme-light';
|
||||
iframeRef.value?.contentWindow?.postMessage({ type: theme });
|
||||
}
|
||||
|
||||
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 });
|
||||
});
|
||||
iframeRef.value?.addEventListener('load', iframeLoadEvent);
|
||||
});
|
||||
|
||||
// onBeforeUnmount(() => {
|
||||
// iframeRef.value?.removeEventListener('load', iframeLoadEvent);
|
||||
// });
|
||||
|
||||
// 监听主题切换 通知iframe切换
|
||||
watch(isDark, (dark) => {
|
||||
if (!iframeRef.value) {
|
||||
|
||||
8
apps/web-antd/src/views/workflow/components/type.d.ts
vendored
Normal file
8
apps/web-antd/src/views/workflow/components/type.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
export {};
|
||||
/**
|
||||
* myself 我发起的
|
||||
* readonly 只读 只用于查看
|
||||
* approve 审批(我的待办)
|
||||
* admin 流程监控 - 待办任务使用
|
||||
*/
|
||||
export type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
||||
@ -150,9 +150,10 @@ const [InstanceVariableModal, instanceVariableModalApi] = useVbenModal({
|
||||
connectedComponent: instanceVariableModal,
|
||||
});
|
||||
function handleVariable(row: Recordable<any>) {
|
||||
instanceVariableModalApi.setData({ record: row.variable });
|
||||
instanceVariableModalApi.setData({ instanceId: row.id });
|
||||
instanceVariableModalApi.open();
|
||||
}
|
||||
|
||||
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
|
||||
connectedComponent: flowInfoModal,
|
||||
});
|
||||
|
||||
@ -1,28 +1,214 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { JsonPreview, useVbenModal } from '@vben/common-ui';
|
||||
import { cn, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { message, Modal, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { instanceVariable, updateFlowVariable } from '#/api/workflow/instance';
|
||||
|
||||
interface ModalData {
|
||||
/**
|
||||
* 变量 json字符串
|
||||
*/
|
||||
record: string;
|
||||
instanceId: string;
|
||||
}
|
||||
|
||||
const data = ref({});
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程变量',
|
||||
fullscreenButton: false,
|
||||
footer: false,
|
||||
onOpenChange: (visible) => {
|
||||
onOpenChange: async (visible) => {
|
||||
if (!visible) {
|
||||
data.value = {};
|
||||
return null;
|
||||
}
|
||||
const recordString = modalApi.getData().record;
|
||||
data.value = JSON.parse(recordString);
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
await loadData();
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const fieldTypeColors = {
|
||||
string: 'cyan',
|
||||
number: 'blue',
|
||||
boolean: 'orange',
|
||||
object: 'purple',
|
||||
};
|
||||
function getFieldTypeColor(fieldType: string) {
|
||||
return (
|
||||
fieldTypeColors[fieldType as keyof typeof fieldTypeColors] ?? 'default'
|
||||
);
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const { instanceId } = modalApi.getData() as ModalData;
|
||||
const resp = await instanceVariable(instanceId);
|
||||
const jsonObj = JSON.parse(resp.variable);
|
||||
data.value = jsonObj;
|
||||
|
||||
// 表单
|
||||
const objEntry = Object.entries(jsonObj);
|
||||
|
||||
interface OptionsType {
|
||||
label: string;
|
||||
value: string;
|
||||
fieldType: string;
|
||||
}
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'key',
|
||||
componentProps: {
|
||||
options: objEntry.map(
|
||||
([key, value]) =>
|
||||
({
|
||||
label: key,
|
||||
value: key,
|
||||
fieldType: typeof value,
|
||||
}) as OptionsType,
|
||||
),
|
||||
},
|
||||
renderComponentContent: () => ({
|
||||
option: (option: OptionsType) => (
|
||||
<div>
|
||||
{option.label}
|
||||
<Tag class="ml-1" color={getFieldTypeColor(option.fieldType)}>
|
||||
{option.fieldType}
|
||||
</Tag>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
allowClear: true,
|
||||
},
|
||||
labelWidth: 80,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'key',
|
||||
component: 'Select',
|
||||
label: '变量名称',
|
||||
rules: 'selectRequired',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'valueType',
|
||||
component: 'Select',
|
||||
label: '变量类型',
|
||||
rules: 'selectRequired',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
options: [
|
||||
{
|
||||
label: 'string',
|
||||
value: 'string',
|
||||
},
|
||||
{
|
||||
label: 'boolean | number | object (使用JSON.parse)',
|
||||
value: 'object',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'value',
|
||||
component: 'Input',
|
||||
label: '变量值',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
resetButtonOptions: {
|
||||
show: false,
|
||||
},
|
||||
submitButtonOptions: {
|
||||
content: '修改',
|
||||
},
|
||||
handleSubmit: async (values) => {
|
||||
console.log(values);
|
||||
Modal.confirm({
|
||||
title: '修改流程变量',
|
||||
content: '确认修改流程变量吗?',
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
onOk: async () => {
|
||||
await handleSubmit(values);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit(values: any) {
|
||||
try {
|
||||
modalApi.lock(true);
|
||||
|
||||
const { instanceId } = modalApi.getData() as ModalData;
|
||||
|
||||
let transformValue = values.value;
|
||||
if (values.valueType !== 'string') {
|
||||
try {
|
||||
transformValue = JSON.parse(values.value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改
|
||||
const requestData = {
|
||||
instanceId,
|
||||
key: values.key,
|
||||
value: transformValue,
|
||||
};
|
||||
await updateFlowVariable(requestData);
|
||||
await formApi.resetForm();
|
||||
|
||||
// 查询修改后的
|
||||
const resp = await instanceVariable(instanceId);
|
||||
const jsonObj = JSON.parse(resp.variable);
|
||||
data.value = jsonObj;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<div class="min-h-[400px] overflow-y-auto">
|
||||
<div
|
||||
:class="cn('min-h-[400px] overflow-y-auto border', 'rounded-[4px] p-2')"
|
||||
>
|
||||
<JsonPreview :data="data" />
|
||||
</div>
|
||||
<div class="mt-2 break-all text-sm font-medium text-orange-500">
|
||||
需要支持变量类型需要更改后端代码(原版只支持string类型)
|
||||
<div>
|
||||
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowVariableBo.java
|
||||
</div>
|
||||
将value的类型改为Object才能使用
|
||||
</div>
|
||||
<Form class="mt-2" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
@ -473,6 +473,8 @@ export interface FormSchema<
|
||||
fieldName: string;
|
||||
/** 帮助信息 */
|
||||
help?: CustomRenderType;
|
||||
/** 是否隐藏表单项 */
|
||||
hide?: boolean;
|
||||
/** 表单的标签(如果是一个string,会用于默认必选规则的消息提示) */
|
||||
label?: CustomRenderType;
|
||||
/** 自定义组件内部渲染 */
|
||||
|
||||
@ -27,8 +27,8 @@
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
|
||||
"build:analyze": "turbo build:analyze",
|
||||
"build:antd": "pnpm run build --filter=@vben/web-antd",
|
||||
"build:antd:test": "pnpm run build --filter=@vben/web-antd -- --mode test",
|
||||
"build:antd": "pnpm run build --filter=@vben/web-antd build:prod",
|
||||
"build:antd:test": "pnpm run build --filter=@vben/web-antd build:test",
|
||||
"build:docker": "./scripts/deploy/build-local-docker-image.sh",
|
||||
"build:docs": "pnpm run build --filter=@vben/docs",
|
||||
"build:play": "pnpm run build --filter=@vben/playground",
|
||||
|
||||
@ -85,3 +85,17 @@
|
||||
.z-popup {
|
||||
z-index: var(--popup-z-index);
|
||||
}
|
||||
|
||||
@keyframes shrink {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ const defaultPreferences: Preferences = {
|
||||
enableCheckUpdates: true,
|
||||
enablePreferences: true,
|
||||
enableRefreshToken: false,
|
||||
enableStickyPreferencesNavigationBar: true,
|
||||
isMobile: false,
|
||||
layout: 'sidebar-nav',
|
||||
locale: 'zh-CN',
|
||||
@ -29,6 +30,7 @@ const defaultPreferences: Preferences = {
|
||||
name: 'Vben Admin',
|
||||
preferencesButtonPosition: 'auto',
|
||||
watermark: false,
|
||||
watermarkContent: '',
|
||||
zIndex: 200,
|
||||
},
|
||||
breadcrumb: {
|
||||
|
||||
@ -59,6 +59,10 @@ interface AppPreferences {
|
||||
* @zh_CN 是否开启refreshToken
|
||||
*/
|
||||
enableRefreshToken: boolean;
|
||||
/**
|
||||
* @zh_CN 是否开启首选项导航栏吸顶效果
|
||||
*/
|
||||
enableStickyPreferencesNavigationBar: boolean;
|
||||
/** 是否移动端 */
|
||||
isMobile: boolean;
|
||||
/** 布局方式 */
|
||||
@ -75,6 +79,10 @@ interface AppPreferences {
|
||||
* @zh_CN 是否开启水印
|
||||
*/
|
||||
watermark: boolean;
|
||||
/**
|
||||
* @zh_CN 水印文案
|
||||
*/
|
||||
watermarkContent: string;
|
||||
/** z-index */
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
@ -342,13 +342,12 @@ export class FormApi {
|
||||
isObject(obj[key]) &&
|
||||
!isDayjsObject(obj[key]) &&
|
||||
!isDate(obj[key])
|
||||
? fieldMergeFn(obj[key], value)
|
||||
? fieldMergeFn(value, obj[key])
|
||||
: value;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const filteredFields = fieldMergeFn(fields, form.values);
|
||||
this.handleStringToArrayFields(filteredFields);
|
||||
form.setValues(filteredFields, shouldValidate);
|
||||
}
|
||||
|
||||
@ -358,7 +357,6 @@ export class FormApi {
|
||||
const form = await this.getForm();
|
||||
await form.submitForm();
|
||||
const rawValues = toRaw(await this.getValues());
|
||||
this.handleArrayToStringFields(rawValues);
|
||||
await this.state?.handleSubmit?.(rawValues);
|
||||
|
||||
return rawValues;
|
||||
@ -458,16 +456,31 @@ export class FormApi {
|
||||
return this.form;
|
||||
}
|
||||
|
||||
private handleArrayToStringFields = (originValues: Record<string, any>) => {
|
||||
private handleMultiFields = (originValues: Record<string, any>) => {
|
||||
const arrayToStringFields = this.state?.arrayToStringFields;
|
||||
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const processFields = (fields: string[], separator: string = ',') => {
|
||||
this.processFields(fields, separator, originValues, (value, sep) =>
|
||||
Array.isArray(value) ? value.join(sep) : value,
|
||||
);
|
||||
this.processFields(fields, separator, originValues, (value, sep) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(sep);
|
||||
} else if (typeof value === 'string') {
|
||||
// 处理空字符串的情况
|
||||
if (value === '') {
|
||||
return [];
|
||||
}
|
||||
// 处理复杂分隔符的情况
|
||||
const escapedSeparator = sep.replaceAll(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
String.raw`\$&`,
|
||||
);
|
||||
return value.split(new RegExp(escapedSeparator));
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
|
||||
@ -503,8 +516,7 @@ export class FormApi {
|
||||
const values = { ...originValues };
|
||||
const fieldMappingTime = this.state?.fieldMappingTime;
|
||||
|
||||
this.handleStringToArrayFields(values);
|
||||
|
||||
this.handleMultiFields(values);
|
||||
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
|
||||
return values;
|
||||
}
|
||||
@ -550,65 +562,6 @@ export class FormApi {
|
||||
return values;
|
||||
};
|
||||
|
||||
private handleStringToArrayFields = (originValues: Record<string, any>) => {
|
||||
const arrayToStringFields = this.state?.arrayToStringFields;
|
||||
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const processFields = (fields: string[], separator: string = ',') => {
|
||||
this.processFields(fields, separator, originValues, (value, sep) => {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
// 处理空字符串的情况
|
||||
if (value === '') {
|
||||
return [];
|
||||
}
|
||||
// 处理复杂分隔符的情况
|
||||
const escapedSeparator = sep.replaceAll(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
String.raw`\$&`,
|
||||
);
|
||||
return value.split(new RegExp(escapedSeparator));
|
||||
});
|
||||
};
|
||||
|
||||
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
|
||||
if (arrayToStringFields.every((item) => typeof item === 'string')) {
|
||||
const lastItem =
|
||||
arrayToStringFields[arrayToStringFields.length - 1] || '';
|
||||
const fields =
|
||||
lastItem.length === 1
|
||||
? arrayToStringFields.slice(0, -1)
|
||||
: arrayToStringFields;
|
||||
const separator = lastItem.length === 1 ? lastItem : ',';
|
||||
processFields(fields, separator);
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理嵌套数组格式 [['field1'], ';']
|
||||
arrayToStringFields.forEach((fieldConfig) => {
|
||||
if (Array.isArray(fieldConfig)) {
|
||||
const [fields, separator = ','] = fieldConfig;
|
||||
if (Array.isArray(fields)) {
|
||||
processFields(fields, separator);
|
||||
} else if (typeof originValues[fields] === 'string') {
|
||||
const value = originValues[fields];
|
||||
if (value === '') {
|
||||
originValues[fields] = [];
|
||||
} else {
|
||||
const escapedSeparator = separator.replaceAll(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
String.raw`\$&`,
|
||||
);
|
||||
originValues[fields] = value.split(new RegExp(escapedSeparator));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private processFields = (
|
||||
fields: string[],
|
||||
separator: string,
|
||||
|
||||
@ -41,6 +41,7 @@ const {
|
||||
emptyStateValue,
|
||||
fieldName,
|
||||
formFieldProps,
|
||||
hide,
|
||||
label,
|
||||
labelClass,
|
||||
labelWidth,
|
||||
@ -95,7 +96,7 @@ const currentRules = computed(() => {
|
||||
});
|
||||
|
||||
const visible = computed(() => {
|
||||
return isIf.value && isShow.value;
|
||||
return !hide && isIf.value && isShow.value;
|
||||
});
|
||||
|
||||
const shouldRequired = computed(() => {
|
||||
@ -283,7 +284,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<FormField
|
||||
v-if="isIf"
|
||||
v-if="!hide && isIf"
|
||||
v-bind="fieldProps"
|
||||
v-slot="slotProps"
|
||||
:name="fieldName"
|
||||
|
||||
@ -255,6 +255,8 @@ export interface FormSchema<
|
||||
fieldName: string;
|
||||
/** 帮助信息 */
|
||||
help?: CustomRenderType;
|
||||
/** 是否隐藏表单项 */
|
||||
hide?: boolean;
|
||||
/** 表单项 */
|
||||
label?: CustomRenderType;
|
||||
// 自定义组件内部渲染
|
||||
@ -277,7 +279,8 @@ export interface FormRenderProps<
|
||||
*/
|
||||
arrayToStringFields?: ArrayToStringFields;
|
||||
/**
|
||||
* 是否展开,在showCollapseButton=true下生效
|
||||
* 是否折叠,在showCollapseButton=true下生效
|
||||
* true:折叠 false:展开
|
||||
*/
|
||||
collapsed?: boolean;
|
||||
/**
|
||||
|
||||
@ -390,10 +390,10 @@ $namespace: vben;
|
||||
var(--menu-item-margin-x);
|
||||
font-size: var(--menu-font-size);
|
||||
color: var(--menu-item-color);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
background: var(--menu-item-background-color);
|
||||
border: none;
|
||||
border-radius: var(--menu-item-radius);
|
||||
@ -495,7 +495,7 @@ $namespace: vben;
|
||||
&.is-rounded {
|
||||
--menu-item-margin-x: 8px;
|
||||
--menu-item-collapse-margin-x: 6px;
|
||||
--menu-item-radius: 8px;
|
||||
--menu-item-radius: 6px;
|
||||
}
|
||||
|
||||
&.is-horizontal:not(.is-rounded) {
|
||||
@ -717,8 +717,8 @@ $namespace: vben;
|
||||
width: var(--menu-item-icon-size);
|
||||
height: var(--menu-item-icon-size);
|
||||
margin-right: 8px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,10 @@ isFullscreen.value = !!(
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<VbenIconButton @click="toggle">
|
||||
<VbenIconButton
|
||||
class="hover:animate-[shrink_0.3s_ease-in-out]"
|
||||
@click="toggle"
|
||||
>
|
||||
<Minimize v-if="isFullscreen" class="text-foreground size-4" />
|
||||
<Maximize v-else class="text-foreground size-4" />
|
||||
</VbenIconButton>
|
||||
|
||||
@ -35,16 +35,24 @@ const tabsIndicatorStyle = computed(() => {
|
||||
width: `${(100 / props.tabs.length).toFixed(0)}%`,
|
||||
};
|
||||
});
|
||||
|
||||
function activeClass(tab: string): string[] {
|
||||
return tab === activeTab.value ? ['!font-bold', 'text-primary'] : [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tabs v-model="activeTab" :default-value="getDefaultValue">
|
||||
<TabsList :style="tabsStyle" class="bg-accent relative grid w-full">
|
||||
<TabsList
|
||||
:style="tabsStyle"
|
||||
class="bg-accent !outline-heavy relative grid w-full !outline !outline-2"
|
||||
>
|
||||
<TabsIndicator :style="tabsIndicatorStyle" />
|
||||
<template v-for="tab in tabs" :key="tab.value">
|
||||
<TabsTrigger
|
||||
:value="tab.value"
|
||||
class="z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50"
|
||||
:class="activeClass(tab.value)"
|
||||
class="hover:text-primary z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</TabsTrigger>
|
||||
|
||||
@ -21,7 +21,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-1 transition-[width,transform] duration-300',
|
||||
'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-0.5 transition-[width,transform] duration-300',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@ -12,15 +12,13 @@ function isAccessible(
|
||||
el: Element,
|
||||
binding: DirectiveBinding<string | string[]>,
|
||||
) {
|
||||
const { accessMode, hasAccessByCodes, hasAccessByRoles } = useAccess();
|
||||
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
|
||||
|
||||
const value = binding.value;
|
||||
|
||||
if (!value) return;
|
||||
const authMethod =
|
||||
accessMode.value === 'frontend' && binding.arg === 'role'
|
||||
? hasAccessByRoles
|
||||
: hasAccessByCodes;
|
||||
binding.arg === 'role' ? hasAccessByRoles : hasAccessByCodes;
|
||||
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
|
||||
|
||||
@ -17,6 +17,10 @@ function useAccess() {
|
||||
*/
|
||||
function hasAccessByRoles(roles: string[]) {
|
||||
const userRoleSet = new Set(userStore.userRoles);
|
||||
// 超管的角色
|
||||
if (userRoleSet.has('superadmin')) {
|
||||
return true;
|
||||
}
|
||||
const intersection = roles.filter((item) => userRoleSet.has(item));
|
||||
return intersection.length > 0;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { RiDingding } from '@vben/icons';
|
||||
import { SvgDingDingIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { alert, useVbenModal } from '@vben-core/popup-ui';
|
||||
@ -96,7 +96,7 @@ const handleLogin = () => {
|
||||
:tooltip="$t('authentication.dingdingLogin')"
|
||||
tooltip-side="top"
|
||||
>
|
||||
<RiDingding />
|
||||
<SvgDingDingIcon />
|
||||
</VbenIconButton>
|
||||
<Modal>
|
||||
<div id="dingding_qrcode_login_element"></div>
|
||||
|
||||
@ -158,7 +158,9 @@ function clickLogo() {
|
||||
function autoCollapseMenuByRouteMeta(route: RouteLocationNormalizedLoaded) {
|
||||
// 只在双列模式下生效
|
||||
if (
|
||||
preferences.app.layout === 'sidebar-mixed-nav' &&
|
||||
['header-mixed-nav', 'sidebar-mixed-nav'].includes(
|
||||
preferences.app.layout,
|
||||
) &&
|
||||
route.meta &&
|
||||
route.meta.hideInMenu
|
||||
) {
|
||||
|
||||
@ -30,7 +30,7 @@ async function handleUpdate(value: string | undefined) {
|
||||
:model-value="preferences.app.locale"
|
||||
@update:model-value="handleUpdate"
|
||||
>
|
||||
<VbenIconButton>
|
||||
<VbenIconButton class="hover:animate-[shrink_0.3s_ease-in-out]">
|
||||
<Languages class="text-foreground size-4" />
|
||||
</VbenIconButton>
|
||||
</VbenDropdownRadioMenu>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { SUPPORT_LANGUAGES } from '@vben/constants';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import InputItem from '../input-item.vue';
|
||||
import SelectItem from '../select-item.vue';
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
|
||||
@ -12,6 +13,7 @@ defineOptions({
|
||||
const appLocale = defineModel<string>('appLocale');
|
||||
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
|
||||
const appWatermark = defineModel<boolean>('appWatermark');
|
||||
const appWatermarkContent = defineModel<string>('appWatermarkContent');
|
||||
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
|
||||
</script>
|
||||
|
||||
@ -22,9 +24,23 @@ const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
|
||||
<SwitchItem v-model="appDynamicTitle">
|
||||
{{ $t('preferences.dynamicTitle') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="appWatermark">
|
||||
<SwitchItem
|
||||
v-model="appWatermark"
|
||||
@update:model-value="
|
||||
(val) => {
|
||||
if (!val) appWatermarkContent = '';
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ $t('preferences.watermark') }}
|
||||
</SwitchItem>
|
||||
<InputItem
|
||||
v-if="appWatermark"
|
||||
v-model="appWatermarkContent"
|
||||
:placeholder="$t('preferences.watermarkContent')"
|
||||
>
|
||||
{{ $t('preferences.watermarkContent') }}
|
||||
</InputItem>
|
||||
<SwitchItem v-model="appEnableCheckUpdates">
|
||||
{{ $t('preferences.checkUpdates') }}
|
||||
</SwitchItem>
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectOption } from '@vben/types';
|
||||
|
||||
import { CircleHelp } from '@vben/icons';
|
||||
import { Input, VbenTooltip } from '@vben-core/shadcn-ui';
|
||||
import { useSlots } from 'vue';
|
||||
|
||||
import { CircleHelp, CircleX } from '@vben/icons';
|
||||
|
||||
import { Input, VbenTooltip } from '@vben-core/shadcn-ui';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceSelectItem',
|
||||
});
|
||||
@ -45,6 +47,17 @@ const slots = useSlots();
|
||||
<slot name="tip"></slot>
|
||||
</VbenTooltip>
|
||||
</span>
|
||||
<Input v-model="inputValue" class="h-8 w-[165px]" />
|
||||
<div class="relative">
|
||||
<Input
|
||||
v-model="inputValue"
|
||||
class="h-8 w-[165px]"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<CircleX
|
||||
v-if="inputValue"
|
||||
class="hover:text-foreground text-foreground/60 absolute right-2 top-1/2 size-3 -translate-y-1/2 transform cursor-pointer"
|
||||
@click="() => (inputValue = '')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -134,14 +134,14 @@ watch(
|
||||
<template v-if="theme.type !== 'custom'">
|
||||
<div
|
||||
:style="{ backgroundColor: theme.color }"
|
||||
class="mx-10 my-2 size-5 rounded-md"
|
||||
class="mx-9 my-2 size-5 rounded-md"
|
||||
></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="size-full px-10 py-2" @click.stop="selectColor">
|
||||
<div class="size-full px-9 py-2" @click.stop="selectColor">
|
||||
<div class="flex-center relative size-5 rounded-sm">
|
||||
<UserRoundPen
|
||||
class="absolute z-10 size-5 opacity-60 group-hover:opacity-100"
|
||||
class="z-1 absolute size-5 opacity-60 group-hover:opacity-100"
|
||||
/>
|
||||
<input
|
||||
ref="colorInput"
|
||||
|
||||
@ -12,7 +12,7 @@ function clearPreferencesAndLogout() {
|
||||
</script>
|
||||
<template>
|
||||
<Preferences @clear-preferences-and-logout="clearPreferencesAndLogout">
|
||||
<VbenIconButton>
|
||||
<VbenIconButton class="hover:animate-[shrink_0.3s_ease-in-out]">
|
||||
<Settings class="text-foreground size-4" />
|
||||
</VbenIconButton>
|
||||
</Preferences>
|
||||
|
||||
@ -16,7 +16,7 @@ import type { SegmentedItem } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Copy, RotateCw } from '@vben/icons';
|
||||
import { Copy, Pin, PinOff, RotateCw } from '@vben/icons';
|
||||
import { $t, loadLocaleMessages } from '@vben/locales';
|
||||
import {
|
||||
clearPreferencesCache,
|
||||
@ -67,7 +67,11 @@ const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
|
||||
const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
|
||||
const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
|
||||
const appWatermark = defineModel<boolean>('appWatermark');
|
||||
const appWatermarkContent = defineModel<string>('appWatermarkContent');
|
||||
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
|
||||
const appEnableStickyPreferencesNavigationBar = defineModel<boolean>(
|
||||
'appEnableStickyPreferencesNavigationBar',
|
||||
);
|
||||
const appPreferencesButtonPosition = defineModel<PreferencesButtonPositionType>(
|
||||
'appPreferencesButtonPosition',
|
||||
);
|
||||
@ -240,7 +244,7 @@ async function handleReset() {
|
||||
<Drawer
|
||||
:description="$t('preferences.subtitle')"
|
||||
:title="$t('preferences.title')"
|
||||
class="sm:max-w-sm"
|
||||
class="!border-0 sm:max-w-sm"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="flex items-center">
|
||||
@ -248,18 +252,44 @@ async function handleReset() {
|
||||
:disabled="!diffPreference"
|
||||
:tooltip="$t('preferences.resetTip')"
|
||||
class="relative"
|
||||
@click="handleReset"
|
||||
>
|
||||
<span
|
||||
v-if="diffPreference"
|
||||
class="bg-primary absolute right-0.5 top-0.5 h-2 w-2 rounded"
|
||||
></span>
|
||||
<RotateCw class="size-4" @click="handleReset" />
|
||||
<RotateCw class="size-4" />
|
||||
</VbenIconButton>
|
||||
<VbenIconButton
|
||||
:tooltip="
|
||||
appEnableStickyPreferencesNavigationBar
|
||||
? $t('preferences.disableStickyPreferencesNavigationBar')
|
||||
: $t('preferences.enableStickyPreferencesNavigationBar')
|
||||
"
|
||||
class="relative"
|
||||
@click="
|
||||
() =>
|
||||
(appEnableStickyPreferencesNavigationBar =
|
||||
!appEnableStickyPreferencesNavigationBar)
|
||||
"
|
||||
>
|
||||
<PinOff
|
||||
v-if="appEnableStickyPreferencesNavigationBar"
|
||||
class="size-4"
|
||||
/>
|
||||
<Pin v-else class="size-4" />
|
||||
</VbenIconButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="p-1">
|
||||
<VbenSegmented v-model="activeTab" :tabs="tabs">
|
||||
<div>
|
||||
<VbenSegmented
|
||||
v-model="activeTab"
|
||||
:tabs="tabs"
|
||||
:class="{
|
||||
'sticky-tabs-header': appEnableStickyPreferencesNavigationBar,
|
||||
}"
|
||||
>
|
||||
<template #general>
|
||||
<Block :title="$t('preferences.general')">
|
||||
<General
|
||||
@ -267,6 +297,7 @@ async function handleReset() {
|
||||
v-model:app-enable-check-updates="appEnableCheckUpdates"
|
||||
v-model:app-locale="appLocale"
|
||||
v-model:app-watermark="appWatermark"
|
||||
v-model:app-watermark-content="appWatermarkContent"
|
||||
/>
|
||||
</Block>
|
||||
|
||||
@ -447,3 +478,11 @@ async function handleReset() {
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.sticky-tabs-header [role='tablist']) {
|
||||
position: sticky;
|
||||
top: -12px;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -64,7 +64,7 @@ function toggleTheme(event: MouseEvent) {
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${endRadius}px at ${x}px ${y}px)`,
|
||||
];
|
||||
document.documentElement.animate(
|
||||
const animate = document.documentElement.animate(
|
||||
{
|
||||
clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
|
||||
},
|
||||
@ -76,6 +76,9 @@ function toggleTheme(event: MouseEvent) {
|
||||
: '::view-transition-new(root)',
|
||||
},
|
||||
);
|
||||
animate.onfinish = () => {
|
||||
transition.skipTransition();
|
||||
};
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@ -85,7 +88,7 @@ function toggleTheme(event: MouseEvent) {
|
||||
:aria-label="theme"
|
||||
:class="[`is-${theme}`]"
|
||||
aria-live="polite"
|
||||
class="theme-toggle cursor-pointer border-none bg-none"
|
||||
class="theme-toggle cursor-pointer border-none bg-none hover:animate-[shrink_0.3s_ease-in-out]"
|
||||
v-bind="bindProps"
|
||||
@click.stop="toggleTheme"
|
||||
>
|
||||
|
||||
@ -32,6 +32,21 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
|
||||
const { height, width } = useWindowSize();
|
||||
const resizeHandler: () => void = useDebounceFn(resize, 200);
|
||||
|
||||
const getChartEl = (): HTMLElement | null => {
|
||||
const refValue = chartRef?.value as unknown;
|
||||
if (!refValue) return null;
|
||||
if (refValue instanceof HTMLElement) {
|
||||
return refValue;
|
||||
}
|
||||
const maybeComponent = refValue as { $el?: HTMLElement };
|
||||
return maybeComponent.$el ?? null;
|
||||
};
|
||||
|
||||
const isElHidden = (el: HTMLElement | null): boolean => {
|
||||
if (!el) return true;
|
||||
return el.offsetHeight === 0 || el.offsetWidth === 0;
|
||||
};
|
||||
|
||||
const getOptions = computed((): EChartsOption => {
|
||||
if (!isDark.value) {
|
||||
return {};
|
||||
@ -69,6 +84,13 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
const el = getChartEl();
|
||||
if (isElHidden(el)) {
|
||||
useTimeoutFn(async () => {
|
||||
resolve(await renderEcharts(currentOptions));
|
||||
}, 30);
|
||||
return;
|
||||
}
|
||||
useTimeoutFn(() => {
|
||||
if (!chartInstance) {
|
||||
const instance = initCharts();
|
||||
@ -83,6 +105,10 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
|
||||
};
|
||||
|
||||
function resize(withAnimation = true) {
|
||||
const el = getChartEl();
|
||||
if (isElHidden(el)) {
|
||||
return;
|
||||
}
|
||||
chartInstance?.resize({
|
||||
animation: withAnimation
|
||||
? {
|
||||
|
||||
@ -49,6 +49,9 @@
|
||||
--vxe-ui-font-primary-lighten-color: hsl(var(--primary) / 60%);
|
||||
--vxe-ui-font-primary-darken-color: hsl(var(--primary));
|
||||
|
||||
/* 拖拽列宽颜色 */
|
||||
--vxe-ui-table-resizable-drag-line-color: hsl(var(--primary));
|
||||
|
||||
/* --vxe-ui-table-fixed-scrolling-box-shadow-color: rgb(0 0 0 / 80%); */
|
||||
|
||||
height: auto !important;
|
||||
@ -138,3 +141,8 @@ TODO: 最后一条数据hover/check仍会显示边框
|
||||
border-radius: var(--vxe-ui-table-border-radius)
|
||||
var(--vxe-ui-table-border-radius) 0 0;
|
||||
}
|
||||
|
||||
/* modal/drawer里使用列配置 重置列弹窗被遮挡 */
|
||||
.vxe-dynamics--modal > .vxe-modal--wrapper {
|
||||
z-index: calc(var(--popup-z-index) + 1) !important;
|
||||
}
|
||||
|
||||
1
packages/icons/src/svg/icons/dingding.svg
Normal file
1
packages/icons/src/svg/icons/dingding.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M512 2C230.2 2 2 230.2 2 512s228.2 510 510 510 510-228.2 510-510S793.3 2 512 2z m235.9 442c-1 4.6-3.6 10.8-7.2 19.1l-0.5 0.5c-21.6 45.8-77.3 135.5-77.3 135.5l-0.5-0.5-16.5 28.3h78.8L574.3 826.8l34-136h-61.8l21.6-90.2c-17.5 4.1-38.1 9.8-62.3 18 0 0-33 19.1-94.8-37.1 0 0-41.7-37.1-17.5-45.8 10.3-4.1 50-8.8 81.4-12.9 42.2-5.7 68.5-8.8 68.5-8.8s-130.3 2.1-161.2-3.1c-30.9-4.6-70.1-56.7-78.3-102 0 0-12.9-24.7 27.8-12.9 40.2 11.8 209.2 45.8 209.2 45.8S321.4 375 307 358.5c-14.4-16.5-42.8-89.6-39.2-134.5 0 0 1.5-11.3 12.9-8.2 0 0 161.8 74.2 272.5 114.4C664.5 371.4 760.8 392 747.9 444z" fill="#3296FA"/></svg>
|
||||
|
After Width: | Height: | Size: 705 B |
1
packages/icons/src/svg/icons/github.svg
Normal file
1
packages/icons/src/svg/icons/github.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M517.30912244 23.0407001c-277.94803947-0.11230224-503.114027 225.0536853-503.11402702 503.00172479 0 219.66317786 140.82700667 406.42179996 337.01901675 475.03846748 26.39102597 6.73813429 22.34814539-12.24094396 22.3481454-25.04339914v-87.14653682c-152.61874168 17.9683581-158.68306256-83.10365624-169.01486845-99.94899196-20.77591406-35.37520501-69.62738767-44.35938408-55.0280967-61.20471981 34.81369383-17.9683581 70.18889885 4.49208953 111.17921577 65.13529813 29.76009311 43.91017513 87.48344355 36.49822743 116.90662996 29.19858195 6.40122758-26.39102597 20.21440287-49.97449598 38.96887665-68.39206307-157.89694687-28.07555954-223.93066292-124.65548435-223.93066294-239.42837176 0-55.5896079 18.30526482-106.7994285 54.35428331-148.01434994-22.90965658-68.16745859 2.13374251-126.34001793 5.50280964-134.98729027 65.3599026-5.95201861 133.07815223 46.71773107 138.46865968 50.87291388 37.17204084-9.99489919 79.50998463-15.38540662 126.90152914-15.38540659 47.72845123 0 90.29099949 5.50280967 127.57534256 15.61001106 12.69015291-9.65799248 75.69170854-54.80349223 136.33491714-49.30068255 3.25676491 8.64727234 27.73865283 65.47220485 6.1766231 132.74124553 36.49822743 41.32722364 54.91579446 92.87395098 54.91579447 148.80046559 0 114.99749189-66.25832052 211.57741672-224.82908082 239.54067401 26.5033282 26.05411926 42.89945497 62.32774217 42.89945498 102.41964122v126.45232019c0.89841792 9.99489919 0 20.21440287 16.95763795 20.21440287 199.22417052-67.26904065 342.52182644-255.3752896 342.52182644-477.05990776 0-277.94803947-225.27828977-503.114027-503.11402701-503.11402703z" fill="currentColor"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
packages/icons/src/svg/icons/google.svg
Normal file
1
packages/icons/src/svg/icons/google.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M213.573746 512.08574a296.892457 296.892457 0 0 1 15.663636-94.698456L53.712514 286.089155a504.154105 504.154105 0 0 0 0 451.737229l175.422492-131.502883a296.022255 296.022255 0 0 1-15.356507-94.288949" fill="#FBBC05"/><path d="M521.881206 208.845928a305.696854 305.696854 0 0 1 192.007518 67.210309l151.773472-148.446228A527.547183 527.547183 0 0 0 53.763703 284.195186l175.422491 131.349318a306.771809 306.771809 0 0 1 292.541447-206.698576" fill="#EA4335"/><path d="M521.881206 815.325553a307.130128 307.130128 0 0 1-292.643824-206.698576l-175.524868 131.29813A521.148639 521.148639 0 0 0 521.881206 1023.969287a503.130338 503.130338 0 0 0 339.839486-127.100685l-166.771659-126.128105a327.861412 327.861412 0 0 1-173.221392 44.585056" fill="#34A853"/><path d="M1023.475893 513.774956a415.64944 415.64944 0 0 0-11.875698-92.70211h-486.289369v197.023977h279.744358a229.528582 229.528582 0 0 1-106.522966 152.43892l166.618094 126.128106a502.004194 502.004194 0 0 0 158.069639-382.888893" fill="#4285F4"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
packages/icons/src/svg/icons/qqchat.svg
Normal file
1
packages/icons/src/svg/icons/qqchat.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M511.09761 957.257c-80.159 0-153.737-25.019-201.11-62.386-24.057 6.702-54.831 17.489-74.252 30.864-16.617 11.439-14.546 23.106-11.55 27.816 13.15 20.689 225.583 13.211 286.912 6.767v-3.061z" fill="#FAAD08"/><path d="M496.65061 957.257c80.157 0 153.737-25.019 201.11-62.386 24.057 6.702 54.83 17.489 74.253 30.864 16.616 11.439 14.543 23.106 11.55 27.816-13.15 20.689-225.584 13.211-286.914 6.767v-3.061z" fill="#FAAD08"/><path d="M497.12861 474.524c131.934-0.876 237.669-25.783 273.497-35.34 8.541-2.28 13.11-6.364 13.11-6.364 0.03-1.172 0.542-20.952 0.542-31.155C784.27761 229.833 701.12561 57.173 496.64061 57.162 292.15661 57.173 209.00061 229.832 209.00061 401.665c0 10.203 0.516 29.983 0.547 31.155 0 0 3.717 3.821 10.529 5.67 33.078 8.98 140.803 35.139 276.08 36.034h0.972z" fill="#000000"/><path d="M860.28261 619.782c-8.12-26.086-19.204-56.506-30.427-85.72 0 0-6.456-0.795-9.718 0.148-100.71 29.205-222.773 47.818-315.792 46.695h-0.962C410.88561 582.017 289.65061 563.617 189.27961 534.698 185.44461 533.595 177.87261 534.063 177.87261 534.063 166.64961 563.276 155.56661 593.696 147.44761 619.782 108.72961 744.168 121.27261 795.644 130.82461 796.798c20.496 2.474 79.78-93.637 79.78-93.637 0 97.66 88.324 247.617 290.576 248.996a718.01 718.01 0 0 1 5.367 0C708.80161 950.778 797.12261 800.822 797.12261 703.162c0 0 59.284 96.111 79.783 93.637 9.55-1.154 22.093-52.63-16.623-177.017" fill="#000000"/><path d="M434.38261 316.917c-27.9 1.24-51.745-30.106-53.24-69.956-1.518-39.877 19.858-73.207 47.764-74.454 27.875-1.224 51.703 30.109 53.218 69.974 1.527 39.877-19.853 73.2-47.742 74.436m206.67-69.956c-1.494 39.85-25.34 71.194-53.24 69.956-27.888-1.238-49.269-34.559-47.742-74.435 1.513-39.868 25.341-71.201 53.216-69.974 27.909 1.247 49.285 34.576 47.767 74.453" fill="#FFFFFF"/><path d="M683.94261 368.627c-7.323-17.609-81.062-37.227-172.353-37.227h-0.98c-91.29 0-165.031 19.618-172.352 37.227a6.244 6.244 0 0 0-0.535 2.505c0 1.269 0.393 2.414 1.006 3.386 6.168 9.765 88.054 58.018 171.882 58.018h0.98c83.827 0 165.71-48.25 171.881-58.016a6.352 6.352 0 0 0 1.002-3.395c0-0.897-0.2-1.736-0.531-2.498" fill="#FAAD08"/><path d="M467.63161 256.377c1.26 15.886-7.377 30-19.266 31.542-11.907 1.544-22.569-10.083-23.836-25.978-1.243-15.895 7.381-30.008 19.25-31.538 11.927-1.549 22.607 10.088 23.852 25.974m73.097 7.935c2.533-4.118 19.827-25.77 55.62-17.886 9.401 2.07 13.75 5.116 14.668 6.316 1.355 1.77 1.726 4.29 0.352 7.684-2.722 6.725-8.338 6.542-11.454 5.226-2.01-0.85-26.94-15.889-49.905 6.553-1.579 1.545-4.405 2.074-7.085 0.242-2.678-1.834-3.786-5.553-2.196-8.135" fill="#000000"/><path d="M504.33261 584.495h-0.967c-63.568 0.752-140.646-7.504-215.286-21.92-6.391 36.262-10.25 81.838-6.936 136.196 8.37 137.384 91.62 223.736 220.118 224.996H506.48461c128.498-1.26 211.748-87.612 220.12-224.996 3.314-54.362-0.547-99.938-6.94-136.203-74.654 14.423-151.745 22.684-215.332 21.927" fill="#FFFFFF"/><path d="M323.27461 577.016v137.468s64.957 12.705 130.031 3.91V591.59c-41.225-2.262-85.688-7.304-130.031-14.574" fill="#EB1C26"/><path d="M788.09761 432.536s-121.98 40.387-283.743 41.539h-0.962c-161.497-1.147-283.328-41.401-283.744-41.539l-40.854 106.952c102.186 32.31 228.837 53.135 324.598 51.926l0.96-0.002c95.768 1.216 222.4-19.61 324.6-51.924l-40.855-106.952z" fill="#EB1C26"/></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742660391897" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3162" xmlns:xlink="http://www.w3.org/1999/xlink" width="292.5" height="256"><path d="M331.429 263.429q0-23.429-14.286-37.715t-37.714-14.285q-24.572 0-43.429 14.571t-18.857 37.429q0 22.285 18.857 36.857t43.429 14.571q23.428 0 37.714-14t14.286-37.428zM756 553.143q0-16-14.571-28.572T704 512q-15.429 0-28.286 12.857t-12.857 28.286q0 16 12.857 28.857T704 594.857q22.857 0 37.429-12.571T756 553.143zM621.143 263.429q0-23.429-14-37.715t-37.429-14.285q-24.571 0-43.428 14.571t-18.857 37.429q0 22.285 18.857 36.857t43.428 14.571q23.429 0 37.429-14t14-37.428zM984 553.143q0-16-14.857-28.572T932 512q-15.429 0-28.286 12.857t-12.857 28.286q0 16 12.857 28.857T932 594.857q22.286 0 37.143-12.571T984 553.143zM832 326.286Q814.286 324 792 324q-96.571 0-177.714 44T486.57 487.143 440 651.429q0 44.571 13.143 86.857-20 1.714-38.857 1.714-14.857 0-28.572-0.857t-31.428-3.714-25.429-4-31.143-6-28.571-6L124.57 792l41.143-124.571Q0 551.429 0 387.429q0-96.572 55.714-177.715T206.571 82t207.715-46.571q100.571 0 190 37.714T754 177.429t78 148.857z m338.286 320.571q0 66.857-39.143 127.714t-106 110.572l31.428 103.428-113.714-62.285q-85.714 21.143-124.571 21.143-96.572 0-177.715-40.286T512.857 797.714t-46.571-150.857T512.857 496t127.714-109.429 177.715-40.285q92 0 173.143 40.285t130 109.715 48.857 150.571z" fill="#0e932e" p-id="3163"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742660391897" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3162" xmlns:xlink="http://www.w3.org/1999/xlink" width="292.5" height="256"><path d="M331.429 263.429q0-23.429-14.286-37.715t-37.714-14.285q-24.572 0-43.429 14.571t-18.857 37.429q0 22.285 18.857 36.857t43.429 14.571q23.428 0 37.714-14t14.286-37.428zM756 553.143q0-16-14.571-28.572T704 512q-15.429 0-28.286 12.857t-12.857 28.286q0 16 12.857 28.857T704 594.857q22.857 0 37.429-12.571T756 553.143zM621.143 263.429q0-23.429-14-37.715t-37.429-14.285q-24.571 0-43.428 14.571t-18.857 37.429q0 22.285 18.857 36.857t43.428 14.571q23.429 0 37.429-14t14-37.428zM984 553.143q0-16-14.857-28.572T932 512q-15.429 0-28.286 12.857t-12.857 28.286q0 16 12.857 28.857T932 594.857q22.286 0 37.143-12.571T984 553.143zM832 326.286Q814.286 324 792 324q-96.571 0-177.714 44T486.57 487.143 440 651.429q0 44.571 13.143 86.857-20 1.714-38.857 1.714-14.857 0-28.572-0.857t-31.428-3.714-25.429-4-31.143-6-28.571-6L124.57 792l41.143-124.571Q0 551.429 0 387.429q0-96.572 55.714-177.715T206.571 82t207.715-46.571q100.571 0 190 37.714T754 177.429t78 148.857z m338.286 320.571q0 66.857-39.143 127.714t-106 110.572l31.428 103.428-113.714-62.285q-85.714 21.143-124.571 21.143-96.572 0-177.715-40.286T512.857 797.714t-46.571-150.857T512.857 496t127.714-109.429 177.715-40.285q92 0 173.143 40.285t130 109.715 48.857 150.571z" fill="#0e932e" p-id="3163"></path></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@ -51,6 +51,10 @@
|
||||
"sendCode": "Get Security code",
|
||||
"sendText": "Resend in {0}s",
|
||||
"thirdPartyLogin": "Or continue with",
|
||||
"weChat": "WeChat",
|
||||
"qq": "QQ",
|
||||
"gitHub": "GitHub",
|
||||
"google": "Google",
|
||||
"loginAgainTitle": "Please Log In Again",
|
||||
"loginAgainSubTitle": "Your login session has expired. Please log in again to continue.",
|
||||
"layout": {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{
|
||||
"title": "Preferences",
|
||||
"subtitle": "Customize Preferences & Preview in Real Time",
|
||||
"enableStickyPreferencesNavigationBar": "Enable sticky preferences navigation bar",
|
||||
"disableStickyPreferencesNavigationBar": "Disable sticky preferences navigation bar",
|
||||
"resetTip": "Data has changed, click to reset",
|
||||
"resetTitle": "Reset Preferences",
|
||||
"resetSuccess": "Preferences reset successfully",
|
||||
@ -37,6 +39,7 @@
|
||||
"language": "Language",
|
||||
"dynamicTitle": "Dynamic Title",
|
||||
"watermark": "Watermark",
|
||||
"watermarkContent": "Please input Watermark content",
|
||||
"checkUpdates": "Periodic update check",
|
||||
"position": {
|
||||
"title": "Preferences Postion",
|
||||
|
||||
@ -51,6 +51,10 @@
|
||||
"sendCode": "获取验证码",
|
||||
"sendText": "{0}秒后重新获取",
|
||||
"thirdPartyLogin": "其他登录方式",
|
||||
"weChat": "微信",
|
||||
"qq": "QQ",
|
||||
"gitHub": "GitHub",
|
||||
"google": "Google",
|
||||
"loginAgainTitle": "重新登录",
|
||||
"loginAgainSubTitle": "您的登录状态已过期,请重新登录以继续。",
|
||||
"layout": {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{
|
||||
"title": "偏好设置",
|
||||
"subtitle": "自定义偏好设置 & 实时预览",
|
||||
"enableStickyPreferencesNavigationBar": "开启首选项导航栏吸顶效果",
|
||||
"disableStickyPreferencesNavigationBar": "关闭首选项导航栏吸顶效果",
|
||||
"resetTitle": "重置偏好设置",
|
||||
"resetTip": "数据有变化,点击可进行重置",
|
||||
"resetSuccess": "重置偏好设置成功",
|
||||
@ -37,6 +39,7 @@
|
||||
"language": "语言",
|
||||
"dynamicTitle": "动态标题",
|
||||
"watermark": "水印",
|
||||
"watermarkContent": "请输入水印文案",
|
||||
"checkUpdates": "定时检查更新",
|
||||
"position": {
|
||||
"title": "偏好设置位置",
|
||||
|
||||
@ -6,7 +6,7 @@ import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||||
import { useWatermark } from '@vben/hooks';
|
||||
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
||||
import { BookOpenText, CircleHelp, SvgGithubIcon } from '@vben/icons';
|
||||
import {
|
||||
BasicLayout,
|
||||
LockScreen,
|
||||
@ -89,7 +89,7 @@ const menus = computed(() => [
|
||||
target: '_blank',
|
||||
});
|
||||
},
|
||||
icon: MdiGithub,
|
||||
icon: SvgGithubIcon,
|
||||
text: 'GitHub',
|
||||
},
|
||||
{
|
||||
@ -122,11 +122,16 @@ function handleMakeAll() {
|
||||
function handleClickLogo() {}
|
||||
|
||||
watch(
|
||||
() => preferences.app.watermark,
|
||||
async (enable) => {
|
||||
() => ({
|
||||
enable: preferences.app.watermark,
|
||||
content: preferences.app.watermarkContent,
|
||||
}),
|
||||
async ({ enable, content }) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||
content:
|
||||
content ||
|
||||
`${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
|
||||
@ -3,11 +3,7 @@ import { h, ref } from 'vue';
|
||||
|
||||
import { IconPicker, Page } from '@vben/common-ui';
|
||||
import {
|
||||
MdiGithub,
|
||||
MdiGoogle,
|
||||
MdiKeyboardEsc,
|
||||
MdiQqchat,
|
||||
MdiWechat,
|
||||
SvgAvatar1Icon,
|
||||
SvgAvatar2Icon,
|
||||
SvgAvatar3Icon,
|
||||
@ -16,6 +12,10 @@ import {
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
SvgGithubIcon,
|
||||
SvgGoogleIcon,
|
||||
SvgQQChatIcon,
|
||||
SvgWeChatIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import { Card, Input } from 'ant-design-vue';
|
||||
@ -46,10 +46,10 @@ const inputComponent = h(Input);
|
||||
|
||||
<Card class="mb-5" title="Iconify">
|
||||
<div class="flex items-center gap-5">
|
||||
<MdiGithub class="size-8" />
|
||||
<MdiGoogle class="size-8 text-red-500" />
|
||||
<MdiQqchat class="size-8 text-green-500" />
|
||||
<MdiWechat class="size-8" />
|
||||
<SvgGithubIcon class="size-8" />
|
||||
<SvgGoogleIcon class="size-8" />
|
||||
<SvgQQChatIcon class="size-8" />
|
||||
<SvgWeChatIcon class="size-8" />
|
||||
<MdiKeyboardEsc class="size-8" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -8,7 +8,7 @@ const { destroyWatermark, updateWatermark, watermark } = useWatermark();
|
||||
|
||||
async function recreateWaterMark() {
|
||||
destroyWatermark();
|
||||
await updateWatermark({});
|
||||
await createWaterMark();
|
||||
}
|
||||
|
||||
async function createWaterMark() {
|
||||
|
||||
@ -187,7 +187,7 @@ catalog:
|
||||
vue-router: ^4.5.1
|
||||
vue-tippy: ^6.7.1
|
||||
vue-tsc: 2.2.10
|
||||
vxe-pc-ui: ^4.9.29
|
||||
vxe-pc-ui: 4.10.36
|
||||
vxe-table: ^4.16.11
|
||||
watermark-js-plus: ^1.6.2
|
||||
zod: ^3.25.67
|
||||
|
||||
10
turbo.json
10
turbo.json
@ -23,6 +23,16 @@
|
||||
".vitepress/dist/**"
|
||||
]
|
||||
},
|
||||
|
||||
"@vben/web-antd#build:prod": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"@vben/web-antd#build:test": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
|
||||
"preview": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user