mirror of
https://gitee.com/yudaocode/yudao-ui-admin-vben.git
synced 2025-12-30 10:32:25 +00:00
Compare commits
180 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9be7bb5065 | ||
|
|
63a8d562ce | ||
|
|
a109168c66 | ||
|
|
304f2442eb | ||
|
|
826a1b355a | ||
|
|
9ef218f930 | ||
|
|
5a5d2f17da | ||
|
|
70f075b003 | ||
|
|
ab7b77989f | ||
|
|
eacff553bd | ||
|
|
331cb2ca70 | ||
|
|
703561ea33 | ||
|
|
c32a4c3e05 | ||
|
|
77cd814c99 | ||
|
|
3908f03418 | ||
|
|
68bb90a503 | ||
|
|
11b5068e91 | ||
|
|
cb8a633f1f | ||
|
|
a1bb132233 | ||
|
|
022d538940 | ||
|
|
ccf70a1b76 | ||
|
|
6bf9acbfb2 | ||
|
|
13f81b3130 | ||
|
|
835da00f2c | ||
|
|
bc654c9d45 | ||
|
|
0f864b22c1 | ||
|
|
08f6cb7d14 | ||
|
|
3ea5510a21 | ||
|
|
a3f282cba3 | ||
|
|
7ab917dc48 | ||
|
|
e850ffb038 | ||
|
|
1fc39e3fbb | ||
|
|
568d5aa4cf | ||
|
|
535c82c844 | ||
|
|
c1b1343794 | ||
|
|
41f0a9465d | ||
|
|
a7054ec09c | ||
|
|
ebf7221fd2 | ||
|
|
fad560db52 | ||
|
|
503ed01c57 | ||
|
|
b9466282fc | ||
|
|
d3a7a874a6 | ||
|
|
d4b99f321d | ||
|
|
e1e0554aca | ||
|
|
f429e74e79 | ||
|
|
f99d708c97 | ||
|
|
57855eff06 | ||
|
|
348cc35aec | ||
|
|
6922ab13fc | ||
|
|
01dd8171e9 | ||
|
|
0d043bca94 | ||
|
|
9504fa3980 | ||
|
|
a91be61c21 | ||
|
|
b18353e171 | ||
|
|
fbcb498f5b | ||
|
|
32263c2b09 | ||
|
|
2202ef3b3c | ||
|
|
191e15975c | ||
|
|
eb17976e96 | ||
|
|
70982eff92 | ||
|
|
5532f59c40 | ||
|
|
439a35c165 | ||
|
|
42bdc15df5 | ||
|
|
3744069aa2 | ||
|
|
936e127c0d | ||
|
|
ba126288a0 | ||
|
|
1d8d70e71a | ||
|
|
5cb412a4da | ||
|
|
dde16e26fe | ||
|
|
ca8ac99b6e | ||
|
|
e3c1676523 | ||
|
|
4ec82f0fd0 | ||
|
|
ef0f0a9a9d | ||
|
|
b36c3c4209 | ||
|
|
f137a66b6c | ||
|
|
e8526674c5 | ||
|
|
5417b19a8b | ||
|
|
e0d3fac19e | ||
|
|
f943b175eb | ||
|
|
3088fb3d46 | ||
|
|
43f3303ad2 | ||
|
|
bdf1c293bd | ||
|
|
12b0575ca1 | ||
|
|
3102eb511f | ||
|
|
d8c87c0f7c | ||
|
|
f849f3ad3d | ||
|
|
47dfccd9eb | ||
|
|
1d0b14bbe5 | ||
|
|
07c8763fae | ||
|
|
8df5fbc843 | ||
|
|
51ce864dbd | ||
|
|
d50b9fae60 | ||
|
|
0db2710e80 | ||
|
|
380f74015e | ||
|
|
2fc76789e2 | ||
|
|
43ed7aeefb | ||
|
|
28e4305916 | ||
|
|
223be87dc8 | ||
|
|
70bcc5ea0f | ||
|
|
fba43de19f | ||
|
|
f1c7a4ebfb | ||
|
|
67ed1753a7 | ||
|
|
1c17746864 | ||
|
|
2cf7e70b70 | ||
|
|
cfb9a9b3c9 | ||
|
|
2a4c774aca | ||
|
|
250109507f | ||
|
|
03d25bf85a | ||
|
|
2fc86b7bda | ||
|
|
274aa7da73 | ||
|
|
7366b948a3 | ||
|
|
89f93d0291 | ||
|
|
2b270caf30 | ||
|
|
21b5dc255e | ||
|
|
1479f159aa | ||
|
|
5e1abfb08a | ||
|
|
604517b2ab | ||
|
|
d3cfc67bd7 | ||
|
|
5bae28516c | ||
|
|
5a6122ab75 | ||
|
|
5021f2487d | ||
|
|
75a2b331b7 | ||
|
|
cc375100cb | ||
|
|
dea8bf4631 | ||
|
|
d7d883a54c | ||
|
|
943a8e0cee | ||
|
|
9105d4d14a | ||
|
|
62b12235f7 | ||
|
|
00ee233f14 | ||
|
|
ae1c75ae9a | ||
|
|
b94513dee4 | ||
|
|
16f9057e1c | ||
|
|
60400525cc | ||
|
|
c05463ca0a | ||
|
|
a2e6e5097d | ||
|
|
68fc2f6a33 | ||
|
|
a5b51f45da | ||
|
|
05c064a250 | ||
|
|
d16ebea639 | ||
|
|
29e79448e4 | ||
|
|
a9a075346f | ||
|
|
0731999e7d | ||
|
|
aedcf2d05c | ||
|
|
c76db7d8d1 | ||
|
|
df7135b288 | ||
|
|
867ebf2967 | ||
|
|
56669b134c | ||
|
|
e18bbca376 | ||
|
|
86894d6e66 | ||
|
|
2548db3fda | ||
|
|
3f9dc0becc | ||
|
|
22aefe72f4 | ||
|
|
6c9affae76 | ||
|
|
71c80efab0 | ||
|
|
f7ce553771 | ||
|
|
f9913692f0 | ||
|
|
72bbfd4a9c | ||
|
|
7aab11b984 | ||
|
|
09300af7bc | ||
|
|
6fb3480676 | ||
|
|
3409a8a88f | ||
|
|
bdb63cb293 | ||
|
|
0ffebd6de4 | ||
|
|
56409edff4 | ||
|
|
d868e4abfc | ||
|
|
6214c33c86 | ||
|
|
1dc2a31f84 | ||
|
|
0d0d9e30c0 | ||
|
|
ffc48fa171 | ||
|
|
6f39e9136e | ||
|
|
1f1ba16ead | ||
|
|
1d77b018bb | ||
|
|
f7d9d1b1af | ||
|
|
aaf0274fe9 | ||
|
|
c142af482b | ||
|
|
cd7c11c7d0 | ||
|
|
fb8f36eeec | ||
|
|
c3a7562e2c | ||
|
|
0bc7169698 | ||
|
|
24b6e7a835 |
14
README.md
14
README.md
@@ -9,7 +9,7 @@
|
||||
|
||||
## 🐶 新手必读
|
||||
|
||||
- nodejs > 20.12.0 && pnpm > 10.14.0 (强制使用pnpm)
|
||||
- nodejs > 20.12.0 && pnpm > 10.22.0 (强制使用pnpm)
|
||||
- 演示地址【Vue3 + element-plus】:<http://dashboard-vue3.yudao.iocoder.cn>
|
||||
- 演示地址【Vue3 + vben5(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
|
||||
- 演示地址【Vue2 + element-ui】:<http://dashboard.yudao.iocoder.cn>
|
||||
@@ -41,22 +41,22 @@
|
||||
|
||||
| 框架 | 说明 | 版本 |
|
||||
| --- | --- | --- |
|
||||
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.17 |
|
||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 7.1.2 |
|
||||
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.24 |
|
||||
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 7.2.2 |
|
||||
| [Ant Design Vue](https://www.antdv.com/) | Ant Design Vue | 4.2.6 |
|
||||
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.10.2 |
|
||||
| [Naive UI](https://www.naiveui.com/) | Naive UI | 2.42.0 |
|
||||
| [TDesign](https://tdesign.tencent.com/) | TDesign | 1.17.1 |
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 超集 | 5.8.3 |
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 超集 | 5.9.3 |
|
||||
| [pinia](https://pinia.vuejs.org/) | Vue 存储库替代 vuex5 | 3.0.3 |
|
||||
| [vueuse](https://vueuse.org/) | 常用工具集 | 13.4.0 |
|
||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 11.1.7 |
|
||||
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.5.1 |
|
||||
| [Tailwind CSS](https://tailwindcss.com/) | 原子 CSS | 3.4.17 |
|
||||
| [Tailwind CSS](https://tailwindcss.com/) | 原子 CSS | 3.4.18 |
|
||||
| [Iconify](https://iconify.design/) | 图标组件 | 5.0.0 |
|
||||
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.354 |
|
||||
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.406 |
|
||||
| [TinyMCE](https://www.tiny.cloud/) | 富文本编辑器 | 6.1.0 |
|
||||
| [Echarts](https://echarts.apache.org/) | 图表库 | 5.6.0 |
|
||||
| [Echarts](https://echarts.apache.org/) | 图表库 | 6.0.0 |
|
||||
| [axios](https://axios-http.com/) | http客户端 | 1.10.0 |
|
||||
| [dayjs](https://day.js.org/) | 日期处理库 | 1.11.13 |
|
||||
| [vee-validate](https://vee-validate.logaretm.com/) | 表单验证 | 4.15.1 |
|
||||
|
||||
@@ -3,15 +3,31 @@
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
import type {
|
||||
UploadChangeParam,
|
||||
UploadFile,
|
||||
UploadProps,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import type { Component, Ref } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
h,
|
||||
ref,
|
||||
render,
|
||||
unref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
|
||||
@@ -22,9 +38,6 @@ const AutoComplete = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/auto-complete'),
|
||||
);
|
||||
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
||||
const Cascader = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/cascader'),
|
||||
);
|
||||
const Checkbox = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/checkbox'),
|
||||
);
|
||||
@@ -68,7 +81,14 @@ const TimeRangePicker = defineAsyncComponent(() =>
|
||||
const TreeSelect = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/tree-select'),
|
||||
);
|
||||
const Cascader = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/cascader'),
|
||||
);
|
||||
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
||||
const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
|
||||
const PreviewGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/image').then((res) => res.ImagePreviewGroup),
|
||||
);
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
@@ -104,12 +124,223 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
});
|
||||
};
|
||||
|
||||
const withPreviewUpload = () => {
|
||||
return defineComponent({
|
||||
name: Upload.name,
|
||||
emits: ['change', 'update:modelValue'],
|
||||
setup: (
|
||||
props: any,
|
||||
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
|
||||
) => {
|
||||
const previewVisible = ref<boolean>(false);
|
||||
|
||||
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
|
||||
|
||||
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
|
||||
|
||||
const fileList = ref<UploadProps['fileList']>(
|
||||
attrs?.fileList || attrs?.['file-list'] || [],
|
||||
);
|
||||
|
||||
const handleChange = async (event: UploadChangeParam) => {
|
||||
fileList.value = event.fileList;
|
||||
emit('change', event);
|
||||
emit(
|
||||
'update:modelValue',
|
||||
event.fileList?.length ? fileList.value : undefined,
|
||||
);
|
||||
};
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
previewVisible.value = true;
|
||||
await previewImage(file, previewVisible, fileList);
|
||||
};
|
||||
|
||||
const renderUploadButton = (): any => {
|
||||
const isDisabled = attrs.disabled;
|
||||
|
||||
// 如果禁用,不渲染上传按钮
|
||||
if (isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 否则渲染默认上传按钮
|
||||
return isEmpty(slots)
|
||||
? createDefaultSlotsWithUpload(listType, placeholder)
|
||||
: slots;
|
||||
};
|
||||
|
||||
// 可以监听到表单API设置的值
|
||||
watch(
|
||||
() => attrs.modelValue,
|
||||
(res) => {
|
||||
fileList.value = res;
|
||||
},
|
||||
);
|
||||
|
||||
return () =>
|
||||
h(
|
||||
Upload,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
fileList: fileList.value,
|
||||
onChange: handleChange,
|
||||
onPreview: handlePreview,
|
||||
},
|
||||
renderUploadButton(),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createDefaultSlotsWithUpload = (
|
||||
listType: string,
|
||||
placeholder: string,
|
||||
) => {
|
||||
switch (listType) {
|
||||
case 'picture-card': {
|
||||
return {
|
||||
default: () => placeholder,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
icon: h(IconifyIcon, {
|
||||
icon: 'ant-design:upload-outlined',
|
||||
class: 'mb-1 size-4',
|
||||
}),
|
||||
},
|
||||
() => placeholder,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const previewImage = async (
|
||||
file: UploadFile,
|
||||
visible: Ref<boolean>,
|
||||
fileList: Ref<UploadProps['fileList']>,
|
||||
) => {
|
||||
// 检查是否为图片文件的辅助函数
|
||||
const isImageFile = (file: UploadFile): boolean => {
|
||||
const imageExtensions = new Set([
|
||||
'bmp',
|
||||
'gif',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'png',
|
||||
'webp',
|
||||
]);
|
||||
if (file.url) {
|
||||
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||
return ext ? imageExtensions.has(ext) : false;
|
||||
}
|
||||
if (!file.type) {
|
||||
const ext = file.name?.split('.').pop()?.toLowerCase();
|
||||
return ext ? imageExtensions.has(ext) : false;
|
||||
}
|
||||
return file.type.startsWith('image/');
|
||||
};
|
||||
|
||||
// 如果当前文件不是图片,直接打开
|
||||
if (!isImageFile(file)) {
|
||||
if (file.url) {
|
||||
window.open(file.url, '_blank');
|
||||
} else if (file.preview) {
|
||||
window.open(file.preview, '_blank');
|
||||
} else {
|
||||
console.warn('无法打开文件,没有可用的URL或预览地址');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于图片文件,继续使用预览组
|
||||
const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
||||
Image,
|
||||
PreviewGroup,
|
||||
]);
|
||||
|
||||
const getBase64 = (file: File) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => resolve(reader.result));
|
||||
reader.addEventListener('error', (error) => reject(error));
|
||||
});
|
||||
};
|
||||
// 从fileList中过滤出所有图片文件
|
||||
const imageFiles = (unref(fileList) || []).filter((element) =>
|
||||
isImageFile(element),
|
||||
);
|
||||
|
||||
// 为所有没有预览地址的图片生成预览
|
||||
for (const imgFile of imageFiles) {
|
||||
if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
||||
imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
|
||||
}
|
||||
}
|
||||
const container: HTMLElement | null = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
// 用于追踪组件是否已卸载
|
||||
let isUnmounted = false;
|
||||
|
||||
const PreviewWrapper = {
|
||||
setup() {
|
||||
return () => {
|
||||
if (isUnmounted) return null;
|
||||
return h(
|
||||
PreviewGroupComponent,
|
||||
{
|
||||
class: 'hidden',
|
||||
preview: {
|
||||
visible: visible.value,
|
||||
// 设置初始显示的图片索引
|
||||
current: imageFiles.findIndex((f) => f.uid === file.uid),
|
||||
onVisibleChange: (value: boolean) => {
|
||||
visible.value = value;
|
||||
if (!value) {
|
||||
// 延迟清理,确保动画完成
|
||||
setTimeout(() => {
|
||||
if (!isUnmounted && container) {
|
||||
isUnmounted = true;
|
||||
render(null, container);
|
||||
container.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
() =>
|
||||
// 渲染所有图片文件
|
||||
imageFiles.map((imgFile) =>
|
||||
h(ImageComponent, {
|
||||
key: imgFile.uid,
|
||||
src: imgFile.url || imgFile.preview,
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
render(h(PreviewWrapper), container);
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiCascader'
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
| 'Cascader'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
@@ -143,21 +374,13 @@ async function initComponentAdapter() {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiCascader: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiCascader',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: Cascader,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: Cascader,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
ApiSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
@@ -187,6 +410,7 @@ async function initComponentAdapter() {
|
||||
},
|
||||
),
|
||||
AutoComplete,
|
||||
Cascader,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
@@ -221,6 +445,7 @@ async function initComponentAdapter() {
|
||||
TimeRangePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload,
|
||||
PreviewUpload: withPreviewUpload(),
|
||||
FileUpload,
|
||||
ImageUpload,
|
||||
};
|
||||
|
||||
@@ -84,9 +84,10 @@ setupVbenVxeTable({
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field] });
|
||||
return h(Image, { src: row[column.field], ...props });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export namespace BpmModelApi {
|
||||
deploymentTime: number;
|
||||
suspensionState: number;
|
||||
formType?: number;
|
||||
formCustomCreatePath?: string;
|
||||
formCustomViewPath?: string;
|
||||
formFields?: string[];
|
||||
}
|
||||
|
||||
@@ -7,13 +7,29 @@ import { requestClient } from '#/api/request';
|
||||
export namespace BpmTaskApi {
|
||||
/** 流程任务 */
|
||||
export interface Task {
|
||||
id: number; // 编号
|
||||
name: string; // 监听器名字
|
||||
type: string; // 监听器类型
|
||||
status: number; // 监听器状态
|
||||
event: string; // 监听事件
|
||||
valueType: string; // 监听器值类型
|
||||
processInstance?: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
||||
id: string; // 编号
|
||||
name: string; // 任务名字
|
||||
status: number; // 任务状态
|
||||
createTime: number; // 创建时间
|
||||
endTime: number; // 结束时间
|
||||
durationInMillis: number; // 持续时间
|
||||
reason: string; // 审批理由
|
||||
ownerUser: any; // 负责人
|
||||
assigneeUser: any; // 处理人
|
||||
taskDefinitionKey: string; // 任务定义的标识
|
||||
processInstanceId: string; // 流程实例id
|
||||
processInstance: BpmProcessInstanceApi.ProcessInstance; // 流程实例
|
||||
parentTaskId: any; // 父任务id
|
||||
children: any; // 子任务
|
||||
formId: any; // 表单id
|
||||
formName: any; // 表单名称
|
||||
formConf: any; // 表单配置
|
||||
formFields: any; // 表单字段
|
||||
formVariables: any; // 表单变量
|
||||
buttonsSetting: any; // 按钮设置
|
||||
signEnable: any; // 签名设置
|
||||
reasonRequire: any; // 原因设置
|
||||
nodeType: any; // 节点类型
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export namespace InfraFileConfigApi {
|
||||
accessSecret?: string;
|
||||
pathStyle?: boolean;
|
||||
enablePublicAccess?: boolean;
|
||||
region?: string;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,14 +77,6 @@ export namespace IotDeviceApi {
|
||||
}
|
||||
}
|
||||
|
||||
/** IoT 设备状态枚举 */
|
||||
// TODO @haohao:packages/constants/src/biz-iot-enum.ts 枚举;
|
||||
export enum DeviceStateEnum {
|
||||
INACTIVE = 0, // 未激活
|
||||
OFFLINE = 2, // 离线
|
||||
ONLINE = 1, // 在线
|
||||
}
|
||||
|
||||
/** 查询设备分页 */
|
||||
export function getDevicePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<IotDeviceApi.Device>>(
|
||||
@@ -154,6 +146,14 @@ export function importDeviceTemplate() {
|
||||
return requestClient.download('/iot/device/get-import-template');
|
||||
}
|
||||
|
||||
/** 导入设备 */
|
||||
export function importDevice(file: File, updateSupport: boolean) {
|
||||
return requestClient.upload('/iot/device/import', {
|
||||
file,
|
||||
updateSupport,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取设备属性最新数据 */
|
||||
export function getLatestDeviceProperties(params: any) {
|
||||
return requestClient.get<IotDeviceApi.DevicePropertyDetail[]>(
|
||||
|
||||
@@ -27,27 +27,6 @@ export namespace IotProductApi {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO @haohao:packages/constants/src/biz-iot-enum.ts 枚举;
|
||||
|
||||
/** IOT 产品设备类型枚举类 */
|
||||
export enum DeviceTypeEnum {
|
||||
DEVICE = 0, // 直连设备
|
||||
GATEWAY = 2, // 网关设备
|
||||
GATEWAY_SUB = 1, // 网关子设备
|
||||
}
|
||||
|
||||
/** IOT 产品定位类型枚举类 */
|
||||
export enum LocationTypeEnum {
|
||||
IP = 1, // IP 定位
|
||||
MANUAL = 3, // 手动定位
|
||||
MODULE = 2, // 设备定位
|
||||
}
|
||||
|
||||
/** IOT 数据格式(编解码器类型)枚举类 */
|
||||
export enum CodecTypeEnum {
|
||||
ALINK = 'Alink', // 阿里云 Alink 协议
|
||||
}
|
||||
|
||||
/** 查询产品分页 */
|
||||
export function getProductPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<IotProductApi.Product>>(
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace IotStatisticsApi {
|
||||
// TODO @haohao:需要跟后端对齐,必要的 ReqVO、RespVO
|
||||
/** 统计摘要数据 */
|
||||
export interface StatisticsSummary {
|
||||
productCategoryCount: number;
|
||||
productCount: number;
|
||||
deviceCount: number;
|
||||
deviceMessageCount: number;
|
||||
productCategoryTodayCount: number;
|
||||
productTodayCount: number;
|
||||
deviceTodayCount: number;
|
||||
deviceMessageTodayCount: number;
|
||||
deviceOnlineCount: number;
|
||||
deviceOfflineCount: number;
|
||||
deviceInactiveCount: number;
|
||||
productCategoryDeviceCounts: Record<string, number>;
|
||||
export interface StatisticsSummaryRespVO {
|
||||
productCategoryCount: number; // 品类数量
|
||||
productCount: number; // 产品数量
|
||||
deviceCount: number; // 设备数量
|
||||
deviceMessageCount: number; // 上报数量
|
||||
productCategoryTodayCount: number; // 今日新增品类数量
|
||||
productTodayCount: number; // 今日新增产品数量
|
||||
deviceTodayCount: number; // 今日新增设备数量
|
||||
deviceMessageTodayCount: number; // 今日新增上报数量
|
||||
deviceOnlineCount: number; // 在线数量
|
||||
deviceOfflineCount: number; // 离线数量
|
||||
deviceInactiveCount: number; // 待激活设备数量
|
||||
productCategoryDeviceCounts: Record<string, number>; // 按品类统计的设备数量
|
||||
}
|
||||
|
||||
/** 时间戳-数值的键值对类型 */
|
||||
@@ -30,15 +29,15 @@ export namespace IotStatisticsApi {
|
||||
downstreamCounts: TimeValueItem[];
|
||||
}
|
||||
|
||||
/** 消息统计数据项(按日期) */
|
||||
export interface DeviceMessageSummaryByDate {
|
||||
time: string;
|
||||
upstreamCount: number;
|
||||
downstreamCount: number;
|
||||
/** 设备消息数量统计(按日期) */
|
||||
export interface DeviceMessageSummaryByDateRespVO {
|
||||
time: string; // 时间轴
|
||||
upstreamCount: number; // 上行消息数量
|
||||
downstreamCount: number; // 下行消息数量
|
||||
}
|
||||
|
||||
/** 消息统计接口参数 */
|
||||
export interface DeviceMessageReq {
|
||||
/** 设备消息统计请求 */
|
||||
export interface DeviceMessageReqVO {
|
||||
interval: number;
|
||||
times?: string[];
|
||||
}
|
||||
@@ -46,26 +45,17 @@ export namespace IotStatisticsApi {
|
||||
|
||||
/** 获取 IoT 统计摘要数据 */
|
||||
export function getStatisticsSummary() {
|
||||
return requestClient.get<IotStatisticsApi.StatisticsSummary>(
|
||||
return requestClient.get<IotStatisticsApi.StatisticsSummaryRespVO>(
|
||||
'/iot/statistics/get-summary',
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取设备消息的数据统计(按日期) */
|
||||
export function getDeviceMessageSummaryByDate(
|
||||
params: IotStatisticsApi.DeviceMessageReq,
|
||||
params: IotStatisticsApi.DeviceMessageReqVO,
|
||||
) {
|
||||
return requestClient.get<IotStatisticsApi.DeviceMessageSummaryByDate[]>(
|
||||
return requestClient.get<IotStatisticsApi.DeviceMessageSummaryByDateRespVO[]>(
|
||||
'/iot/statistics/get-device-message-summary-by-date',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
// TODO @haohao:貌似这里,没用到?是不是后面哪里用,或者可以删除哈?
|
||||
/** 获取设备消息统计摘要 */
|
||||
export function getDeviceMessageSummary(statType: number) {
|
||||
return requestClient.get<IotStatisticsApi.DeviceMessageSummary>(
|
||||
'/iot/statistics/get-device-message-summary',
|
||||
{ params: { statType } },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export namespace MallCombinationActivityApi {
|
||||
limitDuration?: number; // 限制时长
|
||||
combinationPrice?: number; // 拼团价格
|
||||
products: CombinationProduct[]; // 商品列表
|
||||
picUrl?: any;
|
||||
}
|
||||
|
||||
/** 拼团活动所需属性 */
|
||||
|
||||
@@ -31,6 +31,7 @@ export namespace MallSeckillActivityApi {
|
||||
totalStock?: number; // 秒杀总库存
|
||||
seckillPrice?: number; // 秒杀价格
|
||||
products?: SeckillProduct[]; // 秒杀商品列表
|
||||
picUrl?: any;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ export namespace SystemMailTemplateApi {
|
||||
content: string;
|
||||
params: string[];
|
||||
status: number;
|
||||
remark: string;
|
||||
createTime: Date;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export namespace SystemSocialClientApi {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
agentId?: string;
|
||||
publicKey?: string;
|
||||
status: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as DeptSelectModal } from './dept-select-modal.vue';
|
||||
export { default as UserSelectModal } from './user-select-modal.vue';
|
||||
@@ -51,12 +51,12 @@ const { getStringAccept } = useUploadType({
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
// 计算当前绑定的值,优先使用 modelValue
|
||||
/** 计算当前绑定的值,优先使用 modelValue */
|
||||
const currentValue = computed(() => {
|
||||
return props.modelValue === undefined ? props.value : props.modelValue;
|
||||
});
|
||||
|
||||
// 判断是否使用 modelValue
|
||||
/** 判断是否使用 modelValue */
|
||||
const isUsingModelValue = computed(() => {
|
||||
return props.modelValue !== undefined;
|
||||
});
|
||||
@@ -82,19 +82,21 @@ watch(
|
||||
} else {
|
||||
value.push(v);
|
||||
}
|
||||
fileList.value = value.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.DONE,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}) as UploadProps['fileList'];
|
||||
fileList.value = value
|
||||
.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.DONE,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as UploadProps['fileList'];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
@@ -107,6 +109,7 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/** 处理文件删除 */
|
||||
async function handleRemove(file: UploadFile) {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
@@ -120,17 +123,17 @@ async function handleRemove(file: UploadFile) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件预览
|
||||
/** 处理文件预览 */
|
||||
function handlePreview(file: UploadFile) {
|
||||
emit('preview', file);
|
||||
}
|
||||
|
||||
// 处理文件数量超限
|
||||
/** 处理文件数量超限 */
|
||||
function handleExceed() {
|
||||
message.error($t('ui.upload.maxNumber', [maxNumber.value]));
|
||||
}
|
||||
|
||||
// 处理上传错误
|
||||
/** 处理上传错误 */
|
||||
function handleUploadError(error: any) {
|
||||
console.error('上传错误:', error);
|
||||
message.error($t('ui.upload.uploadError'));
|
||||
@@ -138,6 +141,11 @@ function handleUploadError(error: any) {
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前校验
|
||||
* @param file 待上传的文件
|
||||
* @returns 是否允许上传
|
||||
*/
|
||||
async function beforeUpload(file: File) {
|
||||
const fileContent = await file.text();
|
||||
emit('returnText', fileContent);
|
||||
@@ -171,7 +179,8 @@ async function beforeUpload(file: File) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
/** 自定义上传请求 */
|
||||
async function customRequest(info: UploadRequestOption) {
|
||||
let { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
api = useUpload(props.directory).httpRequest;
|
||||
@@ -196,7 +205,11 @@ async function customRequest(info: UploadRequestOption<any>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传成功
|
||||
/**
|
||||
* 处理上传成功
|
||||
* @param res 上传响应结果
|
||||
* @param file 上传的文件
|
||||
*/
|
||||
function handleUploadSuccess(res: any, file: File) {
|
||||
// 删除临时文件
|
||||
const index = fileList.value?.findIndex((item) => item.name === file.name);
|
||||
@@ -228,6 +241,10 @@ function handleUploadSuccess(res: any, file: File) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文件列表的值
|
||||
* @returns 文件 URL 列表或字符串
|
||||
*/
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||
|
||||
@@ -55,12 +55,12 @@ const { getStringAccept } = useUploadType({
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
// 计算当前绑定的值,优先使用 modelValue
|
||||
/** 计算当前绑定的值,优先使用 modelValue */
|
||||
const currentValue = computed(() => {
|
||||
return props.modelValue === undefined ? props.value : props.modelValue;
|
||||
});
|
||||
|
||||
// 判断是否使用 modelValue
|
||||
/** 判断是否使用 modelValue */
|
||||
const isUsingModelValue = computed(() => {
|
||||
return props.modelValue !== undefined;
|
||||
});
|
||||
@@ -89,19 +89,21 @@ watch(
|
||||
} else {
|
||||
value.push(v);
|
||||
}
|
||||
fileList.value = value.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.DONE,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}) as UploadProps['fileList'];
|
||||
fileList.value = value
|
||||
.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.DONE,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as UploadProps['fileList'];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
@@ -114,6 +116,7 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/** 将文件转换为 Base64 格式 */
|
||||
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -125,6 +128,7 @@ function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理图片预览 */
|
||||
async function handlePreview(file: UploadFile) {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64<string>(file.originFileObj!);
|
||||
@@ -138,6 +142,7 @@ async function handlePreview(file: UploadFile) {
|
||||
);
|
||||
}
|
||||
|
||||
/** 处理文件删除 */
|
||||
async function handleRemove(file: UploadFile) {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
@@ -151,11 +156,17 @@ async function handleRemove(file: UploadFile) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭预览弹窗 */
|
||||
function handleCancel() {
|
||||
previewOpen.value = false;
|
||||
previewTitle.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前校验
|
||||
* @param file 待上传的文件
|
||||
* @returns 是否允许上传
|
||||
*/
|
||||
async function beforeUpload(file: File) {
|
||||
// 检查文件数量限制
|
||||
if (fileList.value!.length >= props.maxNumber) {
|
||||
@@ -186,7 +197,8 @@ async function beforeUpload(file: File) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
/** 自定义上传请求 */
|
||||
async function customRequest(info: UploadRequestOption) {
|
||||
let { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
api = useUpload(props.directory).httpRequest;
|
||||
@@ -211,7 +223,11 @@ async function customRequest(info: UploadRequestOption<any>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传成功
|
||||
/**
|
||||
* 处理上传成功
|
||||
* @param res 上传响应结果
|
||||
* @param file 上传的文件
|
||||
*/
|
||||
function handleUploadSuccess(res: any, file: File) {
|
||||
// 删除临时文件
|
||||
const index = fileList.value?.findIndex((item) => item.name === file.name);
|
||||
@@ -243,14 +259,18 @@ function handleUploadSuccess(res: any, file: File) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传错误
|
||||
/** 处理上传错误 */
|
||||
function handleUploadError(error: any) {
|
||||
console.error('上传错误:', error);
|
||||
message.error('上传错误!!!');
|
||||
message.error($t('ui.upload.uploadError'));
|
||||
// 上传失败时减少计数器
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文件列表的值
|
||||
* @returns 文件 URL 列表或字符串
|
||||
*/
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Col, Input, Row, Textarea } from 'ant-design-vue';
|
||||
import { Input, Textarea } from 'ant-design-vue';
|
||||
|
||||
import FileUpload from './file-upload.vue';
|
||||
|
||||
@@ -30,6 +30,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
/** 处理文件内容返回 */
|
||||
function handleReturnText(text: string) {
|
||||
modelValue.value = text;
|
||||
emits('change', modelValue.value);
|
||||
@@ -37,6 +38,7 @@ function handleReturnText(text: string) {
|
||||
emits('update:modelValue', modelValue.value);
|
||||
}
|
||||
|
||||
/** 计算输入框属性 */
|
||||
const inputProps = computed(() => {
|
||||
return {
|
||||
...props.inputProps,
|
||||
@@ -44,6 +46,7 @@ const inputProps = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
/** 计算文本域属性 */
|
||||
const textareaProps = computed(() => {
|
||||
return {
|
||||
...props.textareaProps,
|
||||
@@ -51,6 +54,7 @@ const textareaProps = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
/** 计算文件上传属性 */
|
||||
const fileUploadProps = computed(() => {
|
||||
return {
|
||||
...props.fileUploadProps,
|
||||
@@ -58,17 +62,17 @@ const fileUploadProps = computed(() => {
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Row>
|
||||
<Col :span="18">
|
||||
<Input readonly v-if="inputType === 'input'" v-bind="inputProps" />
|
||||
<Textarea readonly v-else :row="4" v-bind="textareaProps" />
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<FileUpload
|
||||
class="ml-4"
|
||||
v-bind="fileUploadProps"
|
||||
@return-text="handleReturnText"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<div class="w-full">
|
||||
<Input v-if="inputType === 'input'" readonly v-bind="inputProps">
|
||||
<template #suffix>
|
||||
<FileUpload v-bind="fileUploadProps" @return-text="handleReturnText" />
|
||||
</template>
|
||||
</Input>
|
||||
<div v-else class="relative w-full">
|
||||
<Textarea readonly :rows="4" v-bind="textareaProps" />
|
||||
<div class="absolute bottom-2 right-2">
|
||||
<FileUpload v-bind="fileUploadProps" @return-text="handleReturnText" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,28 +12,21 @@ export enum UploadResultStatus {
|
||||
export type UploadListType = 'picture' | 'picture-card' | 'text';
|
||||
|
||||
export interface FileUploadProps {
|
||||
// 根据后缀,或者其他
|
||||
accept?: string[];
|
||||
accept?: string[]; // 根据后缀,或者其他
|
||||
api?: (
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
// 上传的目录
|
||||
directory?: string;
|
||||
) => Promise<AxiosResponse>;
|
||||
directory?: string; // 上传的目录
|
||||
disabled?: boolean;
|
||||
drag?: boolean; // 是否支持拖拽上传
|
||||
helpText?: string;
|
||||
listType?: UploadListType;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
maxNumber?: number; // 最大数量的文件,Infinity不限制
|
||||
modelValue?: string | string[]; // v-model 支持
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
resultField?: string;
|
||||
// 是否显示下面的描述
|
||||
showDescription?: boolean;
|
||||
maxSize?: number; // 文件最大多少MB
|
||||
multiple?: boolean; // 是否支持多选
|
||||
resultField?: string; // support xxx.xxx.xx
|
||||
showDescription?: boolean; // 是否显示下面的描述
|
||||
value?: string | string[];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,14 @@ enum UPLOAD_TYPE {
|
||||
SERVER = 'server',
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传类型钩子函数
|
||||
* @param acceptRef 接受的文件类型
|
||||
* @param helpTextRef 帮助文本
|
||||
* @param maxNumberRef 最大文件数量
|
||||
* @param maxSizeRef 最大文件大小
|
||||
* @returns 文件类型限制和帮助文本的计算属性
|
||||
*/
|
||||
export function useUploadType({
|
||||
acceptRef,
|
||||
helpTextRef,
|
||||
@@ -78,7 +86,11 @@ export function useUploadType({
|
||||
return { getAccept, getStringAccept, getHelpText };
|
||||
}
|
||||
|
||||
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
|
||||
/**
|
||||
* 上传钩子函数
|
||||
* @param directory 上传目录
|
||||
* @returns 上传 URL 和自定义上传方法
|
||||
*/
|
||||
export function useUpload(directory?: string) {
|
||||
// 后端上传地址
|
||||
const uploadUrl = getUploadUrl();
|
||||
|
||||
@@ -9,24 +9,6 @@ const routes: RouteRecordRaw[] = [
|
||||
hideInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'task',
|
||||
name: 'BpmTask',
|
||||
meta: {
|
||||
title: '审批中心',
|
||||
icon: 'ant-design:history-outlined',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'my',
|
||||
name: 'BpmTaskMy',
|
||||
component: () => import('#/views/bpm/processInstance/index.vue'),
|
||||
meta: {
|
||||
title: '我的流程',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'process-instance/detail',
|
||||
component: () => import('#/views/bpm/processInstance/detail/index.vue'),
|
||||
|
||||
@@ -18,8 +18,7 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '产品详情',
|
||||
activePath: '/iot/device/product',
|
||||
},
|
||||
component: () =>
|
||||
import('#/views/iot/product/product/modules/detail/index.vue'),
|
||||
component: () => import('#/views/iot/product/product/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'device/detail/:id',
|
||||
@@ -28,8 +27,7 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '设备详情',
|
||||
activePath: '/iot/device/device',
|
||||
},
|
||||
component: () =>
|
||||
import('#/views/iot/device/device/modules/detail/index.vue'),
|
||||
component: () => import('#/views/iot/device/device/detail/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'ota/firmware/detail/:id',
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'Append Gateway': '追加网关',
|
||||
'Append Task': '追加任务',
|
||||
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
|
||||
|
||||
TextAnnotation: '文本注释',
|
||||
'Activate the global connect tool': '激活全局连接工具',
|
||||
'Append {type}': '添加 {type}',
|
||||
'Add Lane above': '在上面添加道',
|
||||
@@ -31,10 +31,16 @@ export default {
|
||||
'Create expanded SubProcess': '创建扩展子过程',
|
||||
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
|
||||
'Create Pool/Participant': '创建池/参与者',
|
||||
'Parallel Multi Instance': '并行多重事件',
|
||||
'Sequential Multi Instance': '时序多重事件',
|
||||
'Participant Multiplicity': '参与者多重性',
|
||||
'Empty pool/participant (removes content)': '清空池/参与者(移除内容)',
|
||||
'Empty pool/participant': '收缩池/参与者',
|
||||
'Expanded pool/participant': '展开池/参与者',
|
||||
'Parallel Multi-Instance': '并行多重事件',
|
||||
'Sequential Multi-Instance': '时序多重事件',
|
||||
DataObjectReference: '数据对象参考',
|
||||
DataStoreReference: '数据存储参考',
|
||||
'Data object reference': '数据对象引用 ',
|
||||
'Data store reference': '数据存储引用 ',
|
||||
Loop: '循环',
|
||||
'Ad-hoc': '即席',
|
||||
'Create {type}': '创建 {type}',
|
||||
@@ -49,6 +55,9 @@ export default {
|
||||
'Call Activity': '调用活动',
|
||||
'Sub-Process (collapsed)': '子流程(折叠的)',
|
||||
'Sub-Process (expanded)': '子流程(展开的)',
|
||||
'Ad-hoc sub-process': '即席子流程',
|
||||
'Ad-hoc sub-process (collapsed)': '即席子流程(折叠的)',
|
||||
'Ad-hoc sub-process (expanded)': '即席子流程(展开的)',
|
||||
'Start Event': '开始事件',
|
||||
StartEvent: '开始事件',
|
||||
'Intermediate Throw Event': '中间事件',
|
||||
@@ -111,10 +120,10 @@ export default {
|
||||
'Parallel Gateway': '并行网关',
|
||||
'Inclusive Gateway': '相容网关',
|
||||
'Complex Gateway': '复杂网关',
|
||||
'Event based Gateway': '事件网关',
|
||||
'Event-based Gateway': '事件网关',
|
||||
Transaction: '转运',
|
||||
'Sub Process': '子流程',
|
||||
'Event Sub Process': '事件子流程',
|
||||
'sub-process': '子流程',
|
||||
'Event sub-process': '事件子流程',
|
||||
'Collapsed Pool': '折叠池',
|
||||
'Expanded Pool': '展开池',
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ import 'bpmn-js/dist/assets/diagram-js.css';
|
||||
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
|
||||
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
|
||||
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
|
||||
// TODO @puhui999:样式问题:设计器那,位置不太对;
|
||||
|
||||
export { default as MyProcessDesigner } from './designer';
|
||||
// TODO @puhui999:流程发起时,预览相关的,需要使用;
|
||||
export { default as MyProcessViewer } from './designer/index2';
|
||||
export { default as MyProcessPenal } from './penal';
|
||||
|
||||
@@ -191,7 +191,7 @@ const initFormOnChanged = (element: any) => {
|
||||
conditionFormVisible.value =
|
||||
elementType.value === 'SequenceFlow' &&
|
||||
activatedElement.source &&
|
||||
(activatedElement.source.type as string).includes('StartEvent');
|
||||
!(activatedElement.source.type as string).includes('StartEvent');
|
||||
formVisible.value =
|
||||
elementType.value === 'UserTask' || elementType.value === 'StartEvent';
|
||||
} catch (error) {
|
||||
@@ -390,8 +390,9 @@ watch(() => props.businessObject, syncFromBusinessObject, { deep: true });
|
||||
<template #extra>
|
||||
<IconifyIcon icon="ep:timer" />
|
||||
</template>
|
||||
<!-- 相关 issue:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICNRW2 -->
|
||||
<TimeEventConfig
|
||||
:business-object="bpmnElement.value?.businessObject"
|
||||
:business-object="elementBusinessObject"
|
||||
:key="elementId"
|
||||
/>
|
||||
</CollapsePanel>
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
import { inject, nextTick, ref, toRaw, watch } from 'vue';
|
||||
|
||||
import {
|
||||
Col,
|
||||
Divider,
|
||||
FormItem,
|
||||
InputNumber,
|
||||
RadioButton,
|
||||
RadioGroup,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Switch,
|
||||
TypographyText,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { convertTimeUnit } from '#/views/bpm/components/simple-process-design/components/nodes-config/utils';
|
||||
@@ -73,7 +76,7 @@ const resetElement = () => {
|
||||
// 执行动作
|
||||
timeoutHandlerType.value = elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:TimeoutHandlerType`,
|
||||
)?.[0];
|
||||
);
|
||||
if (timeoutHandlerType.value) {
|
||||
configExtensions.value.push(timeoutHandlerType.value);
|
||||
if (eventDefinition.value.timeCycle) {
|
||||
@@ -243,38 +246,54 @@ watch(
|
||||
</RadioButton>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem label="超时时间设置" v-if="timeoutHandlerEnable">
|
||||
<span class="mr-2">当超过</span>
|
||||
<FormItem name="timeDuration">
|
||||
<InputNumber
|
||||
class="mr-2"
|
||||
:style="{ width: '100px' }"
|
||||
v-model:value="timeDuration"
|
||||
:min="1"
|
||||
:controls="true"
|
||||
@change="
|
||||
() => {
|
||||
updateTimeModdle();
|
||||
updateElementExtensions();
|
||||
}
|
||||
"
|
||||
/>
|
||||
</FormItem>
|
||||
<Select
|
||||
v-model:value="timeUnit"
|
||||
class="mr-2"
|
||||
:style="{ width: '100px' }"
|
||||
@change="onTimeUnitChange"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in TIME_UNIT_TYPES"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
未处理
|
||||
<FormItem
|
||||
label="超时时间设置"
|
||||
v-if="timeoutHandlerEnable"
|
||||
:label-col="{ span: 24 }"
|
||||
:wrapper-col="{ span: 24 }"
|
||||
>
|
||||
<Row :gutter="[0, 0]">
|
||||
<Col>
|
||||
<TypographyText class="mr-2 mt-2 inline-flex text-sm">
|
||||
当超过
|
||||
</TypographyText>
|
||||
</Col>
|
||||
<Col>
|
||||
<FormItem name="timeDuration" class="mb-0">
|
||||
<InputNumber
|
||||
class="mr-2 mt-0.5"
|
||||
v-model:value="timeDuration"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
@change="
|
||||
() => {
|
||||
updateTimeModdle();
|
||||
updateElementExtensions();
|
||||
}
|
||||
"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col>
|
||||
<Select
|
||||
v-model:value="timeUnit"
|
||||
class="mr-2 !w-24"
|
||||
@change="onTimeUnitChange"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in TIME_UNIT_TYPES"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
<TypographyText class="mr-2 mt-2 inline-flex text-sm">
|
||||
未处理
|
||||
</TypographyText>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="最大提醒次数"
|
||||
@@ -295,5 +314,3 @@ watch(
|
||||
</FormItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -8,17 +8,20 @@
|
||||
7. 是否需要签名
|
||||
-->
|
||||
<script lang="ts" setup>
|
||||
import type { ComponentPublicInstance } from 'vue';
|
||||
|
||||
import type { SystemUserApi } from '#/api/system/user';
|
||||
import type { ButtonSetting } from '#/views/bpm/components/simple-process-design/consts';
|
||||
|
||||
import { inject, nextTick, onMounted, ref, toRaw, watch } from 'vue';
|
||||
|
||||
import { BpmModelFormType } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Select,
|
||||
@@ -74,9 +77,67 @@ const assignEmptyUserIdsEl = ref<any>();
|
||||
const assignEmptyUserIds = ref<any>();
|
||||
|
||||
// 操作按钮
|
||||
const buttonsSettingEl = ref<any>();
|
||||
const { btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
|
||||
useButtonsSetting();
|
||||
// const buttonsSettingEl = ref<any>();
|
||||
// const { btnDisplayNameEdit, changeBtnDisplayName } = useButtonsSetting();
|
||||
// const btnDisplayNameBlurEvent = (index: number) => {
|
||||
// btnDisplayNameEdit.value[index] = false;
|
||||
// const buttonItem = buttonsSettingEl.value[index];
|
||||
// buttonItem.displayName =
|
||||
// buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!;
|
||||
// updateElementExtensions();
|
||||
// };
|
||||
|
||||
// 操作按钮设置
|
||||
const {
|
||||
buttonsSetting,
|
||||
btnDisplayNameEdit,
|
||||
changeBtnDisplayName,
|
||||
btnDisplayNameBlurEvent,
|
||||
setInputRef,
|
||||
} = useButtonsSetting();
|
||||
|
||||
/** 操作按钮设置 */
|
||||
function useButtonsSetting() {
|
||||
const buttonsSetting = ref<any[]>([]);
|
||||
// 操作按钮显示名称可编辑
|
||||
const btnDisplayNameEdit = ref<boolean[]>([]);
|
||||
// 输入框的引用数组 - 内部使用,不暴露出去
|
||||
const _btnDisplayNameInputRefs = ref<Array<HTMLInputElement | null>>([]);
|
||||
|
||||
const changeBtnDisplayName = (index: number) => {
|
||||
btnDisplayNameEdit.value[index] = true;
|
||||
// 输入框自动聚集
|
||||
nextTick(() => {
|
||||
if (_btnDisplayNameInputRefs.value[index]) {
|
||||
_btnDisplayNameInputRefs.value[index]?.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const btnDisplayNameBlurEvent = (index: number) => {
|
||||
btnDisplayNameEdit.value[index] = false;
|
||||
const buttonItem = buttonsSetting.value![index];
|
||||
if (buttonItem)
|
||||
buttonItem.displayName =
|
||||
buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!;
|
||||
};
|
||||
|
||||
// 设置 ref 引用的方法
|
||||
const setInputRef = (
|
||||
el: ComponentPublicInstance | Element | null,
|
||||
index: number,
|
||||
) => {
|
||||
_btnDisplayNameInputRefs.value[index] = el as HTMLInputElement;
|
||||
};
|
||||
|
||||
return {
|
||||
buttonsSetting,
|
||||
btnDisplayNameEdit,
|
||||
changeBtnDisplayName,
|
||||
btnDisplayNameBlurEvent,
|
||||
setInputRef,
|
||||
};
|
||||
}
|
||||
|
||||
// 字段权限
|
||||
const fieldsPermissionEl = ref<any[]>([]);
|
||||
@@ -172,12 +233,12 @@ const resetCustomConfigList = () => {
|
||||
});
|
||||
|
||||
// 操作按钮
|
||||
buttonsSettingEl.value = elExtensionElements.value.values?.find(
|
||||
buttonsSetting.value = elExtensionElements.value.values?.filter(
|
||||
(ex: any) => ex.$type === `${prefix}:ButtonsSetting`,
|
||||
);
|
||||
if (buttonsSettingEl.value.length === 0) {
|
||||
if (buttonsSetting.value.length === 0) {
|
||||
DEFAULT_BUTTON_SETTING.forEach((item) => {
|
||||
buttonsSettingEl.value.push(
|
||||
buttonsSetting.value.push(
|
||||
bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, {
|
||||
'flowable:id': item.id,
|
||||
'flowable:displayName': item.displayName,
|
||||
@@ -189,7 +250,7 @@ const resetCustomConfigList = () => {
|
||||
|
||||
// 字段权限
|
||||
if (formType.value === BpmModelFormType.NORMAL) {
|
||||
const fieldsPermissionList = elExtensionElements.value.values?.find(
|
||||
const fieldsPermissionList = elExtensionElements.value.values?.filter(
|
||||
(ex: any) => ex.$type === `${prefix}:FieldsPermission`,
|
||||
);
|
||||
fieldsPermissionEl.value = [];
|
||||
@@ -220,7 +281,7 @@ const resetCustomConfigList = () => {
|
||||
|
||||
// 保留剩余扩展元素,便于后面更新该元素对应属性
|
||||
otherExtensions.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
elExtensionElements.value.values?.filter(
|
||||
(ex: any) =>
|
||||
ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
|
||||
ex.$type !== `${prefix}:RejectHandlerType` &&
|
||||
@@ -281,7 +342,7 @@ const updateElementExtensions = () => {
|
||||
assignEmptyHandlerTypeEl.value,
|
||||
assignEmptyUserIdsEl.value,
|
||||
approveType.value,
|
||||
...buttonsSettingEl.value,
|
||||
...buttonsSetting.value,
|
||||
...fieldsPermissionEl.value,
|
||||
signEnable.value,
|
||||
reasonRequire.value,
|
||||
@@ -351,31 +412,21 @@ function findAllPredecessorsExcludingStart(elementId: string, modeler: any) {
|
||||
return [...predecessors]; // 返回前置节点数组
|
||||
}
|
||||
|
||||
function useButtonsSetting() {
|
||||
const buttonsSetting = ref<ButtonSetting[]>();
|
||||
// 操作按钮显示名称可编辑
|
||||
const btnDisplayNameEdit = ref<boolean[]>([]);
|
||||
const changeBtnDisplayName = (index: number) => {
|
||||
btnDisplayNameEdit.value[index] = true;
|
||||
};
|
||||
const btnDisplayNameBlurEvent = (index: number) => {
|
||||
btnDisplayNameEdit.value[index] = false;
|
||||
const buttonItem = buttonsSetting.value?.[index];
|
||||
if (buttonItem) {
|
||||
buttonItem.displayName =
|
||||
buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!;
|
||||
}
|
||||
};
|
||||
return {
|
||||
buttonsSetting,
|
||||
btnDisplayNameEdit,
|
||||
changeBtnDisplayName,
|
||||
btnDisplayNameBlurEvent,
|
||||
};
|
||||
}
|
||||
// function useButtonsSetting() {
|
||||
// const buttonsSetting = ref<ButtonSetting[]>();
|
||||
// // 操作按钮显示名称可编辑
|
||||
// const btnDisplayNameEdit = ref<boolean[]>([]);
|
||||
// const changeBtnDisplayName = (index: number) => {
|
||||
// btnDisplayNameEdit.value[index] = true;
|
||||
// };
|
||||
// return {
|
||||
// buttonsSetting,
|
||||
// btnDisplayNameEdit,
|
||||
// changeBtnDisplayName,
|
||||
// };
|
||||
// }
|
||||
|
||||
/** 批量更新权限 */
|
||||
// TODO @lesan:这个页面,有一些 idea 红色报错,咱要不要 fix 下!
|
||||
const updatePermission = (type: string) => {
|
||||
fieldsPermissionEl.value.forEach((field: any) => {
|
||||
if (type === 'READ') {
|
||||
@@ -417,13 +468,13 @@ onMounted(async () => {
|
||||
:disabled="returnTaskList.length === 0"
|
||||
@change="updateRejectHandlerType"
|
||||
>
|
||||
<div class="flex-col">
|
||||
<div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
|
||||
<Radio :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</div>
|
||||
</div>
|
||||
<Radio
|
||||
v-for="(item, index) in REJECT_HANDLER_TYPES"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -449,12 +500,12 @@ onMounted(async () => {
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">审批人为空时</Divider>
|
||||
<Form.Item prop="assignEmptyHandlerType">
|
||||
<Form.Item name="assignEmptyHandlerType">
|
||||
<RadioGroup
|
||||
v-model:value="assignEmptyHandlerType"
|
||||
@change="updateAssignEmptyHandlerType"
|
||||
>
|
||||
<div class="flex-col">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
|
||||
<Radio :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
@@ -466,7 +517,7 @@ onMounted(async () => {
|
||||
<Form.Item
|
||||
v-if="assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER"
|
||||
label="指定用户"
|
||||
prop="assignEmptyHandlerUserIds"
|
||||
name="assignEmptyHandlerUserIds"
|
||||
>
|
||||
<Select
|
||||
v-model:value="assignEmptyUserIds"
|
||||
@@ -490,7 +541,7 @@ onMounted(async () => {
|
||||
v-model:value="assignStartUserHandlerType"
|
||||
@change="updateAssignStartUserHandlerType"
|
||||
>
|
||||
<div class="flex-col">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES"
|
||||
:key="index"
|
||||
@@ -503,75 +554,97 @@ onMounted(async () => {
|
||||
</RadioGroup>
|
||||
|
||||
<Divider orientation="left">操作按钮</Divider>
|
||||
<div class="button-setting-pane">
|
||||
<div class="button-setting-title">
|
||||
<div class="button-title-label">操作按钮</div>
|
||||
<div class="button-title-label pl-4">显示名称</div>
|
||||
<div class="button-title-label">启用</div>
|
||||
</div>
|
||||
<div class="mt-2 text-sm">
|
||||
<!-- 头部标题行 -->
|
||||
<div
|
||||
class="button-setting-item"
|
||||
v-for="(item, index) in buttonsSettingEl"
|
||||
:key="index"
|
||||
class="flex items-center justify-between border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold text-slate-900"
|
||||
>
|
||||
<div class="button-setting-item-label">
|
||||
<div class="w-28 text-left">操作按钮</div>
|
||||
<div class="w-40 pl-2 text-left">显示名称</div>
|
||||
<div class="w-20 text-center">启用</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮配置行 -->
|
||||
<div
|
||||
v-for="(item, index) in buttonsSetting"
|
||||
:key="index"
|
||||
class="flex items-center justify-between border border-t-0 border-slate-200 px-3 py-2 text-sm"
|
||||
>
|
||||
<div class="w-28 truncate text-left">
|
||||
{{ OPERATION_BUTTON_NAME.get(item.id) }}
|
||||
</div>
|
||||
<div class="button-setting-item-label">
|
||||
<input
|
||||
type="text"
|
||||
class="editable-title-input"
|
||||
@blur="btnDisplayNameBlurEvent(index)"
|
||||
v-mounted-focus
|
||||
v-model="item.displayName"
|
||||
:placeholder="item.displayName"
|
||||
<div class="flex w-40 items-center truncate text-left">
|
||||
<Input
|
||||
v-if="btnDisplayNameEdit[index]"
|
||||
:ref="(el) => setInputRef(el, index)"
|
||||
@blur="btnDisplayNameBlurEvent(index)"
|
||||
@press-enter="btnDisplayNameBlurEvent(index)"
|
||||
type="text"
|
||||
v-model:value="item.displayName"
|
||||
:placeholder="item.displayName"
|
||||
class="max-w-32 focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||
/>
|
||||
<Button v-else type="text" @click="changeBtnDisplayName(index)">
|
||||
{{ item.displayName }}
|
||||
<Button v-else @click="changeBtnDisplayName(index)">
|
||||
<div class="flex items-center">
|
||||
{{ item.displayName }}
|
||||
<IconifyIcon icon="lucide:edit" class="ml-2" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="button-setting-item-label">
|
||||
<Switch v-model:checked="item.enable" />
|
||||
<div class="flex w-20 items-center justify-center">
|
||||
<Switch
|
||||
v-model:checked="item.enable"
|
||||
@change="updateElementExtensions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider orientation="left">字段权限</Divider>
|
||||
<div class="field-setting-pane" v-if="formType === BpmModelFormType.NORMAL">
|
||||
<div class="field-permit-title">
|
||||
<div class="setting-title-label first-title">字段名称</div>
|
||||
<div class="other-titles">
|
||||
<div v-if="formType === BpmModelFormType.NORMAL" class="mt-2 text-sm">
|
||||
<!-- 头部标题行 -->
|
||||
<div
|
||||
class="flex items-center justify-between border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold text-slate-900"
|
||||
>
|
||||
<div class="w-28 text-left">字段名称</div>
|
||||
<div class="flex flex-1 justify-between">
|
||||
<span
|
||||
class="setting-title-label cursor-pointer"
|
||||
class="inline-block w-24 cursor-pointer text-center hover:text-blue-500"
|
||||
@click="updatePermission('READ')"
|
||||
>只读
|
||||
>
|
||||
只读
|
||||
</span>
|
||||
<span
|
||||
class="setting-title-label cursor-pointer"
|
||||
class="inline-block w-24 cursor-pointer text-center hover:text-blue-500"
|
||||
@click="updatePermission('WRITE')"
|
||||
>
|
||||
可编辑
|
||||
</span>
|
||||
<span
|
||||
class="setting-title-label cursor-pointer"
|
||||
class="inline-block w-24 cursor-pointer text-center hover:text-blue-500"
|
||||
@click="updatePermission('NONE')"
|
||||
>隐藏
|
||||
>
|
||||
隐藏
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字段权限行 -->
|
||||
<div
|
||||
class="field-setting-item"
|
||||
v-for="(item, index) in fieldsPermissionEl"
|
||||
:key="index"
|
||||
class="flex items-center justify-between border border-t-0 border-slate-200 px-3 py-2 text-sm"
|
||||
>
|
||||
<div class="field-setting-item-label">{{ item.title }}</div>
|
||||
<div class="w-28 truncate text-left" :title="item.title">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<RadioGroup
|
||||
class="field-setting-item-group"
|
||||
v-model:value="item.permission"
|
||||
class="flex flex-1 justify-between"
|
||||
>
|
||||
<div class="item-radio-wrap">
|
||||
<div class="flex w-24 items-center justify-center">
|
||||
<Radio
|
||||
class="ml-5"
|
||||
:value="FieldPermissionType.READ"
|
||||
size="large"
|
||||
@change="updateElementExtensions"
|
||||
@@ -579,8 +652,9 @@ onMounted(async () => {
|
||||
<span></span>
|
||||
</Radio>
|
||||
</div>
|
||||
<div class="item-radio-wrap">
|
||||
<div class="flex w-24 items-center justify-center">
|
||||
<Radio
|
||||
class="ml-5"
|
||||
:value="FieldPermissionType.WRITE"
|
||||
size="large"
|
||||
@change="updateElementExtensions"
|
||||
@@ -588,8 +662,9 @@ onMounted(async () => {
|
||||
<span></span>
|
||||
</Radio>
|
||||
</div>
|
||||
<div class="item-radio-wrap">
|
||||
<div class="flex w-24 items-center justify-center">
|
||||
<Radio
|
||||
class="ml-5"
|
||||
:value="FieldPermissionType.NONE"
|
||||
size="large"
|
||||
@change="updateElementExtensions"
|
||||
@@ -602,7 +677,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<Divider orientation="left">是否需要签名</Divider>
|
||||
<Form.Item prop="signEnable">
|
||||
<Form.Item name="signEnable">
|
||||
<Switch
|
||||
v-model:checked="signEnable.value"
|
||||
checked-children="是"
|
||||
@@ -612,7 +687,7 @@ onMounted(async () => {
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">审批意见</Divider>
|
||||
<Form.Item prop="reasonRequire">
|
||||
<Form.Item name="reasonRequire">
|
||||
<Switch
|
||||
v-model:checked="reasonRequire.value"
|
||||
checked-children="必填"
|
||||
@@ -622,162 +697,3 @@ onMounted(async () => {
|
||||
</Form.Item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button-setting-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.button-setting-desc {
|
||||
padding-right: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.button-setting-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 45px;
|
||||
padding-left: 12px;
|
||||
background-color: #f8fafc0a;
|
||||
border: 1px solid #1f38581a;
|
||||
|
||||
& > :first-child {
|
||||
width: 100px !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.button-title-label {
|
||||
width: 150px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.button-setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 38px;
|
||||
padding-left: 12px;
|
||||
border: 1px solid #1f38581a;
|
||||
border-top: 0;
|
||||
|
||||
& > :first-child {
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.button-setting-item-label {
|
||||
width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editable-title-input {
|
||||
max-width: 130px;
|
||||
height: 24px;
|
||||
margin-left: 4px;
|
||||
line-height: 24px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-setting-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
|
||||
.field-setting-desc {
|
||||
padding-right: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-permit-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 45px;
|
||||
padding-left: 12px;
|
||||
line-height: 45px;
|
||||
background-color: #f8fafc0a;
|
||||
border: 1px solid #1f38581a;
|
||||
|
||||
.first-title {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.other-titles {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.setting-title-label {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
padding: 5px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.field-setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 38px;
|
||||
padding-left: 12px;
|
||||
border: 1px solid #1f38581a;
|
||||
border-top: 0;
|
||||
|
||||
.field-setting-item-label {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
min-height: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.field-setting-item-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.item-radio-wrap {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
|
||||
|
||||
import { Form, Input, Select } from 'ant-design-vue';
|
||||
import { Form, FormItem, Input, Select, Textarea } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'FlowCondition' });
|
||||
|
||||
@@ -16,8 +16,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const flowConditionForm = ref<any>({});
|
||||
const bpmnElement = ref();
|
||||
const bpmnElementSource = ref();
|
||||
@@ -153,15 +151,19 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="panel-tab__content">
|
||||
<Form :model="flowConditionForm">
|
||||
<Form.Item label="流转类型">
|
||||
<Form
|
||||
:model="flowConditionForm"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<FormItem label="流转类型">
|
||||
<Select v-model:value="flowConditionForm.type" @change="updateFlowType">
|
||||
<Select.Option value="normal">普通流转路径</Select.Option>
|
||||
<Select.Option value="default">默认流转路径</Select.Option>
|
||||
<Select.Option value="condition">条件流转路径</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="条件格式"
|
||||
v-if="flowConditionForm.type === 'condition'"
|
||||
key="condition"
|
||||
@@ -170,8 +172,8 @@ watch(
|
||||
<Select.Option value="expression">表达式</Select.Option>
|
||||
<Select.Option value="script">脚本</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="表达式"
|
||||
v-if="
|
||||
flowConditionForm.conditionType &&
|
||||
@@ -179,45 +181,45 @@ watch(
|
||||
"
|
||||
key="express"
|
||||
>
|
||||
<Input
|
||||
<Textarea
|
||||
v-model:value="flowConditionForm.body"
|
||||
style="width: 192px"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
allow-clear
|
||||
@change="updateFlowCondition"
|
||||
/>
|
||||
</Form.Item>
|
||||
</FormItem>
|
||||
<template
|
||||
v-if="
|
||||
flowConditionForm.conditionType &&
|
||||
flowConditionForm.conditionType === 'script'
|
||||
"
|
||||
>
|
||||
<Form.Item label="脚本语言" key="language">
|
||||
<FormItem label="脚本语言" key="language">
|
||||
<Input
|
||||
v-model:value="flowConditionForm.language"
|
||||
allow-clear
|
||||
@change="updateFlowCondition"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="脚本类型" key="scriptType">
|
||||
</FormItem>
|
||||
<FormItem label="脚本类型" key="scriptType">
|
||||
<Select v-model:value="flowConditionForm.scriptType">
|
||||
<Select.Option value="inlineScript">内联脚本</Select.Option>
|
||||
<Select.Option value="externalScript">外部脚本</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="脚本"
|
||||
v-if="flowConditionForm.scriptType === 'inlineScript'"
|
||||
key="body"
|
||||
>
|
||||
<TextArea
|
||||
<Textarea
|
||||
v-model:value="flowConditionForm.body"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
allow-clear
|
||||
@change="updateFlowCondition"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="资源地址"
|
||||
v-if="flowConditionForm.scriptType === 'externalScript'"
|
||||
key="resource"
|
||||
@@ -227,7 +229,7 @@ watch(
|
||||
allow-clear
|
||||
@change="updateFlowCondition"
|
||||
/>
|
||||
</Form.Item>
|
||||
</FormItem>
|
||||
</template>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { confirm, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
SelectOption,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import ProcessListenerSelectModal from '#/views/bpm/processListener/components/process-listener-select-modal.vue';
|
||||
|
||||
import { createListenerObject, updateElementExtensions } from '../../utils';
|
||||
import ProcessListenerDialog from './ProcessListenerDialog.vue';
|
||||
import ListenerFieldModal from './ListenerFieldModal.vue';
|
||||
import {
|
||||
fieldType,
|
||||
initListenerForm,
|
||||
@@ -41,29 +41,32 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
const prefix = inject('prefix');
|
||||
const width = inject('width');
|
||||
const elementListenersList = ref<any[]>([]); // 监听器列表
|
||||
const listenerForm = ref<any>({}); // 监听器详情表单
|
||||
const listenerFormModelVisible = ref(false); // 监听器 编辑 侧边栏显示状态
|
||||
const fieldsListOfListener = ref<any[]>([]);
|
||||
const listenerFieldForm = ref<any>({}); // 监听器 注入字段 详情表单
|
||||
const listenerFieldFormModelVisible = ref(false); // 监听器 注入字段表单弹窗 显示状态
|
||||
const editingListenerIndex = ref(-1); // 监听器所在下标,-1 为新增
|
||||
const editingListenerFieldIndex = ref(-1); // 字段所在下标,-1 为新增
|
||||
const listenerTypeObject = ref(listenerType);
|
||||
const fieldTypeObject = ref(fieldType);
|
||||
const bpmnElement = ref();
|
||||
const otherExtensionList = ref();
|
||||
const bpmnElementListeners = ref();
|
||||
const listenerFormRef = ref();
|
||||
const listenerFieldFormRef = ref();
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
const resetListenersList = () => {
|
||||
bpmnElement.value = bpmnInstances().bpmnElement;
|
||||
otherExtensionList.value = [];
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
// 直接使用原始BPMN元素,避免Vue响应式代理问题
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
const businessObject = bpmnElement.businessObject;
|
||||
|
||||
otherExtensionList.value =
|
||||
businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type !== `${prefix}:ExecutionListener`,
|
||||
) ?? []; // 保留非监听器类型的扩展属性,避免移除监听器时清空其他配置(如审批人等)。相关案例:https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICMSYC
|
||||
bpmnElementListeners.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type === `${prefix}:ExecutionListener`,
|
||||
) ?? [];
|
||||
elementListenersList.value = bpmnElementListeners.value.map((listener: any) =>
|
||||
@@ -72,13 +75,12 @@ const resetListenersList = () => {
|
||||
};
|
||||
// 打开 监听器详情 侧边栏
|
||||
const openListenerForm = (listener: any, index: number) => {
|
||||
// debugger
|
||||
if (listener) {
|
||||
listenerForm.value = initListenerForm(listener);
|
||||
editingListenerIndex.value = index;
|
||||
} else {
|
||||
listenerForm.value = {};
|
||||
editingListenerIndex.value = -1; // 标记为新增
|
||||
editingListenerIndex.value = -1;
|
||||
}
|
||||
if (listener && listener.fields) {
|
||||
fieldsListOfListener.value = listener.fields.map((field: any) => ({
|
||||
@@ -89,8 +91,7 @@ const openListenerForm = (listener: any, index: number) => {
|
||||
fieldsListOfListener.value = [];
|
||||
listenerForm.value.fields = [];
|
||||
}
|
||||
// 打开侧边栏并清楚验证状态
|
||||
listenerFormModelVisible.value = true;
|
||||
listenerDrawerApi.open();
|
||||
nextTick(() => {
|
||||
if (listenerFormRef.value) {
|
||||
listenerFormRef.value.clearValidate();
|
||||
@@ -100,87 +101,64 @@ const openListenerForm = (listener: any, index: number) => {
|
||||
|
||||
// 打开监听器字段编辑弹窗
|
||||
const openListenerFieldForm = (field: any, index: number) => {
|
||||
listenerFieldForm.value = field ? cloneDeep(field) : {};
|
||||
const data = field ? cloneDeep(field) : {};
|
||||
editingListenerFieldIndex.value = field ? index : -1;
|
||||
listenerFieldFormModelVisible.value = true;
|
||||
nextTick(() => {
|
||||
if (listenerFieldFormRef.value) {
|
||||
listenerFieldFormRef.value.clearValidate();
|
||||
}
|
||||
});
|
||||
fieldModalApi.setData(data).open();
|
||||
};
|
||||
// 保存监听器注入字段
|
||||
const saveListenerFiled = async () => {
|
||||
// debugger
|
||||
const validateStatus = await listenerFieldFormRef.value.validate();
|
||||
if (!validateStatus) return; // 验证不通过直接返回
|
||||
const saveListenerFiled = async (data: any) => {
|
||||
if (editingListenerFieldIndex.value === -1) {
|
||||
fieldsListOfListener.value.push(listenerFieldForm.value);
|
||||
listenerForm.value.fields.push(listenerFieldForm.value);
|
||||
fieldsListOfListener.value.push(data);
|
||||
listenerForm.value.fields.push(data);
|
||||
} else {
|
||||
fieldsListOfListener.value.splice(
|
||||
editingListenerFieldIndex.value,
|
||||
1,
|
||||
listenerFieldForm.value,
|
||||
);
|
||||
listenerForm.value.fields.splice(
|
||||
editingListenerFieldIndex.value,
|
||||
1,
|
||||
listenerFieldForm.value,
|
||||
);
|
||||
fieldsListOfListener.value.splice(editingListenerFieldIndex.value, 1, data);
|
||||
listenerForm.value.fields.splice(editingListenerFieldIndex.value, 1, data);
|
||||
}
|
||||
listenerFieldFormModelVisible.value = false;
|
||||
nextTick(() => {
|
||||
listenerFieldForm.value = {};
|
||||
});
|
||||
};
|
||||
// 移除监听器字段
|
||||
const removeListenerField = (index: number) => {
|
||||
// debugger
|
||||
Modal.confirm({
|
||||
title: '确认移除该字段吗?',
|
||||
content: '此操作不可撤销',
|
||||
okText: '确 认',
|
||||
cancelText: '取 消',
|
||||
onOk() {
|
||||
fieldsListOfListener.value.splice(index, 1);
|
||||
listenerForm.value.fields.splice(index, 1);
|
||||
},
|
||||
onCancel() {
|
||||
console.warn('操作取消');
|
||||
},
|
||||
confirm({
|
||||
title: '提示',
|
||||
content: '确认移除该字段吗?',
|
||||
}).then(() => {
|
||||
fieldsListOfListener.value.splice(index, 1);
|
||||
listenerForm.value.fields.splice(index, 1);
|
||||
});
|
||||
};
|
||||
// 移除监听器
|
||||
const removeListener = (index: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认移除该监听器吗?',
|
||||
content: '此操作不可撤销',
|
||||
okText: '确 认',
|
||||
cancelText: '取 消',
|
||||
onOk() {
|
||||
bpmnElementListeners.value.splice(index, 1);
|
||||
elementListenersList.value.splice(index, 1);
|
||||
updateElementExtensions(bpmnElement.value, [
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
},
|
||||
onCancel() {
|
||||
console.warn('操作取消');
|
||||
},
|
||||
confirm({
|
||||
title: '提示',
|
||||
content: '确认移除该监听器吗?',
|
||||
}).then(() => {
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
bpmnElementListeners.value.splice(index, 1);
|
||||
elementListenersList.value.splice(index, 1);
|
||||
updateElementExtensions(instances.bpmnElement, [
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
});
|
||||
};
|
||||
// 保存监听器配置
|
||||
const saveListenerConfig = async () => {
|
||||
// debugger
|
||||
const validateStatus = await listenerFormRef.value.validate();
|
||||
if (!validateStatus) return; // 验证不通过直接返回
|
||||
try {
|
||||
await listenerFormRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const listenerObject = createListenerObject(
|
||||
listenerForm.value,
|
||||
false,
|
||||
prefix,
|
||||
);
|
||||
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
|
||||
if (editingListenerIndex.value === -1) {
|
||||
bpmnElementListeners.value.push(listenerObject);
|
||||
elementListenersList.value.push(listenerForm.value);
|
||||
@@ -196,26 +174,115 @@ const saveListenerConfig = async () => {
|
||||
listenerForm.value,
|
||||
);
|
||||
}
|
||||
// 保存其他配置
|
||||
otherExtensionList.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
bpmnElement.businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type !== `${prefix}:ExecutionListener`,
|
||||
) ?? [];
|
||||
updateElementExtensions(bpmnElement.value, [
|
||||
updateElementExtensions(bpmnElement, [
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
// 4. 隐藏侧边栏
|
||||
listenerFormModelVisible.value = false;
|
||||
listenerDrawerApi.close();
|
||||
listenerForm.value = {};
|
||||
};
|
||||
|
||||
// 配置主列表 Grid
|
||||
const [ListenerGrid, listenerGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '序号' },
|
||||
{ field: 'event', title: '事件类型', minWidth: 100 },
|
||||
{
|
||||
field: 'listenerType',
|
||||
title: '监听器类型',
|
||||
minWidth: 100,
|
||||
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||
(listenerTypeObject.value as Record<string, any>)[cellValue],
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 配置字段列表 Grid
|
||||
const [FieldsGrid, fieldsGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '序号' },
|
||||
{ field: 'name', title: '字段名称', minWidth: 100 },
|
||||
{
|
||||
field: 'fieldType',
|
||||
title: '字段类型',
|
||||
minWidth: 80,
|
||||
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||
(fieldTypeObject.value as Record<string, any>)[cellValue],
|
||||
},
|
||||
{
|
||||
title: '字段值/表达式',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }: { row: any }) => row.string || row.expression,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
maxHeight: 200,
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 配置 Drawer
|
||||
const [ListenerDrawer, listenerDrawerApi] = useVbenDrawer({
|
||||
title: '执行监听器',
|
||||
destroyOnClose: true,
|
||||
onConfirm: saveListenerConfig,
|
||||
});
|
||||
|
||||
// 配置字段 Modal
|
||||
const [FieldModal, fieldModalApi] = useVbenModal({
|
||||
connectedComponent: ListenerFieldModal,
|
||||
});
|
||||
|
||||
// 配置选择监听器 Modal
|
||||
const [ProcessListenerSelectModalComp, processListenerSelectModalApi] =
|
||||
useVbenModal({
|
||||
connectedComponent: ProcessListenerSelectModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// 打开监听器弹窗
|
||||
const processListenerDialogRef = ref();
|
||||
const openProcessListenerDialog = async () => {
|
||||
processListenerDialogRef.value.open('execution');
|
||||
processListenerSelectModalApi.setData({ type: 'execution' }).open();
|
||||
};
|
||||
const selectProcessListener = (listener: any) => {
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
const listenerForm = initListenerForm2(listener);
|
||||
const listenerObject = createListenerObject(listenerForm, false, prefix);
|
||||
bpmnElementListeners.value.push(listenerObject);
|
||||
@@ -223,15 +290,31 @@ const selectProcessListener = (listener: any) => {
|
||||
|
||||
// 保存其他配置
|
||||
otherExtensionList.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
bpmnElement.businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type !== `${prefix}:ExecutionListener`,
|
||||
) ?? [];
|
||||
updateElementExtensions(bpmnElement.value, [
|
||||
updateElementExtensions(bpmnElement, [
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
};
|
||||
|
||||
watch(
|
||||
elementListenersList,
|
||||
(val) => {
|
||||
listenerGridApi.setGridOptions({ data: val });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
fieldsListOfListener,
|
||||
(val) => {
|
||||
fieldsGridApi.setGridOptions({ data: val });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.id,
|
||||
(val: string) => {
|
||||
@@ -245,56 +328,44 @@ watch(
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="panel-tab__content">
|
||||
<Table
|
||||
:data-source="elementListenersList"
|
||||
size="small"
|
||||
bordered
|
||||
:pagination="false"
|
||||
>
|
||||
<TableColumn title="序号" width="50px">
|
||||
<template #default="{ index }">
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
</TableColumn>
|
||||
<TableColumn title="事件类型" width="100px" data-index="event" />
|
||||
<TableColumn
|
||||
title="监听器类型"
|
||||
width="100px"
|
||||
:custom-render="
|
||||
({ record }: any) =>
|
||||
listenerTypeObject[record.listenerType as keyof typeof listenerType]
|
||||
"
|
||||
/>
|
||||
<TableColumn title="操作" width="100px">
|
||||
<template #default="{ record, index }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerForm(record, index)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeListener(index)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</TableColumn>
|
||||
</Table>
|
||||
<div class="element-drawer__button">
|
||||
<Button type="primary" size="small" @click="openListenerForm(null, -1)">
|
||||
<div class="-mx-2">
|
||||
<ListenerGrid :data="elementListenersList">
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerForm(row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeListener(rowIndex)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</ListenerGrid>
|
||||
<div class="mt-1 flex w-full items-center justify-center gap-2 px-2">
|
||||
<Button
|
||||
class="flex flex-1 items-center justify-center"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openListenerForm(null, -1)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</template>
|
||||
添加监听器
|
||||
</Button>
|
||||
<Button size="small" @click="openProcessListenerDialog">
|
||||
<Button
|
||||
class="flex flex-1 items-center justify-center"
|
||||
size="small"
|
||||
@click="openProcessListenerDialog"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:select" />
|
||||
</template>
|
||||
@@ -303,13 +374,13 @@ watch(
|
||||
</div>
|
||||
|
||||
<!-- 监听器 编辑/创建 部分 -->
|
||||
<Drawer
|
||||
v-model:open="listenerFormModelVisible"
|
||||
title="执行监听器"
|
||||
:width="width as any"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<Form :model="listenerForm" ref="listenerFormRef">
|
||||
<ListenerDrawer>
|
||||
<Form
|
||||
:model="listenerForm"
|
||||
ref="listenerFormRef"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 19 }"
|
||||
>
|
||||
<FormItem
|
||||
label="事件类型"
|
||||
name="event"
|
||||
@@ -463,8 +534,9 @@ watch(
|
||||
注入字段
|
||||
</span>
|
||||
<Button
|
||||
type="primary"
|
||||
title="添加字段"
|
||||
class="flex items-center"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerFieldForm(null, -1)"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -473,143 +545,32 @@ watch(
|
||||
添加字段
|
||||
</Button>
|
||||
</div>
|
||||
<Table :data-source="fieldsListOfListener" size="small" bordered>
|
||||
<TableColumn title="序号" width="50px">
|
||||
<template #default="{ index }">
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
</TableColumn>
|
||||
<TableColumn title="字段名称" width="100px" data-index="name" />
|
||||
<TableColumn
|
||||
title="字段类型"
|
||||
width="80px"
|
||||
:custom-render="
|
||||
({ record }: any) =>
|
||||
fieldTypeObject[record.fieldType as keyof typeof fieldType]
|
||||
"
|
||||
/>
|
||||
<TableColumn
|
||||
title="字段值/表达式"
|
||||
width="120px"
|
||||
:custom-render="
|
||||
({ record }: any) => record.string || record.expression
|
||||
"
|
||||
/>
|
||||
<TableColumn title="操作" width="80px" fixed="right">
|
||||
<template #default="{ record, index }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerFieldForm(record, index)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeListenerField(index)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</TableColumn>
|
||||
</Table>
|
||||
|
||||
<div class="element-drawer__button">
|
||||
<Button @click="listenerFormModelVisible = false">取 消</Button>
|
||||
<Button type="primary" @click="saveListenerConfig">保 存</Button>
|
||||
</div>
|
||||
</Drawer>
|
||||
<FieldsGrid :data="fieldsListOfListener">
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerFieldForm(row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeListenerField(rowIndex)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</FieldsGrid>
|
||||
</ListenerDrawer>
|
||||
|
||||
<!-- 注入字段 编辑/创建 部分 -->
|
||||
<Modal
|
||||
title="字段配置"
|
||||
v-model:open="listenerFieldFormModelVisible"
|
||||
width="600px"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<Form :model="listenerFieldForm" ref="listenerFieldFormRef">
|
||||
<FormItem
|
||||
label="字段名称:"
|
||||
name="name"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请填写字段名称',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Input v-model:value="listenerFieldForm.name" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="字段类型:"
|
||||
name="fieldType"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择字段类型',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Select v-model:value="listenerFieldForm.fieldType">
|
||||
<SelectOption
|
||||
v-for="i in Object.keys(fieldTypeObject)"
|
||||
:key="i"
|
||||
:value="i"
|
||||
>
|
||||
{{ fieldTypeObject[i as keyof typeof fieldType] }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerFieldForm.fieldType === 'string'"
|
||||
label="字段值:"
|
||||
name="string"
|
||||
key="field-string"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请填写字段值',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Input v-model:value="listenerFieldForm.string" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerFieldForm.fieldType === 'expression'"
|
||||
label="表达式:"
|
||||
name="expression"
|
||||
key="field-expression"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请填写表达式',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Input v-model:value="listenerFieldForm.expression" allow-clear />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button size="small" @click="listenerFieldFormModelVisible = false">
|
||||
取 消
|
||||
</Button>
|
||||
<Button size="small" type="primary" @click="saveListenerFiled">
|
||||
确 定
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<FieldModal @confirm="saveListenerFiled" />
|
||||
</div>
|
||||
|
||||
<!-- 选择弹窗 -->
|
||||
<ProcessListenerDialog
|
||||
ref="processListenerDialogRef"
|
||||
@select="selectProcessListener"
|
||||
/>
|
||||
<ProcessListenerSelectModalComp @select="selectProcessListener" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Form, FormItem, Input, Select, SelectOption } from 'ant-design-vue';
|
||||
|
||||
import { fieldType } from './utilSelf';
|
||||
|
||||
defineOptions({ name: 'ListenerFieldModal' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [data: any];
|
||||
}>();
|
||||
|
||||
const fieldTypeObject = ref(fieldType);
|
||||
const form = ref<any>({});
|
||||
const formRef = ref();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<any>();
|
||||
form.value = data || {};
|
||||
// clear validate
|
||||
setTimeout(() => {
|
||||
formRef.value?.clearValidate();
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
emit('confirm', { ...form.value });
|
||||
await modalApi.close();
|
||||
} catch {
|
||||
// validate failed
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="字段配置" class="w-3/5">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<FormItem
|
||||
label="字段名称:"
|
||||
name="name"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请填写字段名称',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Input v-model:value="form.name" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="字段类型:"
|
||||
name="fieldType"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择字段类型',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Select v-model:value="form.fieldType">
|
||||
<SelectOption
|
||||
v-for="i in Object.keys(fieldTypeObject)"
|
||||
:key="i"
|
||||
:value="i"
|
||||
>
|
||||
{{ fieldTypeObject[i as keyof typeof fieldType] }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="form.fieldType === 'string'"
|
||||
label="字段值:"
|
||||
name="string"
|
||||
key="field-string"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请填写字段值',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Input v-model:value="form.string" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="form.fieldType === 'expression'"
|
||||
label="表达式:"
|
||||
name="expression"
|
||||
key="field-expression"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请填写表达式',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Input v-model:value="form.expression" allow-clear />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,110 +0,0 @@
|
||||
<!-- 执行器选择 -->
|
||||
<script setup lang="ts">
|
||||
import type { BpmProcessListenerApi } from '#/api/bpm/processListener';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||
|
||||
import { Button, Modal, Pagination, Table } from 'ant-design-vue';
|
||||
|
||||
import { getProcessListenerPage } from '#/api/bpm/processListener';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
/** BPM 流程 表单 */
|
||||
defineOptions({ name: 'ProcessListenerDialog' });
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success', 'select']);
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const loading = ref(true); // 列表的加载中
|
||||
const list = ref<BpmProcessListenerApi.ProcessListener[]>([]); // 列表的数据
|
||||
const total = ref(0); // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
type: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
});
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string) => {
|
||||
queryParams.pageNo = 1;
|
||||
queryParams.type = type;
|
||||
await getList();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getProcessListenerPage(queryParams);
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义 success 事件,用于操作成功后的回调
|
||||
const select = async (row: BpmProcessListenerApi.ProcessListener) => {
|
||||
dialogVisible.value = false;
|
||||
// 发送操作成功的事件
|
||||
emit('select', row);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Modal
|
||||
title="请选择监听器"
|
||||
v-model:open="dialogVisible"
|
||||
width="1024px"
|
||||
:footer="null"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:loading="loading"
|
||||
:data-source="list"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
>
|
||||
<Table.Column title="名字" align="center" data-index="name" />
|
||||
<Table.Column title="类型" align="center" data-index="type">
|
||||
<template #default="{ record }">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE"
|
||||
:value="record.type"
|
||||
/>
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column title="事件" align="center" data-index="event" />
|
||||
<Table.Column title="值类型" align="center" data-index="valueType">
|
||||
<template #default="{ record }">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
|
||||
:value="record.valueType"
|
||||
/>
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column title="值" align="center" data-index="value" />
|
||||
<Table.Column title="操作" align="center" fixed="right">
|
||||
<template #default="{ record }">
|
||||
<Button type="primary" @click="select(record)"> 选择 </Button>
|
||||
</template>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,26 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { confirm, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
SelectOption,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import ProcessListenerDialog from '#/views/bpm/components/bpmn-process-designer/package/penal/listeners/ProcessListenerDialog.vue';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import ProcessListenerSelectModal from '#/views/bpm/processListener/components/process-listener-select-modal.vue';
|
||||
|
||||
import { createListenerObject, updateElementExtensions } from '../../utils';
|
||||
import ListenerFieldModal from './ListenerFieldModal.vue';
|
||||
import {
|
||||
eventType,
|
||||
fieldType,
|
||||
@@ -40,59 +39,49 @@ interface Props {
|
||||
}
|
||||
|
||||
const prefix = inject<string>('prefix');
|
||||
const width = inject<number>('width');
|
||||
|
||||
const elementListenersList = ref<any[]>([]);
|
||||
const listenerEventTypeObject = ref(eventType);
|
||||
const listenerTypeObject = ref(listenerType);
|
||||
const listenerFormModelVisible = ref(false);
|
||||
const listenerForm = ref<any>({});
|
||||
const fieldTypeObject = ref(fieldType);
|
||||
const fieldsListOfListener = ref<any[]>([]);
|
||||
const listenerFieldFormModelVisible = ref(false); // 监听器 注入字段表单弹窗 显示状态
|
||||
const editingListenerIndex = ref(-1); // 监听器所在下标,-1 为新增
|
||||
const editingListenerFieldIndex = ref<any>(-1); // 字段所在下标,-1 为新增
|
||||
const listenerFieldForm = ref<any>({}); // 监听器 注入字段 详情表单
|
||||
const bpmnElement = ref<any>();
|
||||
const editingListenerIndex = ref(-1);
|
||||
const editingListenerFieldIndex = ref<any>(-1);
|
||||
const bpmnElementListeners = ref<any[]>([]);
|
||||
const otherExtensionList = ref<any[]>([]);
|
||||
const listenerFormRef = ref<any>({});
|
||||
const listenerFieldFormRef = ref<any>({});
|
||||
|
||||
interface BpmnInstances {
|
||||
bpmnElement: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
bpmnInstances?: BpmnInstances;
|
||||
}
|
||||
}
|
||||
|
||||
const bpmnInstances = () => window.bpmnInstances;
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
const resetListenersList = () => {
|
||||
// console.log(
|
||||
// bpmnInstances().bpmnElement,
|
||||
// 'window.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElementwindow.bpmnInstances.bpmnElement',
|
||||
// );
|
||||
bpmnElement.value = bpmnInstances()?.bpmnElement;
|
||||
otherExtensionList.value = [];
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
// 直接使用原始BPMN元素,避免Vue响应式代理问题
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
const businessObject = bpmnElement.businessObject;
|
||||
|
||||
otherExtensionList.value =
|
||||
businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
||||
) ?? [];
|
||||
bpmnElementListeners.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values.filter(
|
||||
businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type === `${prefix}:TaskListener`,
|
||||
) ?? [];
|
||||
elementListenersList.value = bpmnElementListeners.value.map((listener) =>
|
||||
initListenerType(listener),
|
||||
);
|
||||
};
|
||||
|
||||
const openListenerForm = (listener: any, index?: number) => {
|
||||
if (listener) {
|
||||
listenerForm.value = initListenerForm(listener);
|
||||
editingListenerIndex.value = index || -1;
|
||||
} else {
|
||||
listenerForm.value = {};
|
||||
editingListenerIndex.value = -1; // 标记为新增
|
||||
editingListenerIndex.value = -1;
|
||||
}
|
||||
if (listener && listener.fields) {
|
||||
fieldsListOfListener.value = listener.fields.map((field: any) => ({
|
||||
@@ -103,38 +92,42 @@ const openListenerForm = (listener: any, index?: number) => {
|
||||
fieldsListOfListener.value = [];
|
||||
listenerForm.value.fields = [];
|
||||
}
|
||||
// 打开侧边栏并清楚验证状态
|
||||
listenerFormModelVisible.value = true;
|
||||
listenerDrawerApi.open();
|
||||
nextTick(() => {
|
||||
if (listenerFormRef.value) listenerFormRef.value.clearValidate();
|
||||
});
|
||||
};
|
||||
// 移除监听器
|
||||
|
||||
const removeListener = (_: any, index: number) => {
|
||||
// console.log(listener, 'listener');
|
||||
Modal.confirm({
|
||||
confirm({
|
||||
title: '提示',
|
||||
content: '确认移除该监听器吗?',
|
||||
okText: '确 认',
|
||||
cancelText: '取 消',
|
||||
onOk() {
|
||||
bpmnElementListeners.value.splice(index, 1);
|
||||
elementListenersList.value.splice(index, 1);
|
||||
updateElementExtensions(bpmnElement.value, [
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
},
|
||||
onCancel() {
|
||||
// console.info('操作取消');
|
||||
},
|
||||
}).then(() => {
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
bpmnElementListeners.value.splice(index, 1);
|
||||
elementListenersList.value.splice(index, 1);
|
||||
updateElementExtensions(instances.bpmnElement, [
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
});
|
||||
};
|
||||
// 保存监听器
|
||||
const saveListenerConfig = async () => {
|
||||
const validateStatus = await listenerFormRef.value.validate();
|
||||
if (!validateStatus) return; // 验证不通过直接返回
|
||||
|
||||
async function saveListenerConfig() {
|
||||
try {
|
||||
await listenerFormRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
const listenerObject = createListenerObject(listenerForm.value, true, prefix);
|
||||
|
||||
if (editingListenerIndex.value === -1) {
|
||||
bpmnElementListeners.value.push(listenerObject);
|
||||
elementListenersList.value.push(listenerForm.value);
|
||||
@@ -150,93 +143,174 @@ const saveListenerConfig = async () => {
|
||||
listenerForm.value,
|
||||
);
|
||||
}
|
||||
// 保存其他配置
|
||||
otherExtensionList.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
bpmnElement.businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
||||
) ?? [];
|
||||
updateElementExtensions(bpmnElement.value, [
|
||||
updateElementExtensions(bpmnElement, [
|
||||
...otherExtensionList.value,
|
||||
...bpmnElementListeners.value,
|
||||
]);
|
||||
// 4. 隐藏侧边栏
|
||||
listenerFormModelVisible.value = false;
|
||||
listenerDrawerApi.close();
|
||||
listenerForm.value = {};
|
||||
}
|
||||
|
||||
const openListenerFieldForm = (field: any, index?: number) => {
|
||||
const data = field ? cloneDeep(field) : {};
|
||||
editingListenerFieldIndex.value = field ? index : -1;
|
||||
fieldModalApi.setData(data).open();
|
||||
};
|
||||
|
||||
// 打开监听器字段编辑弹窗
|
||||
const openListenerFieldForm = (field: any, index?: number) => {
|
||||
listenerFieldForm.value = field ? cloneDeep(field) : {};
|
||||
editingListenerFieldIndex.value = field ? index : -1;
|
||||
listenerFieldFormModelVisible.value = true;
|
||||
nextTick(() => {
|
||||
if (listenerFieldFormRef.value) listenerFieldFormRef.value.clearValidate();
|
||||
});
|
||||
};
|
||||
// 保存监听器注入字段
|
||||
const saveListenerFiled = async () => {
|
||||
const validateStatus = await listenerFieldFormRef.value.validate();
|
||||
if (!validateStatus) return; // 验证不通过直接返回
|
||||
const [ListenerGrid, listenerGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '序号' },
|
||||
{
|
||||
field: 'event',
|
||||
title: '事件类型',
|
||||
minWidth: 80,
|
||||
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||
(listenerEventTypeObject.value as Record<string, any>)[cellValue],
|
||||
},
|
||||
{ field: 'id', title: '事件id', minWidth: 80, showOverflow: true },
|
||||
{
|
||||
field: 'listenerType',
|
||||
title: '监听器类型',
|
||||
minWidth: 80,
|
||||
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||
(listenerTypeObject.value as Record<string, any>)[cellValue],
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
async function saveListenerField(data: any) {
|
||||
if (editingListenerFieldIndex.value === -1) {
|
||||
fieldsListOfListener.value.push(listenerFieldForm.value);
|
||||
listenerForm.value.fields.push(listenerFieldForm.value);
|
||||
fieldsListOfListener.value.push(data);
|
||||
listenerForm.value.fields.push(data);
|
||||
} else {
|
||||
fieldsListOfListener.value.splice(
|
||||
editingListenerFieldIndex.value,
|
||||
1,
|
||||
listenerFieldForm.value,
|
||||
);
|
||||
listenerForm.value.fields.splice(
|
||||
editingListenerFieldIndex.value,
|
||||
1,
|
||||
listenerFieldForm.value,
|
||||
);
|
||||
fieldsListOfListener.value.splice(editingListenerFieldIndex.value, 1, data);
|
||||
listenerForm.value.fields.splice(editingListenerFieldIndex.value, 1, data);
|
||||
}
|
||||
listenerFieldFormModelVisible.value = false;
|
||||
nextTick(() => {
|
||||
listenerFieldForm.value = {};
|
||||
});
|
||||
};
|
||||
// 移除监听器字段
|
||||
}
|
||||
|
||||
const removeListenerField = (_: any, index: number) => {
|
||||
// console.log(field, 'field');
|
||||
Modal.confirm({
|
||||
confirm({
|
||||
title: '提示',
|
||||
content: '确认移除该字段吗?',
|
||||
okText: '确 认',
|
||||
cancelText: '取 消',
|
||||
onOk() {
|
||||
fieldsListOfListener.value.splice(index, 1);
|
||||
listenerForm.value.fields.splice(index, 1);
|
||||
},
|
||||
onCancel() {
|
||||
// console.info('操作取消');
|
||||
},
|
||||
}).then(() => {
|
||||
fieldsListOfListener.value.splice(index, 1);
|
||||
listenerForm.value.fields.splice(index, 1);
|
||||
});
|
||||
};
|
||||
|
||||
// 打开监听器弹窗
|
||||
const processListenerDialogRef = ref<any>();
|
||||
const openProcessListenerDialog = async () => {
|
||||
processListenerDialogRef.value.open('task');
|
||||
processListenerSelectModalApi.setData({ type: 'task' }).open();
|
||||
};
|
||||
const selectProcessListener = (listener: any) => {
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
const listenerForm = initListenerForm2(listener);
|
||||
listenerForm.id = listener.id;
|
||||
const listenerObject = createListenerObject(listenerForm, true, prefix);
|
||||
bpmnElementListeners.value.push(listenerObject);
|
||||
elementListenersList.value.push(listenerForm);
|
||||
|
||||
// 保存其他配置
|
||||
otherExtensionList.value =
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
bpmnElement.businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => ex.$type !== `${prefix}:TaskListener`,
|
||||
) ?? [];
|
||||
updateElementExtensions(
|
||||
bpmnElement.value,
|
||||
bpmnElement,
|
||||
otherExtensionList.value?.concat(bpmnElementListeners.value),
|
||||
);
|
||||
};
|
||||
|
||||
const [ListenerDrawer, listenerDrawerApi] = useVbenDrawer({
|
||||
title: '任务监听器',
|
||||
destroyOnClose: true,
|
||||
onConfirm: saveListenerConfig,
|
||||
});
|
||||
|
||||
const [FieldModal, fieldModalApi] = useVbenModal({
|
||||
connectedComponent: ListenerFieldModal,
|
||||
});
|
||||
|
||||
const [ProcessListenerSelectModalComp, processListenerSelectModalApi] =
|
||||
useVbenModal({
|
||||
connectedComponent: ProcessListenerSelectModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [FieldsGrid, fieldsGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '序号' },
|
||||
{ field: 'name', title: '字段名称', minWidth: 100 },
|
||||
{
|
||||
field: 'fieldType',
|
||||
title: '字段类型',
|
||||
width: 80,
|
||||
formatter: ({ cellValue }: { cellValue: string }) =>
|
||||
fieldTypeObject.value[cellValue as keyof typeof fieldType],
|
||||
},
|
||||
{
|
||||
title: '字段值/表达式',
|
||||
width: 100,
|
||||
formatter: ({ row }: { row: any }) => row.string || row.expression,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
minHeight: 200,
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
elementListenersList,
|
||||
(val) => {
|
||||
listenerGridApi.setGridOptions({ data: val });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
fieldsListOfListener,
|
||||
(val) => {
|
||||
fieldsGridApi.setGridOptions({ data: val });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.id,
|
||||
(val) => {
|
||||
@@ -250,257 +324,218 @@ watch(
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="panel-tab__content">
|
||||
<Table :data="elementListenersList" size="small" bordered>
|
||||
<TableColumn title="序号" width="50px" type="index" />
|
||||
<TableColumn
|
||||
title="事件类型"
|
||||
width="80px"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
:custom-render="
|
||||
({ record }: any) =>
|
||||
listenerEventTypeObject[record.event as keyof typeof eventType]
|
||||
"
|
||||
/>
|
||||
<TableColumn
|
||||
title="事件id"
|
||||
width="80px"
|
||||
data-index="id"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
/>
|
||||
<TableColumn
|
||||
title="监听器类型"
|
||||
width="80px"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
:custom-render="
|
||||
({ record }: any) =>
|
||||
listenerTypeObject[record.listenerType as keyof typeof listenerType]
|
||||
"
|
||||
/>
|
||||
<TableColumn title="操作" width="90px">
|
||||
<template #default="{ record, index }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerForm(record, index)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeListener(record, index)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</TableColumn>
|
||||
</Table>
|
||||
<div class="element-drawer__button">
|
||||
<Button size="small" type="primary" @click="openListenerForm(null)">
|
||||
<div class="-mx-2">
|
||||
<ListenerGrid>
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerForm(row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeListener(row, rowIndex)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</ListenerGrid>
|
||||
<div class="mt-1 flex w-full items-center justify-center gap-2 px-2">
|
||||
<Button
|
||||
class="flex flex-1 items-center justify-center"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openListenerForm(null)"
|
||||
>
|
||||
<template #icon> <IconifyIcon icon="ep:plus" /></template>
|
||||
添加监听器
|
||||
</Button>
|
||||
<Button size="small" @click="openProcessListenerDialog">
|
||||
<Button
|
||||
class="flex flex-1 items-center justify-center"
|
||||
size="small"
|
||||
@click="openProcessListenerDialog"
|
||||
>
|
||||
<template #icon> <IconifyIcon icon="ep:select" /></template>
|
||||
选择监听器
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 监听器 编辑/创建 部分 -->
|
||||
<Drawer
|
||||
v-model:open="listenerFormModelVisible"
|
||||
title="任务监听器"
|
||||
:width="width"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<Form :model="listenerForm" ref="listenerFormRef">
|
||||
<FormItem
|
||||
label="事件类型"
|
||||
name="event"
|
||||
:rules="[{ required: true, message: '请选择事件类型' }]"
|
||||
<ListenerDrawer class="w-2/5">
|
||||
<template #default>
|
||||
<Form
|
||||
:label-col="{ span: 6 }"
|
||||
:model="listenerForm"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
ref="listenerFormRef"
|
||||
>
|
||||
<Select v-model:value="listenerForm.event">
|
||||
<SelectOption
|
||||
v-for="i in Object.keys(listenerEventTypeObject)"
|
||||
:key="i"
|
||||
:value="i"
|
||||
>
|
||||
{{ listenerEventTypeObject[i as keyof typeof eventType] }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="监听器ID"
|
||||
name="id"
|
||||
:rules="[{ required: true, message: '请输入监听器ID' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.id" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="监听器类型"
|
||||
name="listenerType"
|
||||
:rules="[{ required: true, message: '请选择监听器类型' }]"
|
||||
>
|
||||
<Select v-model:value="listenerForm.listenerType">
|
||||
<SelectOption
|
||||
v-for="i in Object.keys(listenerTypeObject)"
|
||||
:key="i"
|
||||
:value="i"
|
||||
>
|
||||
{{ listenerTypeObject[i as keyof typeof listenerType] }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.listenerType === 'classListener'"
|
||||
label="Java类"
|
||||
name="class"
|
||||
key="listener-class"
|
||||
:rules="[{ required: true, message: '请输入Java类' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.class" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.listenerType === 'expressionListener'"
|
||||
label="表达式"
|
||||
name="expression"
|
||||
key="listener-expression"
|
||||
:rules="[{ required: true, message: '请输入表达式' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.expression" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
|
||||
label="代理表达式"
|
||||
name="delegateExpression"
|
||||
key="listener-delegate"
|
||||
:rules="[{ required: true, message: '请输入代理表达式' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.delegateExpression" allow-clear />
|
||||
</FormItem>
|
||||
<template v-if="listenerForm.listenerType === 'scriptListener'">
|
||||
<FormItem
|
||||
label="脚本格式"
|
||||
name="scriptFormat"
|
||||
key="listener-script-format"
|
||||
:rules="[{ required: true, message: '请填写脚本格式' }]"
|
||||
label="事件类型"
|
||||
name="event"
|
||||
:rules="[{ required: true, message: '请选择事件类型' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="脚本类型"
|
||||
name="scriptType"
|
||||
key="listener-script-type"
|
||||
:rules="[{ required: true, message: '请选择脚本类型' }]"
|
||||
>
|
||||
<Select v-model:value="listenerForm.scriptType">
|
||||
<SelectOption value="inlineScript">内联脚本</SelectOption>
|
||||
<SelectOption value="externalScript">外部脚本</SelectOption>
|
||||
<Select v-model:value="listenerForm.event">
|
||||
<SelectOption
|
||||
v-for="i in Object.keys(listenerEventTypeObject)"
|
||||
:key="i"
|
||||
:value="i"
|
||||
>
|
||||
{{ listenerEventTypeObject[i as keyof typeof eventType] }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.scriptType === 'inlineScript'"
|
||||
label="脚本内容"
|
||||
name="value"
|
||||
key="listener-script"
|
||||
:rules="[{ required: true, message: '请填写脚本内容' }]"
|
||||
label="监听器ID"
|
||||
name="id"
|
||||
:rules="[{ required: true, message: '请输入监听器ID' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.value" allow-clear />
|
||||
<Input v-model:value="listenerForm.id" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.scriptType === 'externalScript'"
|
||||
label="资源地址"
|
||||
name="resource"
|
||||
key="listener-resource"
|
||||
:rules="[{ required: true, message: '请填写资源地址' }]"
|
||||
label="监听器类型"
|
||||
name="listenerType"
|
||||
:rules="[{ required: true, message: '请选择监听器类型' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.resource" allow-clear />
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<template v-if="listenerForm.event === 'timeout'">
|
||||
<FormItem
|
||||
label="定时器类型"
|
||||
name="eventDefinitionType"
|
||||
key="eventDefinitionType"
|
||||
>
|
||||
<Select v-model:value="listenerForm.eventDefinitionType">
|
||||
<SelectOption value="date">日期</SelectOption>
|
||||
<SelectOption value="duration">持续时长</SelectOption>
|
||||
<SelectOption value="cycle">循环</SelectOption>
|
||||
<SelectOption value="null">无</SelectOption>
|
||||
<Select v-model:value="listenerForm.listenerType">
|
||||
<SelectOption
|
||||
v-for="i in Object.keys(listenerTypeObject)"
|
||||
:key="i"
|
||||
:value="i"
|
||||
>
|
||||
{{ listenerTypeObject[i as keyof typeof listenerType] }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
!!listenerForm.eventDefinitionType &&
|
||||
listenerForm.eventDefinitionType !== 'null'
|
||||
"
|
||||
label="定时器"
|
||||
name="eventTimeDefinitions"
|
||||
key="eventTimeDefinitions"
|
||||
:rules="[{ required: true, message: '请填写定时器配置' }]"
|
||||
v-if="listenerForm.listenerType === 'classListener'"
|
||||
label="Java类"
|
||||
name="class"
|
||||
key="listener-class"
|
||||
:rules="[{ required: true, message: '请输入Java类' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.class" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.listenerType === 'expressionListener'"
|
||||
label="表达式"
|
||||
name="expression"
|
||||
key="listener-expression"
|
||||
:rules="[{ required: true, message: '请输入表达式' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.expression" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.listenerType === 'delegateExpressionListener'"
|
||||
label="代理表达式"
|
||||
name="delegateExpression"
|
||||
key="listener-delegate"
|
||||
:rules="[{ required: true, message: '请输入代理表达式' }]"
|
||||
>
|
||||
<Input
|
||||
v-model:value="listenerForm.eventTimeDefinitions"
|
||||
v-model:value="listenerForm.delegateExpression"
|
||||
allow-clear
|
||||
/>
|
||||
</FormItem>
|
||||
</template>
|
||||
</Form>
|
||||
|
||||
<Divider />
|
||||
<div class="mb-2 flex justify-between">
|
||||
<span class="flex items-center">
|
||||
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
|
||||
注入字段
|
||||
</span>
|
||||
<Button
|
||||
type="primary"
|
||||
title="添加字段"
|
||||
@click="openListenerFieldForm(null)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
<template v-if="listenerForm.listenerType === 'scriptListener'">
|
||||
<FormItem
|
||||
label="脚本格式"
|
||||
name="scriptFormat"
|
||||
key="listener-script-format"
|
||||
:rules="[{ required: true, message: '请填写脚本格式' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.scriptFormat" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="脚本类型"
|
||||
name="scriptType"
|
||||
key="listener-script-type"
|
||||
:rules="[{ required: true, message: '请选择脚本类型' }]"
|
||||
>
|
||||
<Select v-model:value="listenerForm.scriptType">
|
||||
<SelectOption value="inlineScript">内联脚本</SelectOption>
|
||||
<SelectOption value="externalScript">外部脚本</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.scriptType === 'inlineScript'"
|
||||
label="脚本内容"
|
||||
name="value"
|
||||
key="listener-script"
|
||||
:rules="[{ required: true, message: '请填写脚本内容' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.value" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerForm.scriptType === 'externalScript'"
|
||||
label="资源地址"
|
||||
name="resource"
|
||||
key="listener-resource"
|
||||
:rules="[{ required: true, message: '请填写资源地址' }]"
|
||||
>
|
||||
<Input v-model:value="listenerForm.resource" allow-clear />
|
||||
</FormItem>
|
||||
</template>
|
||||
添加字段
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
:data="fieldsListOfListener"
|
||||
size="small"
|
||||
:scroll="{ y: 240 }"
|
||||
bordered
|
||||
style="flex: none"
|
||||
>
|
||||
<TableColumn title="序号" width="50px" type="index" />
|
||||
<TableColumn title="字段名称" width="100px" data-index="name" />
|
||||
<TableColumn
|
||||
title="字段类型"
|
||||
width="80px"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
:custom-render="
|
||||
({ record }: any) =>
|
||||
fieldTypeObject[record.fieldType as keyof typeof fieldType]
|
||||
"
|
||||
/>
|
||||
<TableColumn
|
||||
title="字段值/表达式"
|
||||
width="100px"
|
||||
:ellipsis="{ showTitle: true }"
|
||||
:custom-render="
|
||||
({ record }: any) => record.string || record.expression
|
||||
"
|
||||
/>
|
||||
<TableColumn title="操作" width="100px">
|
||||
<template #default="{ record, index }">
|
||||
|
||||
<template v-if="listenerForm.event === 'timeout'">
|
||||
<FormItem
|
||||
label="定时器类型"
|
||||
name="eventDefinitionType"
|
||||
key="eventDefinitionType"
|
||||
>
|
||||
<Select v-model:value="listenerForm.eventDefinitionType">
|
||||
<SelectOption value="date">日期</SelectOption>
|
||||
<SelectOption value="duration">持续时长</SelectOption>
|
||||
<SelectOption value="cycle">循环</SelectOption>
|
||||
<SelectOption value="null">无</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="
|
||||
!!listenerForm.eventDefinitionType &&
|
||||
listenerForm.eventDefinitionType !== 'null'
|
||||
"
|
||||
label="定时器"
|
||||
name="eventTimeDefinitions"
|
||||
key="eventTimeDefinitions"
|
||||
:rules="[{ required: true, message: '请填写定时器配置' }]"
|
||||
>
|
||||
<Input
|
||||
v-model:value="listenerForm.eventTimeDefinitions"
|
||||
allow-clear
|
||||
/>
|
||||
</FormItem>
|
||||
</template>
|
||||
</Form>
|
||||
|
||||
<Divider />
|
||||
<div class="mb-2 flex justify-between">
|
||||
<span class="flex items-center">
|
||||
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
|
||||
注入字段
|
||||
</span>
|
||||
<Button
|
||||
class="flex items-center"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerFieldForm(null)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon class="size-4" icon="ep:plus" />
|
||||
</template>
|
||||
添加字段
|
||||
</Button>
|
||||
</div>
|
||||
<FieldsGrid>
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openListenerFieldForm(record, index)"
|
||||
@click="openListenerFieldForm(row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
@@ -509,87 +544,19 @@ watch(
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeListenerField(record, index)"
|
||||
@click="removeListenerField(row, rowIndex)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</TableColumn>
|
||||
</Table>
|
||||
|
||||
<div class="element-drawer__button">
|
||||
<Button size="small" @click="listenerFormModelVisible = false">
|
||||
取 消
|
||||
</Button>
|
||||
<Button size="small" type="primary" @click="saveListenerConfig">
|
||||
保 存
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer>
|
||||
</FieldsGrid>
|
||||
</template>
|
||||
</ListenerDrawer>
|
||||
|
||||
<!-- 注入字段 编辑/创建 部分 -->
|
||||
<Modal
|
||||
title="字段配置"
|
||||
v-model:open="listenerFieldFormModelVisible"
|
||||
:width="600"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<Form :model="listenerFieldForm" ref="listenerFieldFormRef">
|
||||
<FormItem
|
||||
label="字段名称:"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: '请输入字段名称' }]"
|
||||
>
|
||||
<Input v-model:value="listenerFieldForm.name" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="字段类型:"
|
||||
name="fieldType"
|
||||
:rules="[{ required: true, message: '请选择字段类型' }]"
|
||||
>
|
||||
<Select v-model:value="listenerFieldForm.fieldType">
|
||||
<SelectOption
|
||||
v-for="i in Object.keys(fieldTypeObject)"
|
||||
:key="i"
|
||||
:value="i"
|
||||
>
|
||||
{{ fieldTypeObject[i as keyof typeof fieldType] }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerFieldForm.fieldType === 'string'"
|
||||
label="字段值:"
|
||||
name="string"
|
||||
key="field-string"
|
||||
:rules="[{ required: true, message: '请输入字段值' }]"
|
||||
>
|
||||
<Input v-model:value="listenerFieldForm.string" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="listenerFieldForm.fieldType === 'expression'"
|
||||
label="表达式:"
|
||||
name="expression"
|
||||
key="field-expression"
|
||||
:rules="[{ required: true, message: '请输入表达式' }]"
|
||||
>
|
||||
<Input v-model:value="listenerFieldForm.expression" allow-clear />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button size="small" @click="listenerFieldFormModelVisible = false">
|
||||
取 消
|
||||
</Button>
|
||||
<Button size="small" type="primary" @click="saveListenerFiled">
|
||||
确 定
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<FieldModal @confirm="saveListenerField" />
|
||||
</div>
|
||||
|
||||
<!-- 选择弹窗 -->
|
||||
<ProcessListenerDialog
|
||||
ref="processListenerDialogRef"
|
||||
@select="selectProcessListener"
|
||||
/>
|
||||
<ProcessListenerSelectModalComp @select="selectProcessListener" />
|
||||
</template>
|
||||
|
||||
@@ -53,6 +53,7 @@ export function initListenerForm2(processListener: any) {
|
||||
class: processListener.value,
|
||||
event: processListener.event,
|
||||
fields: [],
|
||||
id: undefined,
|
||||
};
|
||||
}
|
||||
case 'delegateExpression': {
|
||||
@@ -61,6 +62,7 @@ export function initListenerForm2(processListener: any) {
|
||||
delegateExpression: processListener.value,
|
||||
event: processListener.event,
|
||||
fields: [],
|
||||
id: undefined,
|
||||
};
|
||||
}
|
||||
case 'expression': {
|
||||
@@ -69,6 +71,7 @@ export function initListenerForm2(processListener: any) {
|
||||
expression: processListener.value,
|
||||
event: processListener.event,
|
||||
fields: [],
|
||||
id: undefined,
|
||||
};
|
||||
}
|
||||
// No default
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- eslint-disable unused-imports/no-unused-vars -->
|
||||
<!-- eslint-disable no-unused-vars -->
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
|
||||
|
||||
@@ -73,6 +73,7 @@ declare global {
|
||||
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const getElementLoop = (businessObject: any): void => {
|
||||
if (!businessObject.loopCharacteristics) {
|
||||
loopCharacteristics.value = 'Null';
|
||||
@@ -278,6 +279,8 @@ const approveRatio = ref<number>(100);
|
||||
const otherExtensions = ref<any[]>([]);
|
||||
const getElementLoopNew = (): void => {
|
||||
if (props.type === 'UserTask') {
|
||||
const loopCharacteristics =
|
||||
bpmnElement.value.businessObject?.loopCharacteristics;
|
||||
const extensionElements =
|
||||
bpmnElement.value.businessObject?.extensionElements ??
|
||||
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
|
||||
@@ -294,10 +297,25 @@ const getElementLoopNew = (): void => {
|
||||
approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE;
|
||||
updateLoopCharacteristics();
|
||||
}
|
||||
|
||||
// 如果是按比例会签,从现有 completionCondition 中解析比例,反推到 approveRatio
|
||||
if (
|
||||
approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO &&
|
||||
loopCharacteristics?.completionCondition?.body
|
||||
) {
|
||||
const body = loopCharacteristics.completionCondition.body as string;
|
||||
// 形如 "${ nrOfCompletedInstances/nrOfInstances >= 0.9 }"
|
||||
const match = body.match(/>=\s*(\d+(?:\.\d+)?)/);
|
||||
if (match) {
|
||||
const ratio = Number(match[1]);
|
||||
if (!Number.isNaN(ratio)) {
|
||||
approveRatio.value = ratio * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const onApproveMethodChange = (): void => {
|
||||
approveRatio.value = 100;
|
||||
updateLoopCharacteristics();
|
||||
};
|
||||
const onApproveRatioChange = (): void => {
|
||||
@@ -393,31 +411,29 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel-tab__content">
|
||||
<div class="-mx-2 px-2">
|
||||
<RadioGroup
|
||||
v-if="type === 'UserTask'"
|
||||
v-model:value="approveMethod"
|
||||
@change="onApproveMethodChange"
|
||||
>
|
||||
<div class="flex-col">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-for="(item, index) in APPROVE_METHODS" :key="index">
|
||||
<Radio :value="item.value">
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
<FormItem prop="approveRatio">
|
||||
<InputNumber
|
||||
v-model:value="approveRatio"
|
||||
:min="10"
|
||||
:max="100"
|
||||
:step="10"
|
||||
size="small"
|
||||
v-if="
|
||||
item.value === ApproveMethodType.APPROVE_BY_RATIO &&
|
||||
approveMethod === ApproveMethodType.APPROVE_BY_RATIO
|
||||
"
|
||||
@change="onApproveRatioChange"
|
||||
/>
|
||||
</FormItem>
|
||||
<InputNumber
|
||||
v-if="
|
||||
item.value === ApproveMethodType.APPROVE_BY_RATIO &&
|
||||
approveMethod === ApproveMethodType.APPROVE_BY_RATIO
|
||||
"
|
||||
v-model:value="approveRatio"
|
||||
:min="10"
|
||||
:max="100"
|
||||
:step="10"
|
||||
size="small"
|
||||
@change="onApproveRatioChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
@@ -510,7 +526,7 @@ watch(
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="重试周期"
|
||||
prop="timeCycle"
|
||||
name="timeCycle"
|
||||
v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
|
||||
key="timeCycle"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
|
||||
|
||||
import { Input } from 'ant-design-vue';
|
||||
import { Textarea } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'ElementOtherConfig' });
|
||||
|
||||
@@ -12,8 +12,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const { Textarea } = Input;
|
||||
|
||||
const documentation = ref('');
|
||||
const bpmnElement = ref();
|
||||
|
||||
@@ -58,10 +56,10 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel-tab__content">
|
||||
<div class="element-property input-property">
|
||||
<div class="element-property__label">元素文档:</div>
|
||||
<div class="element-property__value">
|
||||
<div class="px-2 py-1">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-20 pt-1 text-sm text-gray-700">元素文档:</div>
|
||||
<div class="flex-1">
|
||||
<Textarea
|
||||
v-model:value="documentation"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, ref, toRaw, watch } from 'vue';
|
||||
import { inject, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Modal,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from 'ant-design-vue';
|
||||
import { Button, Divider, Form, FormItem, Input } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
defineOptions({ name: 'ElementProperties' });
|
||||
|
||||
@@ -29,13 +23,10 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const prefix = inject('prefix');
|
||||
// const width = inject('width')
|
||||
|
||||
const elementPropertyList = ref<Array<{ name: string; value: string }>>([]);
|
||||
const propertyForm = ref<{ name?: string; value?: string }>({});
|
||||
const editingPropertyIndex = ref(-1);
|
||||
const propertyFormModelVisible = ref(false);
|
||||
const bpmnElement = ref<any>();
|
||||
const otherExtensionList = ref<any[]>([]);
|
||||
const bpmnElementProperties = ref<any[]>([]);
|
||||
const bpmnElementPropertyList = ref<any[]>([]);
|
||||
@@ -43,116 +34,156 @@ const attributeFormRef = ref<any>();
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
const resetAttributesList = () => {
|
||||
bpmnElement.value = bpmnInstances().bpmnElement;
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
// 直接使用原始BPMN元素,避免Vue响应式代理问题
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
const businessObject = bpmnElement.businessObject;
|
||||
|
||||
otherExtensionList.value = []; // 其他扩展配置
|
||||
bpmnElementProperties.value =
|
||||
// bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
|
||||
bpmnElement.value.businessObject?.extensionElements?.values?.filter(
|
||||
(ex: any) => {
|
||||
if (ex.$type !== `${prefix}:Properties`) {
|
||||
otherExtensionList.value.push(ex);
|
||||
}
|
||||
return ex.$type === `${prefix}:Properties`;
|
||||
},
|
||||
) ?? [];
|
||||
businessObject?.extensionElements?.values?.filter((ex: any) => {
|
||||
if (ex.$type !== `${prefix}:Properties`) {
|
||||
otherExtensionList.value.push(ex);
|
||||
}
|
||||
return ex.$type === `${prefix}:Properties`;
|
||||
}) ?? [];
|
||||
|
||||
// 保存所有的 扩展属性字段
|
||||
bpmnElementPropertyList.value = bpmnElementProperties.value.flatMap(
|
||||
(current: any) => current.values,
|
||||
);
|
||||
// 复制 显示
|
||||
elementPropertyList.value = cloneDeep(bpmnElementPropertyList.value ?? []);
|
||||
};
|
||||
|
||||
const openAttributesForm = (
|
||||
attr: null | { name: string; value: string },
|
||||
index: number,
|
||||
) => {
|
||||
editingPropertyIndex.value = index;
|
||||
// @ts-ignore
|
||||
propertyForm.value = index === -1 ? {} : cloneDeep(attr);
|
||||
propertyFormModelVisible.value = true;
|
||||
nextTick(() => {
|
||||
if (attributeFormRef.value) attributeFormRef.value.clearValidate();
|
||||
});
|
||||
};
|
||||
|
||||
const removeAttributes = (
|
||||
_attr: { name: string; value: string },
|
||||
index: number,
|
||||
) => {
|
||||
Modal.confirm({
|
||||
confirm({
|
||||
title: '提示',
|
||||
content: '确认移除该属性吗?',
|
||||
okText: '确 认',
|
||||
cancelText: '取 消',
|
||||
onOk() {
|
||||
elementPropertyList.value.splice(index, 1);
|
||||
bpmnElementPropertyList.value.splice(index, 1);
|
||||
// 新建一个属性字段的保存列表
|
||||
const propertiesObject = bpmnInstances().moddle.create(
|
||||
`${prefix}:Properties`,
|
||||
{
|
||||
values: bpmnElementPropertyList.value,
|
||||
},
|
||||
);
|
||||
updateElementExtensions(propertiesObject);
|
||||
resetAttributesList();
|
||||
},
|
||||
onCancel() {
|
||||
// console.info('操作取消');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const saveAttribute = () => {
|
||||
// console.log(propertyForm.value, 'propertyForm.value');
|
||||
const { name, value } = propertyForm.value;
|
||||
if (editingPropertyIndex.value === -1) {
|
||||
// 新建属性字段
|
||||
const newPropertyObject = bpmnInstances().moddle.create(
|
||||
`${prefix}:Property`,
|
||||
{
|
||||
name,
|
||||
value,
|
||||
},
|
||||
);
|
||||
// 新建一个属性字段的保存列表
|
||||
}).then(() => {
|
||||
elementPropertyList.value.splice(index, 1);
|
||||
bpmnElementPropertyList.value.splice(index, 1);
|
||||
const propertiesObject = bpmnInstances().moddle.create(
|
||||
`${prefix}:Properties`,
|
||||
{
|
||||
values: [...bpmnElementPropertyList.value, newPropertyObject],
|
||||
values: bpmnElementPropertyList.value,
|
||||
},
|
||||
);
|
||||
updateElementExtensions(propertiesObject);
|
||||
resetAttributesList();
|
||||
});
|
||||
};
|
||||
|
||||
const saveAttribute = async () => {
|
||||
try {
|
||||
await attributeFormRef.value?.validate();
|
||||
} catch {
|
||||
// 校验未通过,直接返回
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, value } = propertyForm.value;
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
|
||||
if (editingPropertyIndex.value === -1) {
|
||||
// 新建属性字段
|
||||
const newPropertyObject = instances.moddle.create(`${prefix}:Property`, {
|
||||
name,
|
||||
value,
|
||||
});
|
||||
// 新建一个属性字段的保存列表
|
||||
const propertiesObject = instances.moddle.create(`${prefix}:Properties`, {
|
||||
values: [...bpmnElementPropertyList.value, newPropertyObject],
|
||||
});
|
||||
updateElementExtensions(propertiesObject);
|
||||
} else {
|
||||
bpmnInstances().modeling.updateModdleProperties(
|
||||
toRaw(bpmnElement.value),
|
||||
toRaw(bpmnElementPropertyList.value)[toRaw(editingPropertyIndex.value)],
|
||||
instances.modeling.updateModdleProperties(
|
||||
bpmnElement,
|
||||
bpmnElementPropertyList.value[editingPropertyIndex.value],
|
||||
{
|
||||
name,
|
||||
value,
|
||||
},
|
||||
);
|
||||
}
|
||||
propertyFormModelVisible.value = false;
|
||||
fieldModalApi.close();
|
||||
resetAttributesList();
|
||||
};
|
||||
|
||||
const updateElementExtensions = (properties: any) => {
|
||||
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
const extensions = instances.moddle.create('bpmn:ExtensionElements', {
|
||||
values: [...otherExtensionList.value, properties],
|
||||
});
|
||||
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
|
||||
instances.modeling.updateProperties(bpmnElement, {
|
||||
extensionElements: extensions,
|
||||
});
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '序号' },
|
||||
{ field: 'name', title: '属性名', minWidth: 120 },
|
||||
{ field: 'value', title: '属性值', minWidth: 120 },
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [FieldModal, fieldModalApi] = useVbenModal({
|
||||
title: '属性配置',
|
||||
onConfirm: saveAttribute,
|
||||
});
|
||||
|
||||
const openAttributesForm = (
|
||||
attr: null | { name: string; value: string },
|
||||
index: number,
|
||||
) => {
|
||||
editingPropertyIndex.value = index;
|
||||
propertyForm.value = index === -1 ? {} : cloneDeep(attr || {});
|
||||
fieldModalApi.open();
|
||||
nextTick(() => {
|
||||
if (attributeFormRef.value) attributeFormRef.value.clearValidate();
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
elementPropertyList,
|
||||
(val) => {
|
||||
gridApi.setGridOptions({ data: val });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.id,
|
||||
(val) => {
|
||||
if (val) {
|
||||
val && val.length > 0 && resetAttributesList();
|
||||
if (val && val.length > 0) {
|
||||
resetAttributesList();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -160,38 +191,34 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel-tab__content">
|
||||
<Table :data="elementPropertyList" size="small" bordered>
|
||||
<TableColumn title="序号" width="50">
|
||||
<template #default="{ index }">
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
</TableColumn>
|
||||
<TableColumn title="属性名" data-index="name" />
|
||||
<TableColumn title="属性值" data-index="value" />
|
||||
<TableColumn title="操作">
|
||||
<template #default="{ record, index }">
|
||||
<Button
|
||||
type="link"
|
||||
@click="openAttributesForm(record, index)"
|
||||
size="small"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="removeAttributes(record, index)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</TableColumn>
|
||||
</Table>
|
||||
<div class="element-drawer__button">
|
||||
<Button type="primary" @click="openAttributesForm(null, -1)">
|
||||
<div class="-mx-2">
|
||||
<Grid :data="elementPropertyList">
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openAttributesForm(row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeAttributes(row, rowIndex)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
<div class="mt-1 flex w-full items-center justify-center gap-2 px-2">
|
||||
<Button
|
||||
class="flex flex-1 items-center justify-center"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openAttributesForm(null, -1)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</template>
|
||||
@@ -199,24 +226,28 @@ watch(
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
v-model:open="propertyFormModelVisible"
|
||||
title="属性配置"
|
||||
:width="600"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<Form :model="propertyForm" ref="attributeFormRef">
|
||||
<FormItem label="属性名:" name="name">
|
||||
<FieldModal class="w-3/5">
|
||||
<Form
|
||||
:model="propertyForm"
|
||||
ref="attributeFormRef"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 17 }"
|
||||
>
|
||||
<FormItem
|
||||
label="属性名:"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: '请输入属性名' }]"
|
||||
>
|
||||
<Input v-model:value="propertyForm.name" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem label="属性值:" name="value">
|
||||
<FormItem
|
||||
label="属性值:"
|
||||
name="value"
|
||||
:rules="[{ required: true, message: '请输入属性值' }]"
|
||||
>
|
||||
<Input v-model:value="propertyForm.value" allow-clear />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="propertyFormModelVisible = false">取 消</Button>
|
||||
<Button type="primary" @click="saveAttribute">确 定</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</FieldModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from 'ant-design-vue';
|
||||
import { Button, Divider, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import SignalMessageModal from './SignalMessageModal.vue';
|
||||
|
||||
defineOptions({ name: 'SignalAndMassage' });
|
||||
const signalList = ref<any[]>([]);
|
||||
const messageList = ref<any[]>([]);
|
||||
const dialogVisible = ref(false);
|
||||
const modelType = ref('');
|
||||
const modelObjectForm = ref<any>({});
|
||||
const modelType = ref<'message' | 'signal'>('message');
|
||||
const rootElements = ref();
|
||||
const messageIdMap = ref();
|
||||
const signalIdMap = ref();
|
||||
const modelConfig = computed(() => {
|
||||
return modelType.value === 'message'
|
||||
? { title: '创建消息', idLabel: '消息ID', nameLabel: '消息名称' }
|
||||
: { title: '创建信号', idLabel: '信号ID', nameLabel: '信号名称' };
|
||||
});
|
||||
const editingIndex = ref(-1); // 正在编辑的索引,-1 表示新建
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
// 生成规范化的ID
|
||||
const generateStandardId = (type: string): string => {
|
||||
const prefix = type === 'message' ? 'Message_' : 'Signal_';
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).slice(2, 6).toUpperCase();
|
||||
return `${prefix}${timestamp}_${random}`;
|
||||
};
|
||||
|
||||
const initDataList = () => {
|
||||
// console.log(window, 'window');
|
||||
rootElements.value = bpmnInstances().modeler.getDefinitions().rootElements;
|
||||
messageIdMap.value = {};
|
||||
signalIdMap.value = {};
|
||||
@@ -48,103 +45,289 @@ const initDataList = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
const openModel = (type: any) => {
|
||||
|
||||
const openModel = (type: 'message' | 'signal') => {
|
||||
modelType.value = type;
|
||||
modelObjectForm.value = {};
|
||||
dialogVisible.value = true;
|
||||
editingIndex.value = -1;
|
||||
modalApi
|
||||
.setData({
|
||||
id: generateStandardId(type),
|
||||
isEdit: false,
|
||||
name: '',
|
||||
type,
|
||||
})
|
||||
.open();
|
||||
};
|
||||
const addNewObject = () => {
|
||||
|
||||
const openEditModel = (type: 'message' | 'signal', row: any, index: number) => {
|
||||
modelType.value = type;
|
||||
editingIndex.value = index;
|
||||
modalApi
|
||||
.setData({
|
||||
id: row.id,
|
||||
isEdit: true,
|
||||
name: row.name,
|
||||
type,
|
||||
})
|
||||
.open();
|
||||
};
|
||||
|
||||
const handleConfirm = (formData: { id: string; name: string }) => {
|
||||
if (modelType.value === 'message') {
|
||||
if (messageIdMap.value[modelObjectForm.value.id]) {
|
||||
message.error('该消息已存在,请修改id后重新保存');
|
||||
if (editingIndex.value === -1) {
|
||||
// 新建模式
|
||||
if (messageIdMap.value[formData.id]) {
|
||||
message.error('该消息已存在,请修改id后重新保存');
|
||||
return;
|
||||
}
|
||||
const messageRef = bpmnInstances().moddle.create(
|
||||
'bpmn:Message',
|
||||
formData,
|
||||
);
|
||||
rootElements.value.push(messageRef);
|
||||
} else {
|
||||
// 编辑模式
|
||||
const targetMessage = messageList.value[editingIndex.value];
|
||||
const rootMessage = rootElements.value.find(
|
||||
(el: any) => el.$type === 'bpmn:Message' && el.id === targetMessage.id,
|
||||
);
|
||||
if (rootMessage) {
|
||||
rootMessage.id = formData.id;
|
||||
rootMessage.name = formData.name;
|
||||
}
|
||||
}
|
||||
const messageRef = bpmnInstances().moddle.create(
|
||||
'bpmn:Message',
|
||||
modelObjectForm.value,
|
||||
);
|
||||
rootElements.value.push(messageRef);
|
||||
} else {
|
||||
if (signalIdMap.value[modelObjectForm.value.id]) {
|
||||
message.error('该信号已存在,请修改id后重新保存');
|
||||
if (editingIndex.value === -1) {
|
||||
// 新建模式
|
||||
if (signalIdMap.value[formData.id]) {
|
||||
message.error('该信号已存在,请修改id后重新保存');
|
||||
return;
|
||||
}
|
||||
const signalRef = bpmnInstances().moddle.create('bpmn:Signal', formData);
|
||||
rootElements.value.push(signalRef);
|
||||
} else {
|
||||
// 编辑模式
|
||||
const targetSignal = signalList.value[editingIndex.value];
|
||||
const rootSignal = rootElements.value.find(
|
||||
(el: any) => el.$type === 'bpmn:Signal' && el.id === targetSignal.id,
|
||||
);
|
||||
if (rootSignal) {
|
||||
rootSignal.id = formData.id;
|
||||
rootSignal.name = formData.name;
|
||||
}
|
||||
}
|
||||
const signalRef = bpmnInstances().moddle.create(
|
||||
'bpmn:Signal',
|
||||
modelObjectForm.value,
|
||||
);
|
||||
rootElements.value.push(signalRef);
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
// 触发建模器更新以保存更改
|
||||
saveChanges();
|
||||
initDataList();
|
||||
};
|
||||
|
||||
// 补充"编辑"、"移除"功能。相关 issue:https://github.com/YunaiV/yudao-cloud/issues/270
|
||||
const removeObject = (type: any, row: any) => {
|
||||
confirm({
|
||||
title: '提示',
|
||||
content: `确认移除该${type === 'message' ? '消息' : '信号'}吗?`,
|
||||
}).then(() => {
|
||||
// 从 rootElements 中移除
|
||||
const targetType = type === 'message' ? 'bpmn:Message' : 'bpmn:Signal';
|
||||
const elementIndex = rootElements.value.findIndex(
|
||||
(el: any) => el.$type === targetType && el.id === row.id,
|
||||
);
|
||||
if (elementIndex !== -1) {
|
||||
rootElements.value.splice(elementIndex, 1);
|
||||
}
|
||||
// 刷新列表
|
||||
initDataList();
|
||||
message.success('移除成功');
|
||||
});
|
||||
};
|
||||
|
||||
// 触发建模器更新以保存更改
|
||||
const saveChanges = () => {
|
||||
const modeler = bpmnInstances().modeler;
|
||||
if (!modeler) return;
|
||||
|
||||
try {
|
||||
// 获取 canvas,通过它来触发图表的重新渲染
|
||||
const canvas = modeler.get('canvas');
|
||||
|
||||
// 获取根元素(Process)
|
||||
const rootElement = canvas.getRootElement();
|
||||
|
||||
// 触发 changed 事件,通知建模器数据已更改
|
||||
const eventBus = modeler.get('eventBus');
|
||||
if (eventBus) {
|
||||
eventBus.fire('root.added', { element: rootElement });
|
||||
eventBus.fire('elements.changed', { elements: [rootElement] });
|
||||
}
|
||||
|
||||
// 标记建模器为已修改状态
|
||||
const commandStack = modeler.get('commandStack');
|
||||
if (commandStack && commandStack._stack) {
|
||||
// 添加一个空命令以标记为已修改
|
||||
commandStack.execute('element.updateProperties', {
|
||||
element: rootElement,
|
||||
properties: {},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('保存更改时出错:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const [MessageGrid, messageGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '序号' },
|
||||
{ field: 'id', title: '消息ID', minWidth: 120 },
|
||||
{ field: 'name', title: '消息名称', minWidth: 100 },
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [SignalGrid, signalGridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '序号' },
|
||||
{ field: 'id', title: '信号ID', minWidth: 120 },
|
||||
{ field: 'name', title: '信号名称', minWidth: 100 },
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
connectedComponent: SignalMessageModal,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initDataList();
|
||||
});
|
||||
|
||||
watch(
|
||||
messageList,
|
||||
(val) => {
|
||||
messageGridApi.setGridOptions({ data: val });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
signalList,
|
||||
(val) => {
|
||||
signalGridApi.setGridOptions({ data: val });
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="panel-tab__content">
|
||||
<div class="panel-tab__content--title">
|
||||
<div class="-mx-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
|
||||
消息列表
|
||||
</span>
|
||||
<Button type="primary" title="创建新消息" @click="openModel('message')">
|
||||
<Button
|
||||
class="flex items-center"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openModel('message')"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</template>
|
||||
创建新消息
|
||||
</Button>
|
||||
</div>
|
||||
<Table :data-source="messageList" size="small" bordered>
|
||||
<TableColumn title="序号" width="60px">
|
||||
<template #default="{ index }">
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
</TableColumn>
|
||||
<TableColumn title="消息ID" data-index="id" />
|
||||
<TableColumn title="消息名称" data-index="name" />
|
||||
</Table>
|
||||
<div class="panel-tab__content--title mt-2 border-t border-gray-200 pt-2">
|
||||
<MessageGrid :data="messageList">
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openEditModel('message', row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeObject('message', row)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</MessageGrid>
|
||||
<div
|
||||
class="mb-2 mt-2 flex items-center justify-between border-t border-gray-200 pt-2"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<IconifyIcon icon="ep:menu" class="mr-2 text-gray-600" />
|
||||
信号列表
|
||||
</span>
|
||||
<Button type="primary" title="创建新信号" @click="openModel('signal')">
|
||||
<Button
|
||||
class="flex items-center"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openModel('signal')"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</template>
|
||||
创建新信号
|
||||
</Button>
|
||||
</div>
|
||||
<Table :data-source="signalList" size="small" bordered>
|
||||
<TableColumn title="序号" width="60px">
|
||||
<template #default="{ index }">
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
</TableColumn>
|
||||
<TableColumn title="信号ID" data-index="id" />
|
||||
<TableColumn title="信号名称" data-index="name" />
|
||||
</Table>
|
||||
|
||||
<Modal
|
||||
v-model:open="dialogVisible"
|
||||
:title="modelConfig.title"
|
||||
:mask-closable="false"
|
||||
width="400px"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<Form :model="modelObjectForm">
|
||||
<FormItem :label="modelConfig.idLabel">
|
||||
<Input v-model:value="modelObjectForm.id" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem :label="modelConfig.nameLabel">
|
||||
<Input v-model:value="modelObjectForm.name" allow-clear />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="dialogVisible = false">取 消</Button>
|
||||
<Button type="primary" @click="addNewObject">保 存</Button>
|
||||
<SignalGrid :data="signalList">
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openEditModel('signal', row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeObject('signal', row)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</SignalGrid>
|
||||
|
||||
<Modal @confirm="handleConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Form, FormItem, Input } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'SignalMessageModal' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [data: { id: string; name: string }];
|
||||
}>();
|
||||
|
||||
const formRef = ref();
|
||||
const form = ref<{ id: string; name: string }>({ id: '', name: '' });
|
||||
const modelType = ref<'message' | 'signal'>('message');
|
||||
const isEdit = ref(false);
|
||||
|
||||
const config = computed(() => {
|
||||
return modelType.value === 'message'
|
||||
? {
|
||||
title: isEdit.value ? '编辑消息' : '创建消息',
|
||||
idLabel: '消息 ID',
|
||||
nameLabel: '消息名称',
|
||||
}
|
||||
: {
|
||||
title: isEdit.value ? '编辑信号' : '创建信号',
|
||||
idLabel: '信号 ID',
|
||||
nameLabel: '信号名称',
|
||||
};
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<{
|
||||
id?: string;
|
||||
isEdit?: boolean;
|
||||
name?: string;
|
||||
type: 'message' | 'signal';
|
||||
}>();
|
||||
modelType.value = data?.type || 'message';
|
||||
isEdit.value = data?.isEdit || false;
|
||||
form.value = {
|
||||
id: data?.id || '',
|
||||
name: data?.name || '',
|
||||
};
|
||||
// 清除校验
|
||||
setTimeout(() => {
|
||||
formRef.value?.clearValidate();
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
async onConfirm() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
emit('confirm', { ...form.value });
|
||||
modalApi.close();
|
||||
} catch {
|
||||
// 校验未通过
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="config.title" class="w-3/5">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<FormItem
|
||||
:label="config.idLabel"
|
||||
name="id"
|
||||
:rules="[{ required: true, message: '请输入 ID' }]"
|
||||
>
|
||||
<Input v-model:value="form.id" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
:label="config.nameLabel"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: '请输入名称' }]"
|
||||
>
|
||||
<Input v-model:value="form.name" allow-clear />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, inject, nextTick, ref, toRaw, watch } from 'vue';
|
||||
import { inject, nextTick, onMounted, ref, toRaw, watch } from 'vue';
|
||||
|
||||
import { alert } from '@vben/common-ui';
|
||||
import { PlusOutlined } from '@vben/icons';
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
SelectOption,
|
||||
Switch,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getModelList } from '#/api/bpm/model';
|
||||
|
||||
interface FormData {
|
||||
processInstanceName: string;
|
||||
calledElement: string;
|
||||
@@ -44,8 +46,7 @@ const inVariableList = ref<any[]>([]);
|
||||
const outVariableList = ref<any[]>([]);
|
||||
const variableType = ref<string>(); // 参数类型
|
||||
const editingVariableIndex = ref<number>(-1); // 编辑参数下标
|
||||
const variableDialogVisible = ref<boolean>(false);
|
||||
const varialbeFormRef = ref<any>();
|
||||
const varialbeFormRef = ref();
|
||||
const varialbeFormData = ref<{
|
||||
source: string;
|
||||
target: string;
|
||||
@@ -57,10 +58,10 @@ const varialbeFormData = ref<{
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
const bpmnElement = ref<any>();
|
||||
const otherExtensionList = ref<any[]>([]);
|
||||
const childProcessOptions = ref<{ key: string; name: string }[]>([]);
|
||||
|
||||
const initCallActivity = () => {
|
||||
bpmnElement.value = bpmnInstances().bpmnElement;
|
||||
// console.log(bpmnElement.value.businessObject, 'callActivity');
|
||||
|
||||
// 初始化所有配置项
|
||||
Object.keys(formData.value).forEach((key: string) => {
|
||||
@@ -85,11 +86,6 @@ const initCallActivity = () => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 默认添加
|
||||
// bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
|
||||
// calledElementType: 'key'
|
||||
// })
|
||||
};
|
||||
|
||||
const updateCallActivityAttr = (attr: keyof FormData) => {
|
||||
@@ -98,16 +94,26 @@ const updateCallActivityAttr = (attr: keyof FormData) => {
|
||||
});
|
||||
};
|
||||
|
||||
const [VariableModal, variableModalApi] = useVbenModal({
|
||||
title: '参数配置',
|
||||
onConfirm: () => {
|
||||
saveVariable();
|
||||
},
|
||||
});
|
||||
|
||||
const openVariableForm = (type: string, data: any, index: number) => {
|
||||
editingVariableIndex.value = index;
|
||||
variableType.value = type;
|
||||
varialbeFormData.value = index === -1 ? {} : { ...data };
|
||||
variableDialogVisible.value = true;
|
||||
variableModalApi.open();
|
||||
};
|
||||
|
||||
const removeVariable = async (type: string, index: number) => {
|
||||
try {
|
||||
await alert('是否确认删除?');
|
||||
await confirm({
|
||||
title: '提示',
|
||||
content: '是否确认删除?',
|
||||
});
|
||||
if (type === 'in') {
|
||||
inVariableList.value.splice(index, 1);
|
||||
}
|
||||
@@ -115,10 +121,19 @@ const removeVariable = async (type: string, index: number) => {
|
||||
outVariableList.value.splice(index, 1);
|
||||
}
|
||||
updateElementExtensions();
|
||||
} catch {}
|
||||
} catch (error: any) {
|
||||
console.error(`[removeVariable error ]: ${error.message || error}`);
|
||||
}
|
||||
};
|
||||
|
||||
const saveVariable = () => {
|
||||
const saveVariable = async () => {
|
||||
try {
|
||||
await varialbeFormRef.value?.validate();
|
||||
} catch {
|
||||
// 验证失败直接返回
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingVariableIndex.value === -1) {
|
||||
if (variableType.value === 'in') {
|
||||
inVariableList.value.push(
|
||||
@@ -149,7 +164,7 @@ const saveVariable = () => {
|
||||
varialbeFormData.value.target;
|
||||
}
|
||||
}
|
||||
variableDialogVisible.value = false;
|
||||
variableModalApi.close();
|
||||
};
|
||||
|
||||
const updateElementExtensions = () => {
|
||||
@@ -176,28 +191,93 @@ watch(
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const gridOptions = {
|
||||
columns: [
|
||||
{ title: '源', field: 'source', minWidth: 100 },
|
||||
{ title: '目标', field: 'target', minWidth: 100 },
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
slots: { default: 'action' },
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
],
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
toolbarConfig: { enabled: false },
|
||||
pagerConfig: { enabled: false },
|
||||
};
|
||||
|
||||
const [InVariableGrid, inVariableGridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [OutVariableGrid, outVariableGridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
// 使用浅层监听,避免无限循环
|
||||
watch(
|
||||
() => [...inVariableList.value],
|
||||
(val) => {
|
||||
inVariableGridApi.setGridOptions({ data: val });
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [...outVariableList.value],
|
||||
(val) => {
|
||||
outVariableGridApi.setGridOptions({ data: val });
|
||||
},
|
||||
);
|
||||
|
||||
/** 选择子流程, 更新 bpmn callActivity calledElement 和 processInstanceName 属性 */
|
||||
const handleChildProcessChange = (key: any) => {
|
||||
if (!key) return;
|
||||
const selected = childProcessOptions.value.find((item) => item.key === key);
|
||||
if (selected) {
|
||||
formData.value.calledElement = selected.key;
|
||||
formData.value.processInstanceName = selected.name;
|
||||
updateCallActivityAttr('calledElement');
|
||||
updateCallActivityAttr('processInstanceName');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 获取流程模型列表
|
||||
const list = await getModelList(undefined);
|
||||
childProcessOptions.value = list.map((item) => ({
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取子流程列表失败', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Form>
|
||||
<FormItem label="实例名称">
|
||||
<Input
|
||||
v-model:value="formData.processInstanceName"
|
||||
allow-clear
|
||||
placeholder="请输入实例名称"
|
||||
@change="updateCallActivityAttr('processInstanceName')"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<!-- TODO 需要可选择已存在的流程 -->
|
||||
<FormItem label="被调用流程">
|
||||
<Input
|
||||
<div class="-mx-2">
|
||||
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<FormItem label="被调用子流程">
|
||||
<Select
|
||||
v-model:value="formData.calledElement"
|
||||
placeholder="请选择子流程"
|
||||
allow-clear
|
||||
placeholder="请输入被调用流程"
|
||||
@change="updateCallActivityAttr('calledElement')"
|
||||
/>
|
||||
@change="handleChildProcessChange"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="item in childProcessOptions"
|
||||
:key="item.key"
|
||||
:value="item.key"
|
||||
:label="item.name"
|
||||
>
|
||||
{{ item.name }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="继承变量">
|
||||
@@ -223,134 +303,115 @@ watch(
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<Divider />
|
||||
<div>
|
||||
<div class="mb-10px flex">
|
||||
<span>输入参数</span>
|
||||
<Button
|
||||
class="ml-auto"
|
||||
type="primary"
|
||||
:icon="h(PlusOutlined)"
|
||||
title="添加参数"
|
||||
size="small"
|
||||
@click="openVariableForm('in', null, -1)"
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
:data-source="inVariableList"
|
||||
:scroll="{ y: 240 }"
|
||||
bordered
|
||||
:pagination="false"
|
||||
<div
|
||||
class="mb-1 mt-2 flex items-center justify-between border-t border-gray-200 pt-2"
|
||||
>
|
||||
<span class="flex items-center text-sm font-medium"> 输入参数 </span>
|
||||
<Button
|
||||
class="flex items-center"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openVariableForm('in', null, -1)"
|
||||
>
|
||||
<TableColumn
|
||||
title="源"
|
||||
data-index="source"
|
||||
:min-width="100"
|
||||
:ellipsis="true"
|
||||
/>
|
||||
<TableColumn
|
||||
title="目标"
|
||||
data-index="target"
|
||||
:min-width="100"
|
||||
:ellipsis="true"
|
||||
/>
|
||||
<TableColumn title="操作" :width="110">
|
||||
<template #default="{ record, index }">
|
||||
<Button
|
||||
type="link"
|
||||
@click="openVariableForm('in', record, index)"
|
||||
size="small"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="removeVariable('in', index)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</TableColumn>
|
||||
</Table>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</template>
|
||||
添加参数
|
||||
</Button>
|
||||
</div>
|
||||
<InVariableGrid class="-mx-2 mb-4">
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openVariableForm('in', row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeVariable('in', rowIndex)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</InVariableGrid>
|
||||
|
||||
<Divider />
|
||||
<div>
|
||||
<div class="mb-10px flex">
|
||||
<span>输出参数</span>
|
||||
<Button
|
||||
class="ml-auto"
|
||||
type="primary"
|
||||
:icon="h(PlusOutlined)"
|
||||
title="添加参数"
|
||||
size="small"
|
||||
@click="openVariableForm('out', null, -1)"
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
:data-source="outVariableList"
|
||||
:scroll="{ y: 240 }"
|
||||
bordered
|
||||
:pagination="false"
|
||||
<div
|
||||
class="mb-1 mt-2 flex items-center justify-between border-t border-gray-200 pt-2"
|
||||
>
|
||||
<span class="flex items-center text-sm font-medium"> 输出参数 </span>
|
||||
<Button
|
||||
class="flex items-center"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openVariableForm('out', null, -1)"
|
||||
>
|
||||
<TableColumn
|
||||
title="源"
|
||||
data-index="source"
|
||||
:min-width="100"
|
||||
:ellipsis="true"
|
||||
/>
|
||||
<TableColumn
|
||||
title="目标"
|
||||
data-index="target"
|
||||
:min-width="100"
|
||||
:ellipsis="true"
|
||||
/>
|
||||
<TableColumn title="操作" :width="110">
|
||||
<template #default="{ record, index }">
|
||||
<Button
|
||||
type="link"
|
||||
@click="openVariableForm('out', record, index)"
|
||||
size="small"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="removeVariable('out', index)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</TableColumn>
|
||||
</Table>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:plus" class="size-4" />
|
||||
</template>
|
||||
添加参数
|
||||
</Button>
|
||||
</div>
|
||||
<OutVariableGrid class="-mx-2">
|
||||
<template #action="{ row, rowIndex }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openVariableForm('out', row, rowIndex)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="removeVariable('out', rowIndex)"
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
</template>
|
||||
</OutVariableGrid>
|
||||
</Form>
|
||||
|
||||
<!-- 添加或修改参数 -->
|
||||
<Modal
|
||||
v-model:open="variableDialogVisible"
|
||||
title="参数配置"
|
||||
:width="600"
|
||||
:destroy-on-close="true"
|
||||
@ok="saveVariable"
|
||||
@cancel="variableDialogVisible = false"
|
||||
>
|
||||
<Form :model="varialbeFormData" ref="varialbeFormRef">
|
||||
<FormItem label="源:" name="source">
|
||||
<VariableModal>
|
||||
<Form
|
||||
:model="varialbeFormData"
|
||||
ref="varialbeFormRef"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<FormItem
|
||||
label="源"
|
||||
name="source"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '源不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Input v-model:value="varialbeFormData.source" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem label="目标:" name="target">
|
||||
<FormItem
|
||||
label="目标"
|
||||
name="target"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '目标不能为空',
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Input v-model:value="varialbeFormData.target" allow-clear />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</VariableModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Input } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'HttpHeaderEditor' });
|
||||
|
||||
const emit = defineEmits(['save']);
|
||||
|
||||
interface HeaderItem {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const headerList = ref<HeaderItem[]>([]);
|
||||
|
||||
// 解析请求头字符串为列表
|
||||
const parseHeaders = (headersStr: string): HeaderItem[] => {
|
||||
if (!headersStr || !headersStr.trim()) {
|
||||
return [{ key: '', value: '' }];
|
||||
}
|
||||
|
||||
const lines = headersStr.split('\n').filter((line) => line.trim());
|
||||
const parsed = lines.map((line) => {
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
return {
|
||||
key: line.slice(0, Math.max(0, colonIndex)).trim(),
|
||||
value: line.slice(Math.max(0, colonIndex + 1)).trim(),
|
||||
};
|
||||
}
|
||||
return { key: line.trim(), value: '' };
|
||||
});
|
||||
|
||||
return parsed.length > 0 ? parsed : [{ key: '', value: '' }];
|
||||
};
|
||||
|
||||
// 将列表转换为请求头字符串
|
||||
const stringifyHeaders = (headers: HeaderItem[]): string => {
|
||||
return headers
|
||||
.filter((item) => item.key.trim())
|
||||
.map((item) => `${item.key}: ${item.value}`)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
// 添加请求头
|
||||
const addHeader = () => {
|
||||
headerList.value.push({ key: '', value: '' });
|
||||
};
|
||||
|
||||
// 移除请求头
|
||||
const removeHeader = (index: number) => {
|
||||
if (headerList.value.length === 1) {
|
||||
// 至少保留一行
|
||||
headerList.value = [{ key: '', value: '' }];
|
||||
} else {
|
||||
headerList.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存
|
||||
const handleSave = () => {
|
||||
const headersStr = stringifyHeaders(headerList.value);
|
||||
emit('save', headersStr);
|
||||
modalApi.close();
|
||||
};
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
const { headers } = modalApi.getData();
|
||||
headerList.value = parseHeaders(headers);
|
||||
},
|
||||
onConfirm: handleSave,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="编辑请求头" class="w-3/5">
|
||||
<div class="space-y-4">
|
||||
<div class="mb-2 space-y-3 overflow-y-auto">
|
||||
<div
|
||||
v-for="(item, index) in headerList"
|
||||
:key="index"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Input
|
||||
v-model:value="item.key"
|
||||
placeholder="请输入参数名"
|
||||
class="w-48"
|
||||
allow-clear
|
||||
/>
|
||||
<span class="font-medium text-gray-600">:</span>
|
||||
<Input
|
||||
v-model:value="item.value"
|
||||
placeholder="请输入参数值 (支持表达式 ${变量名})"
|
||||
class="flex-1"
|
||||
allow-clear
|
||||
/>
|
||||
<Button type="text" danger size="small" @click="removeHeader(index)">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:delete" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" class="w-full" @click="addHeader">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:plus" />
|
||||
</template>
|
||||
添加请求头
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,96 +0,0 @@
|
||||
<!-- 表达式选择 -->
|
||||
<script setup lang="ts">
|
||||
import type { BpmProcessExpressionApi } from '#/api/bpm/processExpression';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { CommonStatusEnum } from '@vben/constants';
|
||||
|
||||
import { Button, Modal, Pagination, Table, TableColumn } from 'ant-design-vue';
|
||||
|
||||
import { getProcessExpressionPage } from '#/api/bpm/processExpression';
|
||||
|
||||
/** BPM 流程 表单 */
|
||||
defineOptions({ name: 'ProcessExpressionDialog' });
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['select']);
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const loading = ref(true); // 列表的加载中
|
||||
const list = ref<BpmProcessExpressionApi.ProcessExpression[]>([]); // 列表的数据
|
||||
const total = ref(0); // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
type: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
});
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = (type: string) => {
|
||||
queryParams.pageNo = 1;
|
||||
queryParams.type = type;
|
||||
getList();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getProcessExpressionPage(queryParams);
|
||||
list.value = data.list;
|
||||
total.value = data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 定义 select 事件,用于操作成功后的回调
|
||||
const select = async (row: BpmProcessExpressionApi.ProcessExpression) => {
|
||||
dialogVisible.value = false;
|
||||
// 发送操作成功的事件
|
||||
emit('select', row);
|
||||
};
|
||||
|
||||
// const handleCancel = () => {
|
||||
// dialogVisible.value = false;
|
||||
// };
|
||||
</script>
|
||||
<template>
|
||||
<Modal
|
||||
title="请选择表达式"
|
||||
v-model:open="dialogVisible"
|
||||
width="1024px"
|
||||
:footer="null"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:loading="loading"
|
||||
:data-source="list"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
>
|
||||
<TableColumn title="名字" align="center" data-index="name" />
|
||||
<TableColumn title="表达式" align="center" data-index="expression" />
|
||||
<TableColumn title="操作" align="center">
|
||||
<template #default="{ record }">
|
||||
<Button type="primary" @click="select(record)"> 选择 </Button>
|
||||
</template>
|
||||
</TableColumn>
|
||||
</Table>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-4 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,25 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
h,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
toRaw,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue';
|
||||
|
||||
import { PlusOutlined } from '@vben/icons';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
SelectOption,
|
||||
} from 'ant-design-vue';
|
||||
import { Button, message, Select, SelectOption } from 'ant-design-vue';
|
||||
|
||||
import SignalMessageModal from '../../signal-message/SignalMessageModal.vue';
|
||||
|
||||
defineOptions({ name: 'ReceiveTask' });
|
||||
const props = defineProps({
|
||||
@@ -28,40 +15,54 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const bindMessageId = ref('');
|
||||
const newMessageForm = ref<Record<string, any>>({});
|
||||
const messageMap = ref<Record<string, any>>({});
|
||||
const messageModelVisible = ref(false);
|
||||
const bpmnElement = ref<any>();
|
||||
const bpmnMessageRefsMap = ref<Record<string, any>>();
|
||||
const bpmnRootElements = ref<any>();
|
||||
|
||||
const bpmnInstances = () => (window as any).bpmnInstances;
|
||||
|
||||
const getBindMessage = () => {
|
||||
bpmnElement.value = bpmnInstances().bpmnElement;
|
||||
bindMessageId.value =
|
||||
bpmnElement.value.businessObject?.messageRef?.id || '-1';
|
||||
};
|
||||
const openMessageModel = () => {
|
||||
messageModelVisible.value = true;
|
||||
newMessageForm.value = {};
|
||||
|
||||
/** 生成消息 ID */
|
||||
const generateMessageId = (): string => {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).slice(2, 6).toUpperCase();
|
||||
return `Message_${timestamp}_${random}`;
|
||||
};
|
||||
const createNewMessage = () => {
|
||||
if (messageMap.value[newMessageForm.value.id]) {
|
||||
message.error('该消息已存在,请修改id后重新保存');
|
||||
|
||||
/** 打开创建消息弹窗 */
|
||||
const openCreateModal = () => {
|
||||
modalApi
|
||||
.setData({
|
||||
id: generateMessageId(),
|
||||
isEdit: false,
|
||||
name: '',
|
||||
type: 'message',
|
||||
})
|
||||
.open();
|
||||
};
|
||||
|
||||
const handleConfirm = (formData: { id: string; name: string }) => {
|
||||
if (messageMap.value[formData.id]) {
|
||||
message.error('该消息已存在, 请修改id后重新保存');
|
||||
return;
|
||||
}
|
||||
const newMessage = bpmnInstances().moddle.create(
|
||||
'bpmn:Message',
|
||||
newMessageForm.value,
|
||||
);
|
||||
const newMessage = bpmnInstances().moddle.create('bpmn:Message', formData);
|
||||
bpmnRootElements.value.push(newMessage);
|
||||
messageMap.value[newMessageForm.value.id] = newMessageForm.value.name;
|
||||
// @ts-ignore
|
||||
messageMap.value[formData.id] = formData.name;
|
||||
if (bpmnMessageRefsMap.value) {
|
||||
bpmnMessageRefsMap.value[newMessageForm.value.id] = newMessage;
|
||||
bpmnMessageRefsMap.value[formData.id] = newMessage;
|
||||
}
|
||||
messageModelVisible.value = false;
|
||||
};
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
connectedComponent: SignalMessageModal,
|
||||
});
|
||||
const updateTaskMessage = (messageId: string) => {
|
||||
if (messageId === '-1') {
|
||||
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
|
||||
@@ -96,7 +97,6 @@ onBeforeUnmount(() => {
|
||||
watch(
|
||||
() => props.id,
|
||||
() => {
|
||||
// bpmnElement.value = bpmnInstances().bpmnElement
|
||||
nextTick(() => {
|
||||
getBindMessage();
|
||||
});
|
||||
@@ -106,56 +106,31 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="margin-top: 16px">
|
||||
<Form.Item label="消息实例">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
"
|
||||
<div class="mt-2">
|
||||
<div class="mb-2 flex justify-end">
|
||||
<Button type="link" size="small" class="p-0" @click="openCreateModal">
|
||||
<template #icon>
|
||||
<IconifyIcon class="size-4" icon="lucide:plus" />
|
||||
</template>
|
||||
创建新消息
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mb-1 flex items-center">
|
||||
<span class="w-20 text-foreground">消息实例:</span>
|
||||
<Select
|
||||
v-model:value="bindMessageId"
|
||||
class="w-full"
|
||||
@change="(value: any) => updateTaskMessage(value)"
|
||||
>
|
||||
<Select
|
||||
v-model:value="bindMessageId"
|
||||
@change="(value: any) => updateTaskMessage(value)"
|
||||
<SelectOption
|
||||
v-for="key in Object.keys(messageMap)"
|
||||
:key="key"
|
||||
:value="key"
|
||||
>
|
||||
<SelectOption
|
||||
v-for="key in Object.keys(messageMap)"
|
||||
:value="key"
|
||||
:key="key"
|
||||
>
|
||||
{{ messageMap[key] }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
<Button
|
||||
type="primary"
|
||||
:icon="h(PlusOutlined)"
|
||||
style="margin-left: 8px"
|
||||
@click="openMessageModel"
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Modal
|
||||
v-model:open="messageModelVisible"
|
||||
:mask-closable="false"
|
||||
title="创建新消息"
|
||||
width="400px"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<Form :model="newMessageForm" size="small">
|
||||
<Form.Item label="消息ID">
|
||||
<Input v-model:value="newMessageForm.id" allow-clear />
|
||||
</Form.Item>
|
||||
<Form.Item label="消息名称">
|
||||
<Input v-model:value="newMessageForm.name" allow-clear />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button size="small" type="primary" @click="createNewMessage">
|
||||
确 认
|
||||
</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
{{ messageMap[key] }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
<Modal @confirm="handleConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Select,
|
||||
@@ -75,47 +76,50 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<FormItem label="脚本格式">
|
||||
<Input
|
||||
v-model:value="scriptTaskForm.scriptFormat"
|
||||
allow-clear
|
||||
@input="updateElementTask()"
|
||||
@change="updateElementTask()"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="脚本类型">
|
||||
<Select v-model:value="scriptTaskForm.scriptType">
|
||||
<SelectOption value="inline">内联脚本</SelectOption>
|
||||
<SelectOption value="external">外部资源</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
|
||||
<Textarea
|
||||
v-model:value="scriptTaskForm.script"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
allow-clear
|
||||
@input="updateElementTask()"
|
||||
@change="updateElementTask()"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="资源地址"
|
||||
v-show="scriptTaskForm.scriptType === 'external'"
|
||||
>
|
||||
<Input
|
||||
v-model:value="scriptTaskForm.resource"
|
||||
allow-clear
|
||||
@input="updateElementTask()"
|
||||
@change="updateElementTask()"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="结果变量">
|
||||
<Input
|
||||
v-model:value="scriptTaskForm.resultVariable"
|
||||
allow-clear
|
||||
@input="updateElementTask()"
|
||||
@change="updateElementTask()"
|
||||
/>
|
||||
</FormItem>
|
||||
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<FormItem label="脚本格式">
|
||||
<Input
|
||||
v-model:value="scriptTaskForm.scriptFormat"
|
||||
allow-clear
|
||||
@input="updateElementTask()"
|
||||
@change="updateElementTask()"
|
||||
/>
|
||||
</FormItem>
|
||||
<!-- TODO scriptType 外部资源 和 内联脚本, flowable 文档 https://www.flowable.com/open-source/docs/bpmn/ch07b-BPMN-Constructs#script-task 没看到到有相应的属性 -->
|
||||
<FormItem label="脚本类型">
|
||||
<Select v-model:value="scriptTaskForm.scriptType">
|
||||
<SelectOption value="inline">内联脚本</SelectOption>
|
||||
<SelectOption value="external">外部资源</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="脚本" v-show="scriptTaskForm.scriptType === 'inline'">
|
||||
<Textarea
|
||||
v-model:value="scriptTaskForm.script"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
allow-clear
|
||||
@input="updateElementTask()"
|
||||
@change="updateElementTask()"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="资源地址"
|
||||
v-show="scriptTaskForm.scriptType === 'external'"
|
||||
>
|
||||
<Input
|
||||
v-model:value="scriptTaskForm.resource"
|
||||
allow-clear
|
||||
@input="updateElementTask()"
|
||||
@change="updateElementTask()"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="结果变量">
|
||||
<Input
|
||||
v-model:value="scriptTaskForm.resultVariable"
|
||||
allow-clear
|
||||
@input="updateElementTask()"
|
||||
@change="updateElementTask()"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
<!-- eslint-disable prettier/prettier -->
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onBeforeUnmount, ref, toRaw, watch } from 'vue';
|
||||
import { inject, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
import { FormItem, Input, Select } from 'ant-design-vue';
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
RadioButton,
|
||||
RadioGroup,
|
||||
Select,
|
||||
Switch,
|
||||
Textarea,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { updateElementExtensions } from '../../../utils';
|
||||
import HttpHeaderEditor from './HttpHeaderEditor.vue';
|
||||
|
||||
defineOptions({ name: 'ServiceTask' });
|
||||
const props = defineProps({
|
||||
@@ -9,40 +26,305 @@ const props = defineProps({
|
||||
type: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const defaultTaskForm = ref({
|
||||
const prefix = (inject('prefix', 'flowable') || 'flowable') as string;
|
||||
const flowableTypeKey = `${prefix}:type`;
|
||||
const flowableFieldType = `${prefix}:Field`;
|
||||
|
||||
const HTTP_FIELD_NAMES = [
|
||||
'requestMethod',
|
||||
'requestUrl',
|
||||
'requestHeaders',
|
||||
'disallowRedirects',
|
||||
'ignoreException',
|
||||
'saveResponseParameters',
|
||||
'resultVariablePrefix',
|
||||
'saveResponseParametersTransient',
|
||||
'saveResponseVariableAsJson',
|
||||
];
|
||||
const HTTP_BOOLEAN_FIELDS = new Set([
|
||||
'disallowRedirects',
|
||||
'ignoreException',
|
||||
'saveResponseParameters',
|
||||
'saveResponseParametersTransient',
|
||||
'saveResponseVariableAsJson',
|
||||
]);
|
||||
|
||||
const DEFAULT_TASK_FORM = {
|
||||
executeType: '',
|
||||
class: '',
|
||||
expression: '',
|
||||
delegateExpression: '',
|
||||
});
|
||||
};
|
||||
|
||||
const serviceTaskForm = ref<any>({});
|
||||
const DEFAULT_HTTP_FORM = {
|
||||
requestMethod: 'GET',
|
||||
requestUrl: '',
|
||||
requestHeaders: 'Content-Type: application/json',
|
||||
resultVariablePrefix: '',
|
||||
disallowRedirects: false,
|
||||
ignoreException: false,
|
||||
saveResponseParameters: false,
|
||||
saveResponseParametersTransient: false,
|
||||
saveResponseVariableAsJson: false,
|
||||
};
|
||||
|
||||
const serviceTaskForm = ref({ ...DEFAULT_TASK_FORM });
|
||||
const httpTaskForm = ref<any>({ ...DEFAULT_HTTP_FORM });
|
||||
const bpmnElement = ref();
|
||||
const httpInitializing = ref(false);
|
||||
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
|
||||
const resetTaskForm = () => {
|
||||
for (const key in defaultTaskForm.value) {
|
||||
const value =
|
||||
// @ts-ignore
|
||||
bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key];
|
||||
serviceTaskForm.value[key] = value;
|
||||
if (value) {
|
||||
serviceTaskForm.value.executeType = key;
|
||||
// 判断字符串是否包含表达式
|
||||
const isExpression = (value: string): boolean => {
|
||||
if (!value) return false;
|
||||
// 检测 ${...} 或 #{...} 格式的表达式
|
||||
return /\$\{[^}]+\}/.test(value) || /#\{[^}]+\}/.test(value);
|
||||
};
|
||||
|
||||
const collectHttpExtensionInfo = () => {
|
||||
const businessObject = bpmnElement.value?.businessObject;
|
||||
const extensionElements = businessObject?.extensionElements;
|
||||
const httpFields = new Map<string, string>();
|
||||
const httpFieldTypes = new Map<string, 'expression' | 'string'>();
|
||||
const otherExtensions: any[] = [];
|
||||
|
||||
extensionElements?.values?.forEach((item: any) => {
|
||||
if (
|
||||
item?.$type === flowableFieldType &&
|
||||
HTTP_FIELD_NAMES.includes(item.name)
|
||||
) {
|
||||
const value = item.string ?? item.stringValue ?? item.expression ?? '';
|
||||
const fieldType = item.expression ? 'expression' : 'string';
|
||||
httpFields.set(item.name, value);
|
||||
httpFieldTypes.set(item.name, fieldType);
|
||||
} else {
|
||||
otherExtensions.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return { httpFields, httpFieldTypes, otherExtensions };
|
||||
};
|
||||
|
||||
const resetHttpDefaults = () => {
|
||||
httpInitializing.value = true;
|
||||
httpTaskForm.value = { ...DEFAULT_HTTP_FORM };
|
||||
nextTick(() => {
|
||||
httpInitializing.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const resetHttpForm = () => {
|
||||
httpInitializing.value = true;
|
||||
const { httpFields } = collectHttpExtensionInfo();
|
||||
const nextForm: any = { ...DEFAULT_HTTP_FORM };
|
||||
|
||||
HTTP_FIELD_NAMES.forEach((name) => {
|
||||
const stored = httpFields.get(name);
|
||||
if (stored !== undefined) {
|
||||
nextForm[name] = HTTP_BOOLEAN_FIELDS.has(name)
|
||||
? stored === 'true'
|
||||
: stored;
|
||||
}
|
||||
});
|
||||
|
||||
httpTaskForm.value = nextForm;
|
||||
nextTick(() => {
|
||||
httpInitializing.value = false;
|
||||
updateHttpExtensions(true);
|
||||
});
|
||||
};
|
||||
|
||||
const resetServiceTaskForm = () => {
|
||||
const businessObject = bpmnElement.value?.businessObject;
|
||||
const nextForm = { ...DEFAULT_TASK_FORM };
|
||||
|
||||
if (businessObject) {
|
||||
if (businessObject.class) {
|
||||
nextForm.class = businessObject.class;
|
||||
nextForm.executeType = 'class';
|
||||
}
|
||||
if (businessObject.expression) {
|
||||
nextForm.expression = businessObject.expression;
|
||||
nextForm.executeType = 'expression';
|
||||
}
|
||||
if (businessObject.delegateExpression) {
|
||||
nextForm.delegateExpression = businessObject.delegateExpression;
|
||||
nextForm.executeType = 'delegateExpression';
|
||||
}
|
||||
if (businessObject.$attrs?.[flowableTypeKey] === 'http') {
|
||||
nextForm.executeType = 'http';
|
||||
} else {
|
||||
// 兜底:如缺少 flowable:type=http,但扩展里已有 HTTP 的字段,也认为是 HTTP
|
||||
const { httpFields } = collectHttpExtensionInfo();
|
||||
if (httpFields.size > 0) {
|
||||
nextForm.executeType = 'http';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serviceTaskForm.value = nextForm;
|
||||
|
||||
if (nextForm.executeType === 'http') {
|
||||
resetHttpForm();
|
||||
} else {
|
||||
resetHttpDefaults();
|
||||
}
|
||||
};
|
||||
|
||||
const updateElementTask = () => {
|
||||
const taskAttr = Object.create(null);
|
||||
const type = serviceTaskForm.value.executeType;
|
||||
for (const key in serviceTaskForm.value) {
|
||||
if (key !== 'executeType' && key !== type) taskAttr[key] = null;
|
||||
}
|
||||
taskAttr[type] = serviceTaskForm.value[type] || '';
|
||||
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr);
|
||||
const shouldPersistField = (name: string, value: any) => {
|
||||
if (HTTP_BOOLEAN_FIELDS.has(name)) return true;
|
||||
if (name === 'requestMethod') return true;
|
||||
if (name === 'requestUrl') return !!value;
|
||||
return value !== undefined && value !== '';
|
||||
};
|
||||
|
||||
const updateHttpExtensions = (force = false) => {
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
// 直接使用原始BPMN元素,避免Vue响应式代理问题
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
if (
|
||||
!force &&
|
||||
(httpInitializing.value || serviceTaskForm.value.executeType !== 'http')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
httpFields: existingFields,
|
||||
httpFieldTypes: existingTypes,
|
||||
otherExtensions,
|
||||
} = collectHttpExtensionInfo();
|
||||
|
||||
const desiredEntries: [string, string][] = [];
|
||||
HTTP_FIELD_NAMES.forEach((name) => {
|
||||
const rawValue = httpTaskForm.value[name];
|
||||
if (!shouldPersistField(name, rawValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const persisted = HTTP_BOOLEAN_FIELDS.has(name)
|
||||
? String(!!rawValue)
|
||||
: (rawValue === undefined
|
||||
? ''
|
||||
: rawValue.toString());
|
||||
|
||||
desiredEntries.push([name, persisted]);
|
||||
});
|
||||
|
||||
// 检查是否有变化:不仅比较值,还要比较字段类型(string vs expression)
|
||||
if (!force && desiredEntries.length === existingFields.size) {
|
||||
let noChange = true;
|
||||
for (const [name, value] of desiredEntries) {
|
||||
const existingValue = existingFields.get(name);
|
||||
const existingType = existingTypes.get(name);
|
||||
const currentType = isExpression(value) ? 'expression' : 'string';
|
||||
if (existingValue !== value || existingType !== currentType) {
|
||||
noChange = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (noChange) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const moddle = bpmnInstances().moddle;
|
||||
const httpFieldElements = desiredEntries.map(([name, value]) => {
|
||||
// 根据值是否包含表达式来决定使用 string 还是 expression 属性
|
||||
const isExpr = isExpression(value);
|
||||
return moddle.create(flowableFieldType, {
|
||||
name,
|
||||
...(isExpr ? { expression: value } : { string: value }),
|
||||
});
|
||||
});
|
||||
|
||||
updateElementExtensions(bpmnElement, [
|
||||
...otherExtensions,
|
||||
...httpFieldElements,
|
||||
]);
|
||||
};
|
||||
|
||||
const removeHttpExtensions = () => {
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
// 直接使用原始BPMN元素,避免Vue响应式代理问题
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
const { httpFields, otherExtensions } = collectHttpExtensionInfo();
|
||||
if (httpFields.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (otherExtensions.length === 0) {
|
||||
bpmnInstances().modeling.updateProperties(bpmnElement, {
|
||||
extensionElements: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateElementExtensions(bpmnElement, otherExtensions);
|
||||
};
|
||||
|
||||
const updateElementTask = () => {
|
||||
const instances = bpmnInstances();
|
||||
if (!instances || !instances.bpmnElement) return;
|
||||
// 直接使用原始BPMN元素,避免Vue响应式代理问题
|
||||
const bpmnElement = instances.bpmnElement;
|
||||
|
||||
const taskAttr: Record<string, any> = {
|
||||
class: null,
|
||||
expression: null,
|
||||
delegateExpression: null,
|
||||
[flowableTypeKey]: null,
|
||||
};
|
||||
|
||||
const type = serviceTaskForm.value.executeType;
|
||||
if (
|
||||
type === 'class' ||
|
||||
type === 'expression' ||
|
||||
type === 'delegateExpression'
|
||||
) {
|
||||
taskAttr[type] = serviceTaskForm.value[type] || null;
|
||||
} else if (type === 'http') {
|
||||
taskAttr[flowableTypeKey] = 'http';
|
||||
}
|
||||
|
||||
bpmnInstances().modeling.updateProperties(bpmnElement, taskAttr);
|
||||
|
||||
if (type === 'http') {
|
||||
updateHttpExtensions(true);
|
||||
} else {
|
||||
removeHttpExtensions();
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecuteTypeChange = (value: any) => {
|
||||
serviceTaskForm.value.executeType = value;
|
||||
if (value === 'http') {
|
||||
resetHttpForm();
|
||||
}
|
||||
updateElementTask();
|
||||
};
|
||||
|
||||
/** 打开请求头编辑器 */
|
||||
const openHttpHeaderEditor = () => {
|
||||
httpHeaderEditorApi
|
||||
.setData({
|
||||
headers: httpTaskForm.value.requestHeaders,
|
||||
})
|
||||
.open();
|
||||
};
|
||||
|
||||
/** 保存请求头 */
|
||||
const handleHeadersSave = (headersStr: string) => {
|
||||
httpTaskForm.value.requestHeaders = headersStr;
|
||||
};
|
||||
|
||||
const [HttpHeaderEditorModal, httpHeaderEditorApi] = useVbenModal({
|
||||
connectedComponent: HttpHeaderEditor,
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
bpmnElement.value = null;
|
||||
});
|
||||
@@ -52,60 +334,157 @@ watch(
|
||||
() => {
|
||||
bpmnElement.value = bpmnInstances().bpmnElement;
|
||||
nextTick(() => {
|
||||
resetTaskForm();
|
||||
resetServiceTaskForm();
|
||||
});
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => httpTaskForm.value,
|
||||
() => {
|
||||
updateHttpExtensions();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FormItem label="执行类型" key="executeType">
|
||||
<Select
|
||||
v-model:value="serviceTaskForm.executeType"
|
||||
:options="[
|
||||
{ label: 'Java类', value: 'class' },
|
||||
{ label: '表达式', value: 'expression' },
|
||||
{ label: '代理表达式', value: 'delegateExpression' },
|
||||
]"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="serviceTaskForm.executeType === 'class'"
|
||||
label="Java类"
|
||||
name="class"
|
||||
key="execute-class"
|
||||
>
|
||||
<Input
|
||||
v-model:value="serviceTaskForm.class"
|
||||
allow-clear
|
||||
@change="updateElementTask"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="serviceTaskForm.executeType === 'expression'"
|
||||
label="表达式"
|
||||
name="expression"
|
||||
key="execute-expression"
|
||||
>
|
||||
<Input
|
||||
v-model:value="serviceTaskForm.expression"
|
||||
allow-clear
|
||||
@change="updateElementTask"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="serviceTaskForm.executeType === 'delegateExpression'"
|
||||
label="代理表达式"
|
||||
name="delegateExpression"
|
||||
key="execute-delegate"
|
||||
>
|
||||
<Input
|
||||
v-model:value="serviceTaskForm.delegateExpression"
|
||||
allow-clear
|
||||
@change="updateElementTask"
|
||||
/>
|
||||
</FormItem>
|
||||
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<FormItem label="执行类型" key="executeType">
|
||||
<Select
|
||||
v-model:value="serviceTaskForm.executeType"
|
||||
:options="[
|
||||
{ label: 'Java类', value: 'class' },
|
||||
{ label: '表达式', value: 'expression' },
|
||||
{ label: '代理表达式', value: 'delegateExpression' },
|
||||
{ label: 'HTTP 调用', value: 'http' },
|
||||
]"
|
||||
@change="handleExecuteTypeChange"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="serviceTaskForm.executeType === 'class'"
|
||||
label="Java类"
|
||||
name="class"
|
||||
key="execute-class"
|
||||
>
|
||||
<Input
|
||||
v-model:value="serviceTaskForm.class"
|
||||
allow-clear
|
||||
@change="updateElementTask"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="serviceTaskForm.executeType === 'expression'"
|
||||
label="表达式"
|
||||
name="expression"
|
||||
key="execute-expression"
|
||||
>
|
||||
<Input
|
||||
v-model:value="serviceTaskForm.expression"
|
||||
allow-clear
|
||||
@change="updateElementTask"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
v-if="serviceTaskForm.executeType === 'delegateExpression'"
|
||||
label="代理表达式"
|
||||
name="delegateExpression"
|
||||
key="execute-delegate"
|
||||
>
|
||||
<Input
|
||||
v-model:value="serviceTaskForm.delegateExpression"
|
||||
allow-clear
|
||||
@change="updateElementTask"
|
||||
/>
|
||||
</FormItem>
|
||||
<template v-if="serviceTaskForm.executeType === 'http'">
|
||||
<FormItem label="请求方法" key="http-method" name="requestMethod">
|
||||
<RadioGroup v-model:value="httpTaskForm.requestMethod">
|
||||
<RadioButton value="GET">GET</RadioButton>
|
||||
<RadioButton value="POST">POST</RadioButton>
|
||||
<RadioButton value="PUT">PUT</RadioButton>
|
||||
<RadioButton value="DELETE">DELETE</RadioButton>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem label="请求地址" key="http-url" name="requestUrl">
|
||||
<Input v-model:value="httpTaskForm.requestUrl" allow-clear />
|
||||
</FormItem>
|
||||
<FormItem label="请求头" key="http-headers" name="requestHeaders">
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<Textarea
|
||||
v-model:value="httpTaskForm.requestHeaders"
|
||||
:auto-size="{ minRows: 4, maxRows: 8 }"
|
||||
readonly
|
||||
placeholder="点击右侧编辑按钮添加请求头"
|
||||
class="min-w-0 flex-1"
|
||||
/>
|
||||
<div class="flex w-full items-center justify-center">
|
||||
<Button
|
||||
class="flex flex-1 items-center justify-center"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openHttpHeaderEditor"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:edit" />
|
||||
</template>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="禁止重定向"
|
||||
key="http-disallow-redirects"
|
||||
name="disallowRedirects"
|
||||
>
|
||||
<Switch v-model:checked="httpTaskForm.disallowRedirects" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="忽略异常"
|
||||
key="http-ignore-exception"
|
||||
name="ignoreException"
|
||||
>
|
||||
<Switch v-model:checked="httpTaskForm.ignoreException" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="保存返回变量"
|
||||
key="http-save-response"
|
||||
name="saveResponseParameters"
|
||||
>
|
||||
<Switch v-model:checked="httpTaskForm.saveResponseParameters" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="是否瞬间变量"
|
||||
key="http-save-transient"
|
||||
name="saveResponseParametersTransient"
|
||||
>
|
||||
<Switch
|
||||
v-model:checked="httpTaskForm.saveResponseParametersTransient"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="返回变量前缀"
|
||||
key="http-result-variable-prefix"
|
||||
name="resultVariablePrefix"
|
||||
>
|
||||
<Input v-model:value="httpTaskForm.resultVariablePrefix" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="保存为 JSON 变量"
|
||||
:label-col="{ span: 8 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
key="http-save-json"
|
||||
name="saveResponseVariableAsJson"
|
||||
>
|
||||
<Switch v-model:checked="httpTaskForm.saveResponseVariableAsJson" />
|
||||
</FormItem>
|
||||
</template>
|
||||
</Form>
|
||||
<!-- 请求头编辑器 -->
|
||||
<HttpHeaderEditorModal @save="handleHeadersSave" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { SelectOutlined } from '@vben/icons';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
@@ -42,8 +43,7 @@ import {
|
||||
MULTI_LEVEL_DEPT,
|
||||
} from '#/views/bpm/components/simple-process-design/consts';
|
||||
import { useFormFieldsPermission } from '#/views/bpm/components/simple-process-design/helpers';
|
||||
|
||||
import ProcessExpressionDialog from './ProcessExpressionDialog.vue';
|
||||
import ProcessExpressionSelectModal from '#/views/bpm/processExpression/components/process-expression-select-modal.vue';
|
||||
|
||||
defineOptions({ name: 'UserTask' });
|
||||
const props = defineProps({
|
||||
@@ -120,10 +120,10 @@ const resetTaskForm = () => {
|
||||
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] });
|
||||
userTaskForm.value.candidateStrategy = extensionElements.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:CandidateStrategy`,
|
||||
)?.[0]?.value;
|
||||
)?.value;
|
||||
const candidateParamStr = extensionElements.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:CandidateParam`,
|
||||
)?.[0]?.value;
|
||||
)?.value;
|
||||
if (candidateParamStr && candidateParamStr.length > 0) {
|
||||
// eslint-disable-next-line unicorn/prefer-switch
|
||||
if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) {
|
||||
@@ -292,9 +292,13 @@ const updateSkipExpression = () => {
|
||||
};
|
||||
|
||||
// 打开监听器弹窗
|
||||
const processExpressionDialogRef = ref<any>();
|
||||
const [ProcessExpressionSelectModalComp, ProcessExpressionSelectModalApi] =
|
||||
useVbenModal({
|
||||
connectedComponent: ProcessExpressionSelectModal,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
const openProcessExpressionDialog = async () => {
|
||||
processExpressionDialogRef.value.open();
|
||||
ProcessExpressionSelectModalApi.open();
|
||||
};
|
||||
const selectProcessExpression = (
|
||||
expression: BpmProcessExpressionApi.ProcessExpression,
|
||||
@@ -344,7 +348,7 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form>
|
||||
<Form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<FormItem label="规则类型" name="candidateStrategy">
|
||||
<Select
|
||||
v-model:value="userTaskForm.candidateStrategy"
|
||||
@@ -544,19 +548,19 @@ onBeforeUnmount(() => {
|
||||
style="width: 100%"
|
||||
@change="updateElementTask"
|
||||
/>
|
||||
<Button
|
||||
class="!w-1/1 mt-5px"
|
||||
type="primary"
|
||||
:icon="h(SelectOutlined)"
|
||||
@click="openProcessExpressionDialog"
|
||||
>
|
||||
选择表达式
|
||||
</Button>
|
||||
<div class="mt-2 flex w-full items-center justify-center">
|
||||
<Button
|
||||
class="flex flex-1 items-center justify-center"
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="h(SelectOutlined)"
|
||||
@click="openProcessExpressionDialog"
|
||||
>
|
||||
选择表达式
|
||||
</Button>
|
||||
</div>
|
||||
<!-- 选择弹窗 -->
|
||||
<ProcessExpressionDialog
|
||||
ref="processExpressionDialogRef"
|
||||
@select="selectProcessExpression"
|
||||
/>
|
||||
<ProcessExpressionSelectModalComp @select="selectProcessExpression" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="跳过表达式" name="skipExpression">
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
TabPane,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
@@ -41,7 +43,7 @@ const cronFieldList = [
|
||||
];
|
||||
const activeField = ref('second');
|
||||
const cronMode = ref({
|
||||
second: 'appoint',
|
||||
second: 'every',
|
||||
minute: 'every',
|
||||
hour: 'every',
|
||||
day: 'every',
|
||||
@@ -50,7 +52,7 @@ const cronMode = ref({
|
||||
year: 'every',
|
||||
});
|
||||
const cronAppoint = ref({
|
||||
second: ['00', '01'],
|
||||
second: [],
|
||||
minute: [],
|
||||
hour: [],
|
||||
day: [],
|
||||
@@ -107,103 +109,156 @@ watch(
|
||||
const isoStr = ref('');
|
||||
const repeat = ref(1);
|
||||
const isoDate = ref('');
|
||||
const durationUnits = [
|
||||
{ key: 'Y', label: '年', presets: [1, 2, 3, 4] },
|
||||
{ key: 'M', label: '月', presets: [1, 2, 3, 4] },
|
||||
{ key: 'D', label: '天', presets: [1, 2, 3, 4] },
|
||||
{ key: 'H', label: '时', presets: [4, 8, 12, 24] },
|
||||
{ key: 'm', label: '分', presets: [5, 10, 30, 50] },
|
||||
{ key: 'S', label: '秒', presets: [5, 10, 30, 50] },
|
||||
];
|
||||
const durationCustom = ref({ Y: '', M: '', D: '', H: '', m: '', S: '' });
|
||||
const isoDuration = ref('');
|
||||
function setDuration(type, val) {
|
||||
// 组装ISO 8601字符串
|
||||
let d = isoDuration.value;
|
||||
if (d.includes(type)) {
|
||||
d = d.replace(new RegExp(String.raw`\d+${type}`), val + type);
|
||||
} else {
|
||||
d += val + type;
|
||||
}
|
||||
isoDuration.value = d;
|
||||
|
||||
function setDuration(key, val) {
|
||||
durationCustom.value[key] = !val || Number.isNaN(val) ? '' : val;
|
||||
updateDurationStr();
|
||||
}
|
||||
|
||||
function updateDurationStr() {
|
||||
let str = 'P';
|
||||
str += durationCustom.value.Y ? `${durationCustom.value.Y}Y` : '';
|
||||
str += durationCustom.value.M ? `${durationCustom.value.M}M` : '';
|
||||
str += durationCustom.value.D ? `${durationCustom.value.D}D` : '';
|
||||
str +=
|
||||
durationCustom.value.H || durationCustom.value.m || durationCustom.value.S
|
||||
? 'T'
|
||||
: '';
|
||||
str += durationCustom.value.H ? `${durationCustom.value.H}H` : '';
|
||||
str += durationCustom.value.m ? `${durationCustom.value.m}M` : '';
|
||||
str += durationCustom.value.S ? `${durationCustom.value.S}S` : '';
|
||||
isoDuration.value = str === 'P' ? '' : str;
|
||||
updateIsoStr();
|
||||
}
|
||||
|
||||
function updateIsoStr() {
|
||||
let str = `R${repeat.value}`;
|
||||
if (isoDate.value)
|
||||
str += `/${
|
||||
if (isoDate.value) {
|
||||
const dateStr =
|
||||
typeof isoDate.value === 'string'
|
||||
? isoDate.value
|
||||
: new Date(isoDate.value).toISOString()
|
||||
}`;
|
||||
: isoDate.value.toISOString();
|
||||
str += `/${dateStr}`;
|
||||
}
|
||||
if (isoDuration.value) str += `/${isoDuration.value}`;
|
||||
isoStr.value = str;
|
||||
if (tab.value === 'iso') emit('change', isoStr.value);
|
||||
}
|
||||
watch([repeat, isoDate, isoDuration], updateIsoStr);
|
||||
watch([repeat, isoDate], updateIsoStr);
|
||||
watch(durationCustom, updateDurationStr, { deep: true });
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
if (!val) return;
|
||||
if (tab.value === 'cron') cronStr.value = val;
|
||||
if (tab.value === 'iso') isoStr.value = val;
|
||||
// 自动检测格式:以R开头的是ISO 8601格式,否则是CRON表达式
|
||||
if (val.startsWith('R')) {
|
||||
tab.value = 'iso';
|
||||
isoStr.value = val;
|
||||
// 解析ISO格式:R{repeat}/{date}/{duration}
|
||||
const parts = val.split('/');
|
||||
if (parts[0]) {
|
||||
const repeatMatch = parts[0].match(/^R(\d+)$/);
|
||||
if (repeatMatch) repeat.value = Number.parseInt(repeatMatch[1], 10);
|
||||
}
|
||||
// 解析date部分(ISO 8601日期时间格式)
|
||||
const datePart = parts.find(
|
||||
(p) => p.includes('T') && !p.startsWith('P') && !p.startsWith('R'),
|
||||
);
|
||||
if (datePart) {
|
||||
isoDate.value = dayjs(datePart);
|
||||
}
|
||||
// 解析duration部分
|
||||
const durationPart = parts.find((p) => p.startsWith('P'));
|
||||
if (durationPart) {
|
||||
const match = durationPart.match(
|
||||
/^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/,
|
||||
);
|
||||
if (match) {
|
||||
durationCustom.value.Y = match[1] || '';
|
||||
durationCustom.value.M = match[2] || '';
|
||||
durationCustom.value.D = match[3] || '';
|
||||
durationCustom.value.H = match[4] || '';
|
||||
durationCustom.value.m = match[5] || '';
|
||||
durationCustom.value.S = match[6] || '';
|
||||
isoDuration.value = durationPart;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tab.value = 'cron';
|
||||
cronStr.value = val;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<Tabs v-model:active-key="tab">
|
||||
<Tabs.TabPane key="cron" tab="CRON表达式">
|
||||
<div style="margin-bottom: 10px">
|
||||
<TabPane key="cron" tab="CRON表达式">
|
||||
<div class="mb-2.5">
|
||||
<Input
|
||||
v-model:value="cronStr"
|
||||
readonly
|
||||
style="width: 400px; font-weight: bold"
|
||||
class="w-[400px] font-bold"
|
||||
key="cronStr"
|
||||
/>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 8px">
|
||||
<div class="mb-2 flex gap-2">
|
||||
<Input
|
||||
v-model:value="fields.second"
|
||||
placeholder="秒"
|
||||
style="width: 80px"
|
||||
class="w-20"
|
||||
key="second"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="fields.minute"
|
||||
placeholder="分"
|
||||
style="width: 80px"
|
||||
class="w-20"
|
||||
key="minute"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="fields.hour"
|
||||
placeholder="时"
|
||||
style="width: 80px"
|
||||
class="w-20"
|
||||
key="hour"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="fields.day"
|
||||
placeholder="天"
|
||||
style="width: 80px"
|
||||
class="w-20"
|
||||
key="day"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="fields.month"
|
||||
placeholder="月"
|
||||
style="width: 80px"
|
||||
class="w-20"
|
||||
key="month"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="fields.week"
|
||||
placeholder="周"
|
||||
style="width: 80px"
|
||||
class="w-20"
|
||||
key="week"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="fields.year"
|
||||
placeholder="年"
|
||||
style="width: 80px"
|
||||
class="w-20"
|
||||
key="year"
|
||||
/>
|
||||
</div>
|
||||
<Tabs
|
||||
v-model:active-key="activeField"
|
||||
type="card"
|
||||
style="margin-bottom: 8px"
|
||||
>
|
||||
<Tabs v-model:active-key="activeField" type="card" class="mb-2">
|
||||
<Tabs.TabPane v-for="f in cronFieldList" :key="f.key" :tab="f.label">
|
||||
<div style="margin-bottom: 8px">
|
||||
<div class="mb-2">
|
||||
<Radio.Group
|
||||
v-model:value="cronMode[f.key]"
|
||||
:key="`radio-${f.key}`"
|
||||
@@ -218,7 +273,7 @@ watch(
|
||||
:min="f.min"
|
||||
:max="f.max"
|
||||
size="small"
|
||||
style="width: 60px"
|
||||
class="w-[60px]"
|
||||
:key="`range0-${f.key}`"
|
||||
/>
|
||||
到
|
||||
@@ -227,7 +282,7 @@ watch(
|
||||
:min="f.min"
|
||||
:max="f.max"
|
||||
size="small"
|
||||
style="width: 60px"
|
||||
class="w-[60px]"
|
||||
:key="`range1-${f.key}`"
|
||||
/>
|
||||
之间每{{ f.label }}
|
||||
@@ -239,7 +294,7 @@ watch(
|
||||
:min="f.min"
|
||||
:max="f.max"
|
||||
size="small"
|
||||
style="width: 60px"
|
||||
class="w-[60px]"
|
||||
:key="`step0-${f.key}`"
|
||||
/>
|
||||
开始每
|
||||
@@ -248,7 +303,7 @@ watch(
|
||||
:min="1"
|
||||
:max="f.max"
|
||||
size="small"
|
||||
style="width: 60px"
|
||||
class="w-[60px]"
|
||||
:key="`step1-${f.key}`"
|
||||
/>
|
||||
{{ f.label }}
|
||||
@@ -272,109 +327,64 @@ watch(
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="iso" title="标准格式" tab="iso-tab">
|
||||
<div style="margin-bottom: 10px">
|
||||
</TabPane>
|
||||
<TabPane key="iso" tab="标准格式">
|
||||
<div class="mb-2.5">
|
||||
<Input
|
||||
v-model:value="isoStr"
|
||||
placeholder="如R1/2025-05-21T21:59:54/P3DT30M30S"
|
||||
style="width: 400px; font-weight: bold"
|
||||
class="w-[400px] font-bold"
|
||||
key="isoStr"
|
||||
/>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px">
|
||||
<div class="mb-2.5">
|
||||
循环次数:<InputNumber
|
||||
v-model:value="repeat"
|
||||
:min="1"
|
||||
style="width: 100px"
|
||||
class="w-[100px]"
|
||||
key="repeat"
|
||||
/>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px">
|
||||
日期时间:<DatePicker
|
||||
<div class="mb-2.5">
|
||||
开始时间:<DatePicker
|
||||
v-model:value="isoDate"
|
||||
show-time
|
||||
placeholder="选择日期时间"
|
||||
style="width: 200px"
|
||||
placeholder="选择开始时间"
|
||||
class="w-[200px]"
|
||||
key="isoDate"
|
||||
/>
|
||||
</div>
|
||||
<div style="margin-bottom: 10px">
|
||||
当前时长:<Input
|
||||
<div class="mb-2.5">
|
||||
间隔时长:<Input
|
||||
v-model:value="isoDuration"
|
||||
readonly
|
||||
placeholder="如P3DT30M30S"
|
||||
style="width: 200px"
|
||||
class="w-[200px]"
|
||||
key="isoDuration"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
秒:
|
||||
<Button
|
||||
v-for="s in [5, 10, 30, 50]"
|
||||
@click="setDuration('S', s)"
|
||||
:key="`sec-${s}`"
|
||||
>
|
||||
{{ s }}
|
||||
</Button>
|
||||
自定义
|
||||
</div>
|
||||
<div>
|
||||
分:
|
||||
<Button
|
||||
v-for="m in [5, 10, 30, 50]"
|
||||
@click="setDuration('M', m)"
|
||||
:key="`min-${m}`"
|
||||
>
|
||||
{{ m }}
|
||||
</Button>
|
||||
自定义
|
||||
</div>
|
||||
<div>
|
||||
小时:
|
||||
<Button
|
||||
v-for="h in [4, 8, 12, 24]"
|
||||
@click="setDuration('H', h)"
|
||||
:key="`hour-${h}`"
|
||||
>
|
||||
{{ h }}
|
||||
</Button>
|
||||
自定义
|
||||
</div>
|
||||
<div>
|
||||
天:
|
||||
<Button
|
||||
v-for="d in [1, 2, 3, 4]"
|
||||
@click="setDuration('D', d)"
|
||||
:key="`day-${d}`"
|
||||
>
|
||||
{{ d }}
|
||||
</Button>
|
||||
自定义
|
||||
</div>
|
||||
<div>
|
||||
月:
|
||||
<Button
|
||||
v-for="mo in [1, 2, 3, 4]"
|
||||
@click="setDuration('M', mo)"
|
||||
:key="`mon-${mo}`"
|
||||
>
|
||||
{{ mo }}
|
||||
</Button>
|
||||
自定义
|
||||
</div>
|
||||
<div>
|
||||
年:
|
||||
<Button
|
||||
v-for="y in [1, 2, 3, 4]"
|
||||
@click="setDuration('Y', y)"
|
||||
:key="`year-${y}`"
|
||||
>
|
||||
{{ y }}
|
||||
</Button>
|
||||
自定义
|
||||
<div v-for="unit in durationUnits" :key="unit.key" class="mb-2">
|
||||
<span>{{ unit.label }}:</span>
|
||||
<Button.Group>
|
||||
<Button
|
||||
v-for="val in unit.presets"
|
||||
:key="val"
|
||||
size="small"
|
||||
@click="setDuration(unit.key, val)"
|
||||
>
|
||||
{{ val }}
|
||||
</Button>
|
||||
<Input
|
||||
v-model:value="durationCustom[unit.key]"
|
||||
size="small"
|
||||
class="ml-2 w-[60px]"
|
||||
placeholder="自定义"
|
||||
@change="setDuration(unit.key, durationCustom[unit.key])"
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</template>
|
||||
|
||||
@@ -68,14 +68,10 @@ watch(
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div style="margin-bottom: 10px">
|
||||
当前选择:<Input
|
||||
v-model:value="isoString"
|
||||
readonly
|
||||
style="width: 300px"
|
||||
/>
|
||||
<div class="mb-2.5">
|
||||
当前选择:<Input v-model:value="isoString" readonly class="w-[300px]" />
|
||||
</div>
|
||||
<div v-for="unit in units" :key="unit.key" style="margin-bottom: 8px">
|
||||
<div v-for="unit in units" :key="unit.key" class="mb-2">
|
||||
<span>{{ unit.label }}:</span>
|
||||
<Button.Group>
|
||||
<Button
|
||||
@@ -89,7 +85,7 @@ watch(
|
||||
<Input
|
||||
v-model:value="custom[unit.key]"
|
||||
size="small"
|
||||
style="width: 60px; margin-left: 8px"
|
||||
class="ml-2 w-[60px]"
|
||||
placeholder="自定义"
|
||||
@change="setUnit(unit.key, custom[unit.key])"
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, toRaw, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, DatePicker, Input, Modal, Tooltip } from 'ant-design-vue';
|
||||
import { Button, DatePicker, Input, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import CycleConfig from './CycleConfig.vue';
|
||||
import DurationConfig from './DurationConfig.vue';
|
||||
@@ -20,13 +23,8 @@ const props = defineProps({
|
||||
const bpmnInstances = () => (window as any).bpmnInstances;
|
||||
const type: Ref<string> = ref('time');
|
||||
const condition: Ref<string> = ref('');
|
||||
const valid: Ref<boolean> = ref(true);
|
||||
const showDatePicker: Ref<boolean> = ref(false);
|
||||
const showDurationDialog: Ref<boolean> = ref(false);
|
||||
const showCycleDialog: Ref<boolean> = ref(false);
|
||||
const showHelp: Ref<boolean> = ref(false);
|
||||
const dateValue: Ref<Date | null> = ref(null);
|
||||
// const bpmnElement = ref(null);
|
||||
const valid: Ref<boolean> = ref(false);
|
||||
const dateValue = ref<Dayjs>();
|
||||
|
||||
const placeholder = computed<string>(() => {
|
||||
if (type.value === 'time') return '请输入时间';
|
||||
@@ -49,6 +47,9 @@ const helpHtml = computed<string>(() => {
|
||||
if (type.value === 'cycle') {
|
||||
return `支持CRON表达式(如0 0/30 * * * ?)或ISO 8601周期(如R3/PT10M)。`;
|
||||
}
|
||||
if (type.value === 'time') {
|
||||
return `支持ISO 8601格式的时间(如2024-12-12T12:12:12)`;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
@@ -82,7 +83,6 @@ function setType(t: string) {
|
||||
// 输入校验
|
||||
watch([type, condition], () => {
|
||||
valid.value = validate();
|
||||
// updateNode() // 可以注释掉,避免频繁触发
|
||||
});
|
||||
|
||||
function validate(): boolean {
|
||||
@@ -93,46 +93,74 @@ function validate(): boolean {
|
||||
return /^P.*$/.test(condition.value);
|
||||
}
|
||||
if (type.value === 'cycle') {
|
||||
return /^(?:[0-9*/?, ]+|R\d*\/P.*)$/.test(condition.value);
|
||||
// 支持CRON表达式或ISO 8601周期格式:R{n}/P... 或 R{n}/{date}/P...
|
||||
return /^(?:[0-9*/?, ]+|R\d+(?:\/[^/]+)*\/P.*)$/.test(condition.value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 选择时间
|
||||
// 选择时间 Modal
|
||||
const [DateModal, dateModalApi] = useVbenModal({
|
||||
title: '选择时间',
|
||||
class: 'w-[400px]',
|
||||
onConfirm: onDateConfirm,
|
||||
});
|
||||
|
||||
function onDateChange(val: any) {
|
||||
dateValue.value = val;
|
||||
dateValue.value = val || undefined;
|
||||
}
|
||||
function onDateConfirm(): void {
|
||||
if (dateValue.value) {
|
||||
condition.value = new Date(dateValue.value).toISOString();
|
||||
showDatePicker.value = false;
|
||||
condition.value = dateValue.value.toISOString();
|
||||
dateModalApi.close();
|
||||
updateNode();
|
||||
}
|
||||
}
|
||||
|
||||
// 持续时长
|
||||
// 持续时长 Modal
|
||||
const [DurationModal, durationModalApi] = useVbenModal({
|
||||
title: '时间配置',
|
||||
class: 'w-[600px]',
|
||||
onConfirm: onDurationConfirm,
|
||||
});
|
||||
|
||||
function onDurationChange(val: string) {
|
||||
condition.value = val;
|
||||
}
|
||||
function onDurationConfirm(): void {
|
||||
showDurationDialog.value = false;
|
||||
durationModalApi.close();
|
||||
updateNode();
|
||||
}
|
||||
|
||||
// 循环
|
||||
// 循环配置 Modal
|
||||
const [CycleModal, cycleModalApi] = useVbenModal({
|
||||
title: '时间配置',
|
||||
class: 'w-[800px]',
|
||||
onConfirm: onCycleConfirm,
|
||||
});
|
||||
|
||||
function onCycleChange(val: string) {
|
||||
condition.value = val;
|
||||
}
|
||||
function onCycleConfirm(): void {
|
||||
showCycleDialog.value = false;
|
||||
cycleModalApi.close();
|
||||
updateNode();
|
||||
}
|
||||
|
||||
// 输入框聚焦时弹窗(可选)
|
||||
function handleInputFocus(): void {
|
||||
if (type.value === 'time') showDatePicker.value = true;
|
||||
if (type.value === 'duration') showDurationDialog.value = true;
|
||||
if (type.value === 'cycle') showCycleDialog.value = true;
|
||||
// 帮助说明 Modal
|
||||
const [HelpModal, helpModalApi] = useVbenModal({
|
||||
class: 'w-[600px]',
|
||||
title: '格式说明',
|
||||
showCancelButton: false,
|
||||
confirmText: '关闭',
|
||||
onConfirm: () => helpModalApi.close(),
|
||||
});
|
||||
|
||||
// 点击输入框时弹窗
|
||||
function handleInputClick(): void {
|
||||
if (type.value === 'time') dateModalApi.open();
|
||||
if (type.value === 'duration') durationModalApi.open();
|
||||
if (type.value === 'cycle') cycleModalApi.open();
|
||||
}
|
||||
|
||||
// 同步到节点
|
||||
@@ -210,8 +238,8 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="panel-tab__content">
|
||||
<div style="margin-top: 10px">
|
||||
<span>类型:</span>
|
||||
<div class="mt-2 flex items-center">
|
||||
<span class="w-14">类型:</span>
|
||||
<Button.Group>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -238,17 +266,17 @@ watch(
|
||||
<IconifyIcon
|
||||
icon="ant-design:check-circle-filled"
|
||||
v-if="valid"
|
||||
style="margin-left: 8px; color: green"
|
||||
class="ml-2 text-green-500"
|
||||
/>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-top: 10px">
|
||||
<span>条件:</span>
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="w-14">条件:</span>
|
||||
<Input
|
||||
v-model:value="condition"
|
||||
:placeholder="placeholder"
|
||||
class="w-[calc(100vw-25%)]"
|
||||
class="w-full"
|
||||
:readonly="type !== 'duration' && type !== 'cycle'"
|
||||
@focus="handleInputFocus"
|
||||
@click="handleInputClick"
|
||||
@blur="updateNode"
|
||||
>
|
||||
<template #suffix>
|
||||
@@ -262,13 +290,13 @@ watch(
|
||||
<IconifyIcon
|
||||
icon="ant-design:question-circle-filled"
|
||||
class="cursor-pointer text-[#409eff]"
|
||||
@click="showHelp = true"
|
||||
@click="helpModalApi.open()"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
v-if="type === 'time'"
|
||||
@click="showDatePicker = true"
|
||||
style="margin-left: 4px"
|
||||
@click="dateModalApi.open()"
|
||||
class="ml-1 flex items-center justify-center"
|
||||
shape="circle"
|
||||
size="small"
|
||||
>
|
||||
@@ -276,8 +304,8 @@ watch(
|
||||
</Button>
|
||||
<Button
|
||||
v-if="type === 'duration'"
|
||||
@click="showDurationDialog = true"
|
||||
style="margin-left: 4px"
|
||||
@click="durationModalApi.open()"
|
||||
class="ml-1 flex items-center justify-center"
|
||||
shape="circle"
|
||||
size="small"
|
||||
>
|
||||
@@ -285,8 +313,8 @@ watch(
|
||||
</Button>
|
||||
<Button
|
||||
v-if="type === 'cycle'"
|
||||
@click="showCycleDialog = true"
|
||||
style="margin-left: 4px"
|
||||
@click="cycleModalApi.open()"
|
||||
class="ml-1 flex items-center justify-center"
|
||||
shape="circle"
|
||||
size="small"
|
||||
>
|
||||
@@ -295,62 +323,32 @@ watch(
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
|
||||
<!-- 时间选择器 -->
|
||||
<Modal
|
||||
v-model:open="showDatePicker"
|
||||
title="选择时间"
|
||||
width="400px"
|
||||
@cancel="showDatePicker = false"
|
||||
>
|
||||
<DateModal>
|
||||
<DatePicker
|
||||
v-model:value="dateValue"
|
||||
show-time
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
class="w-full"
|
||||
@change="onDateChange"
|
||||
/>
|
||||
<template #footer>
|
||||
<Button @click="showDatePicker = false">取消</Button>
|
||||
<Button type="primary" @click="onDateConfirm">确定</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</DateModal>
|
||||
|
||||
<!-- 持续时长选择器 -->
|
||||
<Modal
|
||||
v-model:open="showDurationDialog"
|
||||
title="时间配置"
|
||||
width="600px"
|
||||
@cancel="showDurationDialog = false"
|
||||
>
|
||||
<DurationModal>
|
||||
<DurationConfig :value="condition" @change="onDurationChange" />
|
||||
<template #footer>
|
||||
<Button @click="showDurationDialog = false">取消</Button>
|
||||
<Button type="primary" @click="onDurationConfirm">确定</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</DurationModal>
|
||||
|
||||
<!-- 循环配置器 -->
|
||||
<Modal
|
||||
v-model:open="showCycleDialog"
|
||||
title="时间配置"
|
||||
width="800px"
|
||||
@cancel="showCycleDialog = false"
|
||||
>
|
||||
<CycleModal>
|
||||
<CycleConfig :value="condition" @change="onCycleChange" />
|
||||
<template #footer>
|
||||
<Button @click="showCycleDialog = false">取消</Button>
|
||||
<Button type="primary" @click="onCycleConfirm">确定</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</CycleModal>
|
||||
|
||||
<!-- 帮助说明 -->
|
||||
<Modal
|
||||
v-model:open="showHelp"
|
||||
title="格式说明"
|
||||
width="600px"
|
||||
@cancel="showHelp = false"
|
||||
>
|
||||
<HelpModal>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="helpHtml"></div>
|
||||
<template #footer>
|
||||
<Button @click="showHelp = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</HelpModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
const bpmnInstances = () => (window as any)?.bpmnInstances;
|
||||
// 创建监听器实例
|
||||
export function createListenerObject(options, isTask, prefix) {
|
||||
@@ -76,7 +74,8 @@ export function updateElementExtensions(element, extensionList) {
|
||||
const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
|
||||
values: extensionList,
|
||||
});
|
||||
bpmnInstances().modeling.updateProperties(toRaw(element), {
|
||||
// 直接使用原始元素对象,不需要toRaw包装
|
||||
bpmnInstances().modeling.updateProperties(element, {
|
||||
extensionElements: extensions,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,7 +193,6 @@ const childFormFieldOptions = ref<any[]>([]);
|
||||
const saveConfig = async () => {
|
||||
activeTabName.value = 'child';
|
||||
if (!formRef.value) return false;
|
||||
|
||||
const valid = await formRef.value.validate().catch(() => false);
|
||||
if (!valid) return false;
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ const {
|
||||
} = useNodeForm(BpmNodeTypeEnum.COPY_TASK_NODE);
|
||||
|
||||
const configForm = tempConfigForm as Ref<CopyTaskFormType>;
|
||||
// 抄送人策略, 去掉发起人自选 和 发起人自己
|
||||
// 抄送人策略,去掉发起人自选 和 发起人自己
|
||||
const copyUserStrategies = computed(() => {
|
||||
return CANDIDATE_STRATEGY.filter(
|
||||
(item) => item.value !== CandidateStrategy.START_USER,
|
||||
|
||||
@@ -348,7 +348,7 @@ function getShowText(): string {
|
||||
return showText;
|
||||
}
|
||||
|
||||
/** 显示触发器节点配置, 由父组件传过来 */
|
||||
/** 显示触发器节点配置,由父组件传过来 */
|
||||
function showTriggerNodeConfig(node: SimpleFlowNode) {
|
||||
nodeName.value = node.name;
|
||||
originalSetting = node.triggerSetting
|
||||
|
||||
@@ -532,7 +532,7 @@ function useTimeoutHandler() {
|
||||
if (timeUnit.value === TimeUnitType.HOUR) {
|
||||
configForm.value.timeDuration = 6;
|
||||
}
|
||||
// 天, 默认 1天
|
||||
// 天, 默认 1 天
|
||||
if (timeUnit.value === TimeUnitType.DAY) {
|
||||
configForm.value.timeDuration = 1;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const props = defineProps<{
|
||||
flowNode: SimpleFlowNode;
|
||||
}>();
|
||||
|
||||
/** 定义事件,更新父组件。 */
|
||||
/** 定义事件,更新父组件 */
|
||||
const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
@@ -22,7 +22,7 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
// 定义事件,更新父组件。
|
||||
// 定义事件,更新父组件
|
||||
const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
@@ -20,7 +20,7 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
// 定义事件,更新父组件。
|
||||
// 定义事件,更新父组件
|
||||
const emits = defineEmits<{
|
||||
'update:flowNode': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
@@ -289,7 +289,7 @@ function recursiveFindParentNode(
|
||||
:condition-node="item"
|
||||
:ref="item.id"
|
||||
/>
|
||||
<!-- 递归显示子节点 -->
|
||||
<!-- 递归显示子节点 -->
|
||||
<ProcessNodeTree
|
||||
v-if="item && item.childNode"
|
||||
:parent-node="item"
|
||||
|
||||
@@ -291,7 +291,7 @@ function recursiveFindParentNode(
|
||||
:condition-node="item"
|
||||
:ref="item.id"
|
||||
/>
|
||||
<!-- 递归显示子节点 -->
|
||||
<!-- 递归显示子节点 -->
|
||||
<ProcessNodeTree
|
||||
v-if="item && item.childNode"
|
||||
:parent-node="item"
|
||||
|
||||
@@ -30,7 +30,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
try {
|
||||
const data = modalApi.getData<any[]>();
|
||||
// 填充列表数据
|
||||
await gridApi.setGridOptions({ data });
|
||||
gridApi.setGridOptions({ data });
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
try {
|
||||
const data = modalApi.getData<any[]>();
|
||||
// 填充列表数据
|
||||
await gridApi.setGridOptions({ data });
|
||||
gridApi.setGridOptions({ data });
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
|
||||
@@ -53,10 +53,10 @@ const showInputs = ref<boolean[]>([]);
|
||||
watch(
|
||||
showInputs,
|
||||
(newValues) => {
|
||||
// 当输入框显示时, 自动聚焦
|
||||
// 当输入框显示时 自动聚焦
|
||||
newValues.forEach((value, index) => {
|
||||
if (value) {
|
||||
// 当显示状态从 false 变为 true 时, 自动聚焦
|
||||
// 当显示状态从 false 变为 true 时 自动聚焦
|
||||
nextTick(() => {
|
||||
inputRefs.value[index]?.focus();
|
||||
});
|
||||
@@ -212,7 +212,7 @@ function recursiveFindParentNode(
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 递归显示子节点 -->
|
||||
<!-- 递归显示子节点 -->
|
||||
<ProcessNodeTree
|
||||
v-if="item && item.childNode"
|
||||
:parent-node="item"
|
||||
|
||||
@@ -27,7 +27,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
// 定义事件,更新父组件。
|
||||
// 定义事件,更新父组件
|
||||
defineEmits<{
|
||||
'update:modelValue': [node: SimpleFlowNode | undefined];
|
||||
}>();
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
background: url('./svg/simple-process-bg.svg') 0 0 repeat;
|
||||
transform: scale(1);
|
||||
transform-origin: 50% 0 0;
|
||||
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
// 节点容器 定义节点宽度
|
||||
.node-container {
|
||||
width: 200px;
|
||||
|
||||
@@ -259,9 +259,11 @@ async function validateAllSteps() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const saveLoading = ref<boolean>(false);
|
||||
/** 保存操作 */
|
||||
async function handleSave() {
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
// 保存前校验所有步骤的数据
|
||||
const result = await validateAllSteps();
|
||||
if (!result) {
|
||||
@@ -309,9 +311,12 @@ async function handleSave() {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 发布加载中状态
|
||||
const deployLoading = ref<boolean>(false);
|
||||
/** 发布操作 */
|
||||
async function handleDeploy() {
|
||||
try {
|
||||
@@ -319,6 +324,7 @@ async function handleDeploy() {
|
||||
if (!formData.value.id) {
|
||||
await confirm('是否确认发布该流程?');
|
||||
}
|
||||
deployLoading.value = true;
|
||||
// 1.2 校验所有步骤
|
||||
await validateAllSteps();
|
||||
|
||||
@@ -342,6 +348,8 @@ async function handleDeploy() {
|
||||
} catch (error: any) {
|
||||
console.error('发布失败:', error);
|
||||
message.warning(error.message || '发布失败');
|
||||
} finally {
|
||||
deployLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,11 +456,12 @@ onBeforeUnmount(() => {
|
||||
<Button
|
||||
v-if="actionType === 'update'"
|
||||
type="primary"
|
||||
:loading="deployLoading"
|
||||
@click="handleDeploy"
|
||||
>
|
||||
发 布
|
||||
</Button>
|
||||
<Button type="primary" @click="handleSave">
|
||||
<Button type="primary" @click="handleSave" :loading="saveLoading">
|
||||
<span v-if="actionType === 'definition'">恢 复</span>
|
||||
<span v-else>保 存</span>
|
||||
</Button>
|
||||
|
||||
@@ -25,8 +25,9 @@ import {
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { DeptSelectModal, UserSelectModal } from '#/components/select-modal';
|
||||
import { ImageUpload } from '#/components/upload';
|
||||
import { DeptSelectModal } from '#/views/system/dept/components';
|
||||
import { UserSelectModal } from '#/views/system/user/components';
|
||||
|
||||
const props = defineProps({
|
||||
categoryList: {
|
||||
|
||||
@@ -132,7 +132,7 @@ defineExpose({ validate });
|
||||
placeholder="请输入表单提交路由"
|
||||
/>
|
||||
<Tooltip
|
||||
title="自定义表单的提交路径,使用 Vue 的路由地址, 例如说: bpm/oa/leave/create.vue"
|
||||
title="自定义表单的提交路径,使用 Vue 的路由地址,例如说: /bpm/oa/leave/create.vue"
|
||||
placement="top"
|
||||
>
|
||||
<IconifyIcon
|
||||
@@ -154,7 +154,7 @@ defineExpose({ validate });
|
||||
placeholder="请输入表单查看的组件地址"
|
||||
/>
|
||||
<Tooltip
|
||||
title="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue"
|
||||
title="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:/bpm/oa/leave/detail.vue"
|
||||
placement="top"
|
||||
>
|
||||
<IconifyIcon
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { BpmOALeaveApi } from '#/api/bpm/oa/leave';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { confirm, Page, useVbenForm } from '@vben/common-ui';
|
||||
import { BpmCandidateStrategyEnum, BpmNodeIdEnum } from '@vben/constants';
|
||||
@@ -13,7 +14,7 @@ import { Button, Card, message, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { getProcessDefinition } from '#/api/bpm/definition';
|
||||
import { createLeave, updateLeave } from '#/api/bpm/oa/leave';
|
||||
import { createLeave, getLeave, updateLeave } from '#/api/bpm/oa/leave';
|
||||
import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
@@ -22,6 +23,7 @@ import ProcessInstanceTimeline from '#/views/bpm/processInstance/detail/modules/
|
||||
import { useFormSchema } from './data';
|
||||
|
||||
const { closeCurrentTab } = useTabs();
|
||||
const { query } = useRoute();
|
||||
|
||||
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
||||
@@ -35,7 +37,7 @@ const processDefinitionId = ref('');
|
||||
const formData = ref<BpmOALeaveApi.Leave>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['请假'])
|
||||
? '重新发起请假'
|
||||
: $t('ui.actionTitle.create', ['请假']);
|
||||
});
|
||||
|
||||
@@ -157,6 +159,34 @@ function selectUserConfirm(id: string, userList: any[]) {
|
||||
startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id);
|
||||
}
|
||||
|
||||
/** 获取请假数据,用于重新发起时自动填充 */
|
||||
async function getDetail(id: number) {
|
||||
try {
|
||||
formLoading.value = true;
|
||||
const data = await getLeave(id);
|
||||
if (!data) {
|
||||
message.error('重新发起请假失败,原因:请假数据不存在');
|
||||
return;
|
||||
}
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
id: data.id,
|
||||
type: data.type,
|
||||
reason: data.reason,
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
} as BpmOALeaveApi.Leave;
|
||||
await formApi.setValues({
|
||||
type: data.type,
|
||||
reason: data.reason,
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
});
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 审批相关:预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次, formData.value可改成实际业务中的特定字段 */
|
||||
watch(
|
||||
formData.value as object,
|
||||
@@ -190,6 +220,11 @@ onMounted(async () => {
|
||||
processDefinitionId.value = processDefinitionDetail.id;
|
||||
startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks;
|
||||
|
||||
// 如果是重新发起,则加载请假数据
|
||||
if (query.id) {
|
||||
await getDetail(Number(query.id));
|
||||
}
|
||||
|
||||
await getApprovalDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -168,7 +168,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import { router } from '#/router';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
// TODO @jason:这里是不是要迁移下?
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
@@ -32,6 +33,16 @@ function handleCreate() {
|
||||
});
|
||||
}
|
||||
|
||||
/** 重新发起请假 */
|
||||
function handleReCreate(row: BpmOALeaveApi.Leave) {
|
||||
router.push({
|
||||
name: 'OALeaveCreate',
|
||||
query: {
|
||||
id: row.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消请假 */
|
||||
function handleCancel(row: BpmOALeaveApi.Leave) {
|
||||
prompt({
|
||||
@@ -160,9 +171,16 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
ifShow: row.result === BpmProcessInstanceStatus.RUNNING,
|
||||
ifShow: row.status === BpmProcessInstanceStatus.RUNNING,
|
||||
onClick: handleCancel.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '重新发起',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.ADD,
|
||||
ifShow: row.status !== BpmProcessInstanceStatus.RUNNING,
|
||||
onClick: handleReCreate.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessExpressionApi } from '#/api/bpm/processExpression';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { CommonStatusEnum } from '@vben/constants';
|
||||
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getProcessExpressionPage } from '#/api/bpm/processExpression';
|
||||
|
||||
defineOptions({ name: 'ProcessExpressionSelectModal' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [expression: BpmProcessExpressionApi.ProcessExpression];
|
||||
}>();
|
||||
|
||||
// TODO @jason:这里是不是要迁移下?
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
});
|
||||
|
||||
// 配置 VxeGrid
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ field: 'name', title: '名字', minWidth: 160 },
|
||||
{ field: 'expression', title: '表达式', minWidth: 260 },
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
},
|
||||
],
|
||||
showOverflow: true,
|
||||
minHeight: 300,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 查询表达式列表
|
||||
query: async ({ page }) => {
|
||||
return await getProcessExpressionPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
status: queryParams.value.status,
|
||||
});
|
||||
},
|
||||
},
|
||||
} as VxeGridPropTypes.ProxyConfig,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 配置 Modal
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
showConfirmButton: false,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// 选择表达式
|
||||
function handleSelect(row: BpmProcessExpressionApi.ProcessExpression) {
|
||||
emit('select', row);
|
||||
modalApi.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-4/5" title="请选择表达式">
|
||||
<Grid>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '选择',
|
||||
type: 'link',
|
||||
icon: 'lucide:pointer',
|
||||
onClick: handleSelect.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -228,9 +228,10 @@ onMounted(() => {
|
||||
>
|
||||
<Card
|
||||
hoverable
|
||||
class="definition-item-card w-full cursor-pointer"
|
||||
class="w-full cursor-pointer"
|
||||
:class="{
|
||||
'search-match': searchName.trim().length > 0,
|
||||
'animate-bounce-once !bg-[rgb(63_115_247_/_10%)]':
|
||||
searchName.trim().length > 0,
|
||||
}"
|
||||
:body-style="{
|
||||
width: '100%',
|
||||
@@ -241,10 +242,13 @@ onMounted(() => {
|
||||
<img
|
||||
v-if="definition.icon"
|
||||
:src="definition.icon"
|
||||
class="flow-icon-img object-contain"
|
||||
class="size-12 rounded object-contain"
|
||||
alt="流程图标"
|
||||
/>
|
||||
<div v-else class="flow-icon flex-shrink-0">
|
||||
<div
|
||||
v-else
|
||||
class="flex size-12 flex-shrink-0 items-center justify-center rounded bg-primary"
|
||||
>
|
||||
<span class="text-xs text-white">
|
||||
{{ definition.name?.slice(0, 2) }}
|
||||
</span>
|
||||
@@ -283,7 +287,6 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// @jason:看看能不能通过 tailwindcss 简化下
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
50% {
|
||||
@@ -295,30 +298,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.process-definition-container {
|
||||
.definition-item-card {
|
||||
.flow-icon-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.flow-icon {
|
||||
@apply bg-primary;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
&.search-match {
|
||||
background-color: rgb(63 115 247 / 10%);
|
||||
border: 1px solid var(--primary);
|
||||
animation: bounce 0.5s ease;
|
||||
}
|
||||
}
|
||||
.animate-bounce-once {
|
||||
animation: bounce 0.5s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -104,7 +104,7 @@ async function submitForm() {
|
||||
// 关闭并提示
|
||||
message.success('发起流程成功');
|
||||
await closeCurrentTab();
|
||||
await router.push({ name: 'BpmTaskMy' });
|
||||
await router.push({ name: 'BpmProcessInstanceMy' });
|
||||
} finally {
|
||||
processInstanceStartLoading.value = false;
|
||||
}
|
||||
@@ -169,6 +169,7 @@ async function initProcessInfo(row: any, formVariables?: any) {
|
||||
path: row.formCustomCreatePath,
|
||||
});
|
||||
// 返回选择流程
|
||||
// TODO @jason:这里为啥要有个 cancel 事件哈?目前看 vue3 + element-plus 貌似不需要呀;
|
||||
emit('cancel');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,20 +212,27 @@ watch(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await getDetail();
|
||||
// 获得用户列表
|
||||
userOptions.value = await getSimpleUserList();
|
||||
try {
|
||||
loading.value = true;
|
||||
await getDetail();
|
||||
// 获得用户列表
|
||||
userOptions.value = await getSimpleUserList();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Page auto-content-height v-loading="loading">
|
||||
<Card
|
||||
class="flex h-full flex-col"
|
||||
:body-style="{
|
||||
overflowY: 'auto',
|
||||
flex: 1,
|
||||
overflowY: 'hidden',
|
||||
paddingTop: '12px',
|
||||
}"
|
||||
>
|
||||
@@ -286,24 +293,16 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<!-- 流程操作 -->
|
||||
<div class="process-tabs-container flex flex-1 flex-col">
|
||||
<Tabs v-model:active-key="activeTab" class="mt-0 h-full">
|
||||
<TabPane tab="审批详情" key="form" class="tab-pane-content">
|
||||
<Row :gutter="[48, 24]" class="h-full">
|
||||
<Col
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="18"
|
||||
:lg="18"
|
||||
:xl="16"
|
||||
class="h-full"
|
||||
>
|
||||
<div class="flex h-full flex-1 flex-col">
|
||||
<Tabs v-model:active-key="activeTab">
|
||||
<TabPane tab="审批详情" key="form" class="pb-20 pr-3">
|
||||
<Row :gutter="[48, 24]">
|
||||
<Col :xs="24" :sm="24" :md="18" :lg="18" :xl="16">
|
||||
<!-- 流程表单 -->
|
||||
<div
|
||||
v-if="
|
||||
processDefinition?.formType === BpmModelFormType.NORMAL
|
||||
"
|
||||
class="h-full"
|
||||
>
|
||||
<form-create
|
||||
v-model="detailForm.value"
|
||||
@@ -316,13 +315,12 @@ onMounted(async () => {
|
||||
v-else-if="
|
||||
processDefinition?.formType === BpmModelFormType.CUSTOM
|
||||
"
|
||||
class="h-full"
|
||||
>
|
||||
<BusinessFormComponent :id="processInstance?.businessKey" />
|
||||
</div>
|
||||
</Col>
|
||||
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8" class="h-full">
|
||||
<div class="mt-4 h-full">
|
||||
<Col :xs="24" :sm="24" :md="6" :lg="6" :xl="8">
|
||||
<div class="mt-4">
|
||||
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
|
||||
</div>
|
||||
</Col>
|
||||
@@ -331,44 +329,35 @@ onMounted(async () => {
|
||||
<TabPane
|
||||
tab="流程图"
|
||||
key="diagram"
|
||||
class="tab-pane-content"
|
||||
class="pb-20 pr-3"
|
||||
:force-render="true"
|
||||
>
|
||||
<div class="h-full">
|
||||
<ProcessInstanceSimpleViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.SIMPLE
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
<ProcessInstanceBpmnViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.BPMN
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
</div>
|
||||
<ProcessInstanceSimpleViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.SIMPLE
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
<ProcessInstanceBpmnViewer
|
||||
v-show="
|
||||
processDefinition.modelType &&
|
||||
processDefinition.modelType === BpmModelType.BPMN
|
||||
"
|
||||
:loading="processInstanceLoading"
|
||||
:model-view="processModelView"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="流转记录" key="record" class="tab-pane-content">
|
||||
<div class="h-full">
|
||||
<BpmProcessInstanceTaskList
|
||||
ref="taskListRef"
|
||||
:loading="processInstanceLoading"
|
||||
:id="id"
|
||||
/>
|
||||
</div>
|
||||
<TabPane tab="流转记录" key="record" class="pb-20 pr-3">
|
||||
<BpmProcessInstanceTaskList
|
||||
ref="taskListRef"
|
||||
:loading="processInstanceLoading"
|
||||
:id="id"
|
||||
/>
|
||||
</TabPane>
|
||||
<!-- TODO 待开发 -->
|
||||
<TabPane
|
||||
tab="流转评论"
|
||||
key="comment"
|
||||
v-if="false"
|
||||
class="tab-pane-content"
|
||||
>
|
||||
<TabPane tab="流转评论" key="comment" v-if="false" class="pr-3">
|
||||
<div class="h-full">待开发</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
@@ -396,35 +385,18 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// @jason:看看能不能通过 tailwindcss 简化下
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.process-tabs-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-content) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
.ant-tabs-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tabpane) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-pane-content {
|
||||
height: calc(100vh - 420px);
|
||||
padding-right: 12px;
|
||||
overflow: hidden auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { base64ToFile } from '@vben/utils';
|
||||
|
||||
import { Button, Space, Tooltip } from 'ant-design-vue';
|
||||
import { Button, Tooltip } from 'ant-design-vue';
|
||||
import Vue3Signature from 'vue3-signature';
|
||||
|
||||
import { uploadFile } from '#/api/infra/file';
|
||||
@@ -36,30 +36,29 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
<template>
|
||||
<Modal title="流程签名" class="w-3/5">
|
||||
<div class="mb-2 flex justify-end">
|
||||
<Space>
|
||||
<div class="flex h-[50vh] flex-col">
|
||||
<div class="mb-2 flex justify-end gap-2">
|
||||
<Tooltip title="撤销上一步操作">
|
||||
<Button @click="signature?.undo()">
|
||||
<Button @click="signature?.undo()" size="small">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:undo" class="mb-1 size-4" />
|
||||
<IconifyIcon icon="lucide:undo" class="mb-1 size-3" />
|
||||
</template>
|
||||
撤销
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="清空画布">
|
||||
<Button @click="signature?.clear()">
|
||||
<Button @click="signature?.clear()" size="small">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:trash" class="mb-1 size-4" />
|
||||
<IconifyIcon icon="lucide:trash" class="mb-1 size-3" />
|
||||
</template>
|
||||
<span>清除</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
<Vue3Signature
|
||||
class="h-full flex-1 border border-solid border-gray-300"
|
||||
ref="signature"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Vue3Signature
|
||||
class="mx-auto !h-80 border border-solid border-gray-300"
|
||||
ref="signature"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -44,7 +44,7 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
field: 'approver',
|
||||
title: '审批人',
|
||||
slots: {
|
||||
default: ({ row }: { row: BpmTaskApi.TaskManager }) => {
|
||||
default: ({ row }: { row: BpmTaskApi.Task }) => {
|
||||
return row.assigneeUser?.nickname || row.ownerUser?.nickname;
|
||||
},
|
||||
},
|
||||
@@ -106,7 +106,7 @@ function handleRefresh() {
|
||||
}
|
||||
|
||||
/** 显示表单详情 */
|
||||
async function handleShowFormDetail(row: BpmTaskApi.TaskManager) {
|
||||
async function handleShowFormDetail(row: BpmTaskApi.Task) {
|
||||
// 设置表单配置和表单字段
|
||||
taskForm.value = {
|
||||
rule: [],
|
||||
@@ -141,7 +141,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
keepSource: true,
|
||||
showFooter: true,
|
||||
border: true,
|
||||
height: 'auto',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
@@ -159,7 +158,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
||||
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
@@ -168,7 +167,7 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<div>
|
||||
<Grid>
|
||||
<template #slot-reason="{ row }">
|
||||
<div class="flex flex-wrap items-center justify-center">
|
||||
@@ -188,13 +187,13 @@ defineExpose({
|
||||
</div>
|
||||
</template>
|
||||
</Grid>
|
||||
<Modal class="w-[800px]">
|
||||
<form-create
|
||||
ref="formRef"
|
||||
v-model="taskForm.value"
|
||||
:option="taskForm.option"
|
||||
:rule="taskForm.rule"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
<Modal class="w-3/5">
|
||||
<form-create
|
||||
ref="formRef"
|
||||
v-model="taskForm.value"
|
||||
:option="taskForm.option"
|
||||
:rule="taskForm.rule"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { formatDateTime, isEmpty } from '@vben/utils';
|
||||
|
||||
import { Avatar, Button, Image, Timeline, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { UserSelectModal } from '#/components/select-modal';
|
||||
import { UserSelectModal } from '#/views/system/user/components';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceTimeline' });
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DocAlert, Page, prompt } from '@vben/common-ui';
|
||||
import { BpmProcessInstanceStatus, DICT_TYPE } from '@vben/constants';
|
||||
import {
|
||||
BpmModelFormType,
|
||||
BpmProcessInstanceStatus,
|
||||
DICT_TYPE,
|
||||
} from '@vben/constants';
|
||||
|
||||
import { Button, message, Textarea } from 'ant-design-vue';
|
||||
|
||||
@@ -37,23 +41,34 @@ function handleDetail(row: BpmProcessInstanceApi.ProcessInstance) {
|
||||
}
|
||||
|
||||
/** 重新发起流程 */
|
||||
async function handleCreate(row: BpmProcessInstanceApi.ProcessInstance) {
|
||||
// 如果是【业务表单】,不支持重新发起
|
||||
async function handleCreate(row?: BpmProcessInstanceApi.ProcessInstance) {
|
||||
if (row?.id) {
|
||||
const processDefinitionDetail = await getProcessDefinition(
|
||||
row.processDefinitionId,
|
||||
);
|
||||
if (processDefinitionDetail.formType === 20) {
|
||||
message.error(
|
||||
'重新发起流程失败,原因:该流程使用业务表单,不支持重新发起',
|
||||
);
|
||||
if (processDefinitionDetail?.formType === BpmModelFormType.CUSTOM) {
|
||||
if (!processDefinitionDetail.formCustomCreatePath) {
|
||||
message.error('未配置业务表单的提交路由,无法重新发起');
|
||||
return;
|
||||
}
|
||||
await router.push({
|
||||
path: processDefinitionDetail.formCustomCreatePath,
|
||||
query: {
|
||||
id: row.businessKey,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} else if (processDefinitionDetail?.formType === BpmModelFormType.NORMAL) {
|
||||
await router.push({
|
||||
name: 'BpmProcessInstanceCreate',
|
||||
query: { processInstanceId: row.id },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 跳转发起流程界面
|
||||
await router.push({
|
||||
name: 'BpmProcessInstanceCreate',
|
||||
query: { processInstanceId: row?.id },
|
||||
query: row?.id ? { processInstanceId: row.id } : {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
|
||||
/** 选择监听器弹窗的列表字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ field: 'name', title: '名字', minWidth: 120 },
|
||||
{
|
||||
field: 'type',
|
||||
title: '类型',
|
||||
minWidth: 200,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.BPM_PROCESS_LISTENER_TYPE },
|
||||
},
|
||||
},
|
||||
{ field: 'event', title: '事件', minWidth: 200 },
|
||||
{
|
||||
field: 'valueType',
|
||||
title: '值类型',
|
||||
minWidth: 200,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE },
|
||||
},
|
||||
},
|
||||
{ field: 'value', title: '值', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
slots: { default: 'action' },
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridPropTypes } from '#/adapter/vxe-table';
|
||||
import type { BpmProcessListenerApi } from '#/api/bpm/processListener';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { CommonStatusEnum } from '@vben/constants';
|
||||
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getProcessListenerPage } from '#/api/bpm/processListener';
|
||||
|
||||
import { useGridColumns } from './data';
|
||||
|
||||
defineOptions({ name: 'ProcessListenerSelectModal' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [listener: BpmProcessListenerApi.ProcessListener];
|
||||
}>();
|
||||
|
||||
// TODO @jason:这里是不是要迁移下?
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
type: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
});
|
||||
|
||||
// 配置 VxeGrid
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
showOverflow: true,
|
||||
minHeight: 300,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getProcessListenerPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
type: queryParams.value.type,
|
||||
status: queryParams.value.status,
|
||||
});
|
||||
},
|
||||
},
|
||||
} as VxeGridPropTypes.ProxyConfig,
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 配置 Modal
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
showConfirmButton: false,
|
||||
onOpenChange: async (isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
queryParams.value.type = '';
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ type: string }>();
|
||||
if (data?.type) {
|
||||
queryParams.value.type = data.type;
|
||||
}
|
||||
},
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// 选择监听器
|
||||
function handleSelect(row: BpmProcessListenerApi.ProcessListener) {
|
||||
emit('select', row);
|
||||
modalApi.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-4/5" title="请选择监听器">
|
||||
<Grid>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '选择',
|
||||
type: 'link',
|
||||
icon: 'lucide:pointer',
|
||||
onClick: handleSelect.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -8,22 +8,22 @@ import { z } from '#/adapter/form';
|
||||
|
||||
export const EVENT_EXECUTION_OPTIONS = [
|
||||
{
|
||||
label: 'start',
|
||||
label: '开始',
|
||||
value: 'start',
|
||||
},
|
||||
{
|
||||
label: 'end',
|
||||
label: '结束',
|
||||
value: 'end',
|
||||
},
|
||||
];
|
||||
|
||||
export const EVENT_OPTIONS = [
|
||||
{ label: 'create', value: 'create' },
|
||||
{ label: 'assignment', value: 'assignment' },
|
||||
{ label: 'complete', value: 'complete' },
|
||||
{ label: 'delete', value: 'delete' },
|
||||
{ label: 'update', value: 'update' },
|
||||
{ label: 'timeout', value: 'timeout' },
|
||||
{ label: '创建', value: 'create' },
|
||||
{ label: '指派', value: 'assignment' },
|
||||
{ label: '完成', value: 'complete' },
|
||||
{ label: '删除', value: 'delete' },
|
||||
{ label: '更新', value: 'update' },
|
||||
{ label: '超时', value: 'timeout' },
|
||||
];
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
defineOptions({ name: 'BpmDoneTask' });
|
||||
|
||||
/** 查看历史 */
|
||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
||||
function handleHistory(row: BpmTaskApi.Task) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
@@ -26,7 +26,7 @@ function handleHistory(row: BpmTaskApi.TaskManager) {
|
||||
}
|
||||
|
||||
/** 撤回任务 */
|
||||
async function handleWithdraw(row: BpmTaskApi.TaskManager) {
|
||||
async function handleWithdraw(row: BpmTaskApi.Task) {
|
||||
const hideLoading = message.loading({
|
||||
content: '正在撤回中...',
|
||||
duration: 0,
|
||||
@@ -67,7 +67,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<BpmTaskApi.TaskManager>,
|
||||
} as VxeTableGridOptions<BpmTaskApi.Task>,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useGridColumns, useGridFormSchema } from './data';
|
||||
defineOptions({ name: 'BpmManagerTask' });
|
||||
|
||||
/** 查看历史 */
|
||||
function handleHistory(row: BpmTaskApi.TaskManager) {
|
||||
function handleHistory(row: BpmTaskApi.Task) {
|
||||
router.push({
|
||||
name: 'BpmProcessInstanceDetail',
|
||||
query: {
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
// 客户转化率分析
|
||||
case 'conversionStat': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '客户转化率',
|
||||
@@ -40,12 +57,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户转化率分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '转化率(%)',
|
||||
@@ -59,14 +71,13 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'customerSummary': {
|
||||
return {
|
||||
grid: {
|
||||
bottom: '5%',
|
||||
containLabel: true,
|
||||
grid: getGrid({
|
||||
bottom: '8%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
top: '5 %',
|
||||
},
|
||||
legend: {},
|
||||
top: 80,
|
||||
}),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增客户数',
|
||||
@@ -92,12 +103,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -134,13 +140,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
};
|
||||
});
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
@@ -166,12 +167,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -208,13 +204,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
};
|
||||
});
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
@@ -240,12 +231,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -277,13 +263,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
const customerDealCycleByDate = res.customerDealCycleByDate;
|
||||
const customerDealCycleByUser = res.customerDealCycleByUser;
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
@@ -309,12 +290,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -342,15 +318,13 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
},
|
||||
};
|
||||
}
|
||||
// 客户跟进次数分析
|
||||
case 'followUpSummary': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
grid: getGrid({
|
||||
right: 30, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
}),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '跟进客户数',
|
||||
@@ -376,12 +350,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户跟进次数分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -412,20 +381,21 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
},
|
||||
};
|
||||
}
|
||||
// 客户跟进方式分析
|
||||
case 'followUpType': {
|
||||
return {
|
||||
title: {
|
||||
text: '客户跟进方式分析',
|
||||
left: 'center',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
legend: getLegend({
|
||||
left: 'left',
|
||||
},
|
||||
tooltip: {
|
||||
}),
|
||||
tooltip: getTooltip({
|
||||
trigger: 'item',
|
||||
axisPointer: undefined,
|
||||
formatter: '{b} : {c}% ',
|
||||
},
|
||||
}),
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '客户跟进方式分析' }, // 保存为图片
|
||||
@@ -458,13 +428,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'poolSummary': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '进入公海客户数',
|
||||
@@ -490,12 +455,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '公海客户分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import { erpCalculatePercentage } from '@vben/utils';
|
||||
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export function getChartOptions(
|
||||
activeTabName: any,
|
||||
active: boolean,
|
||||
@@ -9,26 +30,19 @@ export function getChartOptions(
|
||||
case 'businessInversionRateSummary': {
|
||||
return {
|
||||
color: ['#6ca2ff', '#6ac9d7', '#ff7474'],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
// 坐标轴指示器,坐标轴触发有效
|
||||
type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
tooltip: getTooltip(),
|
||||
legend: getLegend({
|
||||
data: ['赢单转化率', '商机总数', '赢单商机数'],
|
||||
bottom: '0px',
|
||||
itemWidth: 14,
|
||||
},
|
||||
grid: {
|
||||
}),
|
||||
grid: getGrid({
|
||||
top: '40px',
|
||||
left: '40px',
|
||||
right: '40px',
|
||||
bottom: '40px',
|
||||
containLabel: true,
|
||||
borderColor: '#fff',
|
||||
},
|
||||
}),
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
@@ -117,13 +131,11 @@ export function getChartOptions(
|
||||
}
|
||||
case 'businessSummary': {
|
||||
return {
|
||||
grid: {
|
||||
grid: getGrid({
|
||||
left: 30,
|
||||
right: 30, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
}),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增商机数量',
|
||||
@@ -149,12 +161,7 @@ export function getChartOptions(
|
||||
saveAsImage: { show: true, name: '新增商机分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -211,10 +218,11 @@ export function getChartOptions(
|
||||
title: {
|
||||
text: '销售漏斗',
|
||||
},
|
||||
tooltip: {
|
||||
tooltip: getTooltip({
|
||||
trigger: 'item',
|
||||
axisPointer: undefined,
|
||||
formatter: '{a} <br/>{b}',
|
||||
},
|
||||
}),
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataView: { readOnly: false },
|
||||
@@ -222,9 +230,9 @@ export function getChartOptions(
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
legend: getLegend({
|
||||
data: ['客户', '商机', '赢单'],
|
||||
},
|
||||
}),
|
||||
series: [
|
||||
{
|
||||
name: '销售漏斗',
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'ContractCountPerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '当月合同数量(个)',
|
||||
@@ -65,12 +81,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -131,13 +142,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'ContractPricePerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '当月合同金额(元)',
|
||||
@@ -260,13 +266,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'ReceivablePricePerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '当月回款金额(元)',
|
||||
|
||||
@@ -13,6 +13,71 @@ function areaReplace(areaName: string) {
|
||||
.replace('省', '');
|
||||
}
|
||||
|
||||
const getPieTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'item',
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getPieLegend = (extra: Record<string, any> = {}) => ({
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getPieSeries = (name: string, data: any[]) => ({
|
||||
name,
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
const getPiePanel = ({
|
||||
data,
|
||||
legendExtra,
|
||||
seriesName,
|
||||
title,
|
||||
tooltipExtra,
|
||||
}: {
|
||||
data: any[];
|
||||
legendExtra?: Record<string, any>;
|
||||
seriesName: string;
|
||||
title: string;
|
||||
tooltipExtra?: Record<string, any>;
|
||||
}) => ({
|
||||
title: {
|
||||
text: title,
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: getPieTooltip(tooltipExtra),
|
||||
legend: getPieLegend(legendExtra),
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: title },
|
||||
},
|
||||
},
|
||||
series: [getPieSeries(seriesName, data)],
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'area': {
|
||||
@@ -111,326 +176,62 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'industry': {
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '全部客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '全部客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(
|
||||
DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
r.industryId,
|
||||
),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '成交客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(
|
||||
DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
r.industryId,
|
||||
),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: getPiePanel({
|
||||
title: '全部客户',
|
||||
seriesName: '全部客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
|
||||
value: r.customerCount,
|
||||
})),
|
||||
}),
|
||||
right: getPiePanel({
|
||||
title: '成交客户',
|
||||
seriesName: '成交客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
|
||||
value: r.dealCount,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'level': {
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '全部客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '全部客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '成交客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: getPiePanel({
|
||||
title: '全部客户',
|
||||
seriesName: '全部客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.customerCount,
|
||||
})),
|
||||
}),
|
||||
right: getPiePanel({
|
||||
title: '成交客户',
|
||||
seriesName: '成交客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.dealCount,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'source': {
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '全部客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '全部客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '成交客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: getPiePanel({
|
||||
title: '全部客户',
|
||||
seriesName: '全部客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.customerCount,
|
||||
})),
|
||||
}),
|
||||
right: getPiePanel({
|
||||
title: '成交客户',
|
||||
seriesName: '成交客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.dealCount,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = () => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'contactCountRank': {
|
||||
@@ -8,15 +28,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增联系人数排行',
|
||||
@@ -34,12 +47,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '新增联系人数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '新增联系人数(个)',
|
||||
@@ -56,15 +64,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '签约合同排行',
|
||||
@@ -82,12 +83,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '签约合同排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '签约合同数(个)',
|
||||
@@ -104,15 +100,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '合同金额排行',
|
||||
@@ -130,12 +119,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '合同金额排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '合同金额(元)',
|
||||
@@ -152,15 +136,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增客户数排行',
|
||||
@@ -178,12 +155,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '新增客户数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '新增客户数(个)',
|
||||
@@ -226,12 +198,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '跟进次数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '跟进次数(次)',
|
||||
@@ -274,12 +241,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '跟进客户数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '跟进客户数(个)',
|
||||
@@ -322,12 +284,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '产品销量排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '产品销量',
|
||||
@@ -370,12 +327,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '回款金额排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '回款金额(元)',
|
||||
|
||||
@@ -78,7 +78,8 @@ function handleRowCheckboxChange({
|
||||
}: {
|
||||
records: InfraDataSourceConfigApi.DataSourceConfig[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
// 过滤掉id为 0 的主数据源
|
||||
checkedIds.value = records.map((item) => item.id!).filter((id) => id !== 0);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
@@ -140,6 +141,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['infra:data-source-config:update'],
|
||||
disabled: row.id === 0,
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
@@ -148,6 +150,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['infra:data-source-config:delete'],
|
||||
disabled: row.id === 0,
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
|
||||
@@ -229,6 +229,18 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'config.region',
|
||||
label: '区域',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请填写区域,一般仅 AWS 需要填写',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['storage'],
|
||||
show: (formValues) => formValues.storage === 20,
|
||||
},
|
||||
},
|
||||
// 通用
|
||||
{
|
||||
fieldName: 'config.domain',
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { DeviceTypeEnum, DICT_TYPE, LocationTypeEnum } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleDeviceList } from '#/api/iot/device/device';
|
||||
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
|
||||
import {
|
||||
DeviceTypeEnum,
|
||||
getSimpleProductList,
|
||||
} from '#/api/iot/product/product';
|
||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||
|
||||
/** 新增/修改的表单 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
@@ -33,6 +30,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values: any) => !!values?.id,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -42,6 +43,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
componentProps: {
|
||||
placeholder: '请输入 DeviceName',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
disabled: (values: any) => !!values?.id,
|
||||
},
|
||||
rules: z
|
||||
.string()
|
||||
.min(4, 'DeviceName 长度不能少于 4 个字符')
|
||||
@@ -63,7 +68,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['deviceType'],
|
||||
show: (values) => values.deviceType === 1, // GATEWAY_SUB
|
||||
show: (values) => values.deviceType === DeviceTypeEnum.GATEWAY_SUB,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -129,20 +134,20 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['locationType'],
|
||||
show: (values) => values.locationType === 3, // MANUAL
|
||||
show: (values) => values.locationType === LocationTypeEnum.MANUAL,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'latitude',
|
||||
label: '设备维度',
|
||||
label: '设备纬度',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入设备维度',
|
||||
placeholder: '请输入设备纬度',
|
||||
class: 'w-full',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['locationType'],
|
||||
show: (values) => values.locationType === 3, // MANUAL
|
||||
show: (values) => values.locationType === LocationTypeEnum.MANUAL,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
@@ -7,20 +7,21 @@ import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { DeviceTypeEnum } from '@vben/constants';
|
||||
|
||||
import { message, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { getDevice } from '#/api/iot/device/device';
|
||||
import { DeviceTypeEnum, getProduct } from '#/api/iot/product/product';
|
||||
import { getProduct } from '#/api/iot/product/product';
|
||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
||||
|
||||
import DeviceDetailConfig from './device-detail-config.vue';
|
||||
import DeviceDetailsHeader from './device-details-header.vue';
|
||||
import DeviceDetailsInfo from './device-details-info.vue';
|
||||
import DeviceDetailsMessage from './device-details-message.vue';
|
||||
import DeviceDetailsSimulator from './device-details-simulator.vue';
|
||||
import DeviceDetailsSubDevice from './device-details-sub-device.vue';
|
||||
import DeviceDetailsThingModel from './device-details-thing-model.vue';
|
||||
import DeviceDetailConfig from './modules/config.vue';
|
||||
import DeviceDetailsHeader from './modules/header.vue';
|
||||
import DeviceDetailsInfo from './modules/info.vue';
|
||||
import DeviceDetailsMessage from './modules/message.vue';
|
||||
import DeviceDetailsSimulator from './modules/simulator.vue';
|
||||
import DeviceDetailsSubDevice from './modules/sub-device.vue';
|
||||
import DeviceDetailsThingModel from './modules/thing-model.vue';
|
||||
|
||||
defineOptions({ name: 'IoTDeviceDetail' });
|
||||
|
||||
@@ -52,8 +53,8 @@ async function getDeviceData(deviceId: number) {
|
||||
async function getProductData(productId: number) {
|
||||
try {
|
||||
product.value = await getProduct(productId);
|
||||
} catch (error) {
|
||||
console.error('获取产品详情失败:', error);
|
||||
} catch {
|
||||
message.error('获取产品详情失败');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +63,8 @@ async function getThingModelList(productId: number) {
|
||||
try {
|
||||
const data = await getThingModelListByProductId(productId);
|
||||
thingModelList.value = data || [];
|
||||
} catch (error) {
|
||||
console.error('获取物模型列表失败:', error);
|
||||
} catch {
|
||||
message.error('获取物模型列表失败');
|
||||
thingModelList.value = [];
|
||||
}
|
||||
}
|
||||
@@ -88,9 +89,9 @@ onMounted(async () => {
|
||||
<template>
|
||||
<Page>
|
||||
<DeviceDetailsHeader
|
||||
:device="device"
|
||||
:loading="loading"
|
||||
:product="product"
|
||||
:device="device"
|
||||
@refresh="() => getDeviceData(id)"
|
||||
/>
|
||||
|
||||
@@ -98,8 +99,8 @@ onMounted(async () => {
|
||||
<Tabs.TabPane key="info" tab="设备信息">
|
||||
<DeviceDetailsInfo
|
||||
v-if="activeTab === 'info'"
|
||||
:product="product"
|
||||
:device="device"
|
||||
:product="product"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="model" tab="物模型数据">
|
||||
@@ -128,8 +129,8 @@ onMounted(async () => {
|
||||
<Tabs.TabPane key="simulator" tab="模拟设备">
|
||||
<DeviceDetailsSimulator
|
||||
v-if="activeTab === 'simulator'"
|
||||
:product="product"
|
||||
:device="device"
|
||||
:product="product"
|
||||
:thing-model-list="thingModelList"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
@@ -21,6 +21,7 @@ const emit = defineEmits<{
|
||||
|
||||
const loading = ref(false); // 加载中
|
||||
const pushLoading = ref(false); // 推送加载中
|
||||
const saveLoading = ref(false); // 保存加载中
|
||||
const config = ref<any>({}); // 只存储 config 字段
|
||||
const configString = ref(''); // 用于编辑器的字符串格式
|
||||
|
||||
@@ -50,20 +51,15 @@ const formattedConfig = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
/** 判断配置是否有数据 */
|
||||
const hasConfigData = computed(() => {
|
||||
return config.value && Object.keys(config.value).length > 0;
|
||||
});
|
||||
|
||||
/** 启用编辑模式的函数 */
|
||||
function enableEdit() {
|
||||
function handleEdit() {
|
||||
isEditing.value = true;
|
||||
// 重新同步编辑器内容
|
||||
configString.value = JSON.stringify(config.value, null, 2);
|
||||
}
|
||||
|
||||
/** 取消编辑的函数 */
|
||||
function cancelEdit() {
|
||||
function handleCancelEdit() {
|
||||
try {
|
||||
config.value = props.device.config ? JSON.parse(props.device.config) : {};
|
||||
configString.value = JSON.stringify(config.value, null, 2);
|
||||
@@ -84,29 +80,27 @@ async function saveConfig() {
|
||||
message.error({ content: 'JSON格式错误,请修正后再提交!' });
|
||||
return;
|
||||
}
|
||||
|
||||
await updateDeviceConfig();
|
||||
isEditing.value = false;
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
await updateDeviceConfig();
|
||||
isEditing.value = false;
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 配置推送处理函数 */
|
||||
async function handleConfigPush() {
|
||||
pushLoading.value = true;
|
||||
try {
|
||||
pushLoading.value = true;
|
||||
|
||||
// 调用配置推送接口
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method,
|
||||
params: config.value,
|
||||
});
|
||||
|
||||
// 提示成功
|
||||
message.success({ content: '配置推送成功!' });
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
message.error({ content: '配置推送失败!' });
|
||||
console.error('配置推送错误:', error);
|
||||
}
|
||||
} finally {
|
||||
pushLoading.value = false;
|
||||
}
|
||||
@@ -124,8 +118,6 @@ async function updateDeviceConfig() {
|
||||
message.success({ content: '更新成功!' });
|
||||
// 触发 success 事件
|
||||
emit('success');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -134,35 +126,14 @@ async function updateDeviceConfig() {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 只在没有配置数据时显示提示 -->
|
||||
<!-- 使用说明提示 -->
|
||||
<Alert
|
||||
v-if="!hasConfigData"
|
||||
message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「下发」按钮,设备即可进行远程配置。"
|
||||
type="info"
|
||||
show-icon
|
||||
class="my-4"
|
||||
description="如需编辑文件,请点击下方编辑按钮"
|
||||
message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「配置推送」按钮,设备即可进行远程配置。"
|
||||
show-icon
|
||||
type="info"
|
||||
/>
|
||||
<div class="mt-5 text-center">
|
||||
<Button v-if="isEditing" @click="cancelEdit">取消</Button>
|
||||
<Button
|
||||
v-if="isEditing"
|
||||
type="primary"
|
||||
@click="saveConfig"
|
||||
:loading="loading"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button v-else @click="enableEdit">编辑</Button>
|
||||
<Button
|
||||
v-if="!isEditing"
|
||||
type="primary"
|
||||
@click="handleConfigPush"
|
||||
:loading="pushLoading"
|
||||
>
|
||||
配置推送
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 代码视图 - 只读展示 -->
|
||||
<div v-if="!isEditing" class="json-viewer-container">
|
||||
@@ -174,9 +145,31 @@ async function updateDeviceConfig() {
|
||||
v-else
|
||||
v-model:value="configString"
|
||||
:rows="20"
|
||||
placeholder="请输入 JSON 格式的配置信息"
|
||||
class="json-editor"
|
||||
placeholder="请输入 JSON 格式的配置信息"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-5 text-center">
|
||||
<Button v-if="isEditing" @click="handleCancelEdit">取消</Button>
|
||||
<Button
|
||||
v-if="isEditing"
|
||||
:loading="saveLoading"
|
||||
type="primary"
|
||||
@click="saveConfig"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button v-else @click="handleEdit">编辑</Button>
|
||||
<Button
|
||||
v-if="!isEditing"
|
||||
:loading="pushLoading"
|
||||
type="primary"
|
||||
@click="handleConfigPush"
|
||||
>
|
||||
配置推送
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<!-- 设备信息(头部) -->
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Descriptions, message } from 'ant-design-vue';
|
||||
|
||||
import DeviceForm from '../device-form.vue';
|
||||
import DeviceForm from '../../modules/form.vue';
|
||||
|
||||
interface Props {
|
||||
product: IotProductApi.Product;
|
||||
@@ -26,20 +26,19 @@ const emit = defineEmits<{
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
/** 操作修改 */
|
||||
const formRef = ref();
|
||||
function openForm(type: string, id?: number) {
|
||||
formRef.value.open(type, id);
|
||||
}
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: DeviceForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 复制到剪贴板方法 */
|
||||
/** 复制到剪贴板 */
|
||||
async function copyToClipboard(text: string | undefined) {
|
||||
if (!text) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
message.success({ content: '复制成功' });
|
||||
message.success('复制成功');
|
||||
} catch {
|
||||
message.error({ content: '复制失败' });
|
||||
message.error('复制失败');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,19 +48,25 @@ function goToProductDetail(productId: number | undefined) {
|
||||
router.push({ name: 'IoTProductDetail', params: { id: productId } });
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开编辑表单 */
|
||||
function openEditForm(row: IotDeviceApi.Device) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="mb-4">
|
||||
<FormModal @success="emit('refresh')" />
|
||||
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">{{ device.deviceName }}</h2>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<!-- 右上:按钮 -->
|
||||
<Button
|
||||
v-if="product.status === 0"
|
||||
v-access:code="['iot:device:update']"
|
||||
@click="openForm('update', device.id)"
|
||||
@click="openEditForm(device)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
@@ -69,11 +74,11 @@ function goToProductDetail(productId: number | undefined) {
|
||||
</div>
|
||||
|
||||
<Card class="mt-4">
|
||||
<Descriptions :column="1">
|
||||
<Descriptions :column="2">
|
||||
<Descriptions.Item label="产品">
|
||||
<a
|
||||
@click="goToProductDetail(product.id)"
|
||||
class="cursor-pointer text-blue-600"
|
||||
@click="goToProductDetail(product.id)"
|
||||
>
|
||||
{{ product.name }}
|
||||
</a>
|
||||
@@ -81,8 +86,8 @@ function goToProductDetail(productId: number | undefined) {
|
||||
<Descriptions.Item label="ProductKey">
|
||||
{{ product.productKey }}
|
||||
<Button
|
||||
size="small"
|
||||
class="ml-2"
|
||||
size="small"
|
||||
@click="copyToClipboard(product.productKey)"
|
||||
>
|
||||
复制
|
||||
@@ -90,8 +95,5 @@ function goToProductDetail(productId: number | undefined) {
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DeviceForm ref="formRef" @success="emit('refresh')" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,4 @@
|
||||
<!-- 设备信息 -->
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
@@ -7,7 +6,7 @@ import { computed, ref } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDate } from '@vben/utils';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -24,51 +23,46 @@ import {
|
||||
import { getDeviceAuthInfo } from '#/api/iot/device/device';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
// 消息提示
|
||||
|
||||
const { product, device } = defineProps<{
|
||||
interface Props {
|
||||
device: IotDeviceApi.Device;
|
||||
product: IotProductApi.Product;
|
||||
}>(); // 定义 Props
|
||||
// const emit = defineEmits(['refresh']); // 定义 Emits
|
||||
}
|
||||
|
||||
const authDialogVisible = ref(false); // 定义设备认证信息弹框的可见性
|
||||
const authPasswordVisible = ref(false); // 定义密码可见性状态
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const authDialogVisible = ref(false);
|
||||
const authPasswordVisible = ref(false);
|
||||
const authInfo = ref<IotDeviceApi.DeviceAuthInfo>(
|
||||
{} as IotDeviceApi.DeviceAuthInfo,
|
||||
); // 定义设备认证信息对象
|
||||
);
|
||||
|
||||
/** 控制地图显示的标志 */
|
||||
const showMap = computed(() => {
|
||||
return !!(device.longitude && device.latitude);
|
||||
return !!(props.device.longitude && props.device.latitude);
|
||||
});
|
||||
|
||||
/** 复制到剪贴板方法 */
|
||||
/** 复制到剪贴板 */
|
||||
async function copyToClipboard(text: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
message.success({ content: '复制成功' });
|
||||
message.success('复制成功');
|
||||
} catch {
|
||||
message.error({ content: '复制失败' });
|
||||
message.error('复制失败');
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开设备认证信息弹框的方法 */
|
||||
/** 打开设备认证信息弹框 */
|
||||
async function handleAuthInfoDialogOpen() {
|
||||
if (!device.id) return;
|
||||
if (!props.device.id) return;
|
||||
try {
|
||||
authInfo.value = await getDeviceAuthInfo(device.id);
|
||||
// 显示设备认证信息弹框
|
||||
authInfo.value = await getDeviceAuthInfo(props.device.id);
|
||||
authDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('获取设备认证信息出错:', error);
|
||||
message.error({
|
||||
content: '获取设备认证信息失败,请检查网络连接或联系管理员',
|
||||
});
|
||||
} catch {
|
||||
message.error('获取设备认证信息失败,请检查网络连接或联系管理员');
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭设备认证信息弹框的方法 */
|
||||
/** 关闭设备认证信息弹框 */
|
||||
function handleAuthInfoDialogClose() {
|
||||
authDialogVisible.value = false;
|
||||
}
|
||||
@@ -81,52 +75,59 @@ function handleAuthInfoDialogClose() {
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="ep:info-filled" class="mr-2 text-primary" />
|
||||
<!-- TODO @haohao:图标尽量使用中立的,这样 ep 版本呢好迁移 -->
|
||||
<IconifyIcon class="mr-2 text-primary" icon="ep:info-filled" />
|
||||
<span>设备信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<Descriptions :column="1" bordered size="small">
|
||||
<Descriptions.Item label="产品名称">
|
||||
{{ product.name }}
|
||||
{{ props.product.name }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="ProductKey">
|
||||
{{ product.productKey }}
|
||||
{{ props.product.productKey }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="设备类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="product.deviceType"
|
||||
:value="props.product.deviceType"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="定位类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_LOCATION_TYPE"
|
||||
:value="props.product.locationType"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="DeviceName">
|
||||
{{ device.deviceName }}
|
||||
{{ props.device.deviceName }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="备注名称">
|
||||
{{ device.nickname || '--' }}
|
||||
{{ props.device.nickname || '--' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="当前状态">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_DEVICE_STATE"
|
||||
:value="device.state"
|
||||
:value="props.device.state"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{{ formatDate(device.createTime) }}
|
||||
{{ formatDateTime(props.device.createTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="激活时间">
|
||||
{{ formatDate(device.activeTime) }}
|
||||
{{ formatDateTime(props.device.activeTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后上线时间">
|
||||
{{ formatDate(device.onlineTime) }}
|
||||
{{ formatDateTime(props.device.onlineTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后离线时间">
|
||||
{{ formatDate(device.offlineTime) }}
|
||||
{{ formatDateTime(props.device.offlineTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="MQTT 连接参数">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleAuthInfoDialogOpen"
|
||||
size="small"
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
@@ -141,9 +142,13 @@ function handleAuthInfoDialogClose() {
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="ep:location" class="mr-2 text-primary" />
|
||||
<!-- TODO @haohao:图标尽量使用中立的,这样 ep 版本呢好迁移 -->
|
||||
<IconifyIcon class="mr-2 text-primary" icon="ep:location" />
|
||||
<span>设备位置</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
最后上线:{{ formatDateTime(props.device.onlineTime) || '--' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-[500px] w-full">
|
||||
@@ -157,7 +162,8 @@ function handleAuthInfoDialogClose() {
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center rounded bg-gray-50 text-gray-400"
|
||||
>
|
||||
<IconifyIcon icon="ep:warning" class="mr-2" />
|
||||
<!-- TODO @haohao:图标尽量使用中立的,这样 ep 版本呢好迁移 -->
|
||||
<IconifyIcon class="mr-2" icon="ep:warning" />
|
||||
<span>暂无位置信息</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,9 +174,9 @@ function handleAuthInfoDialogClose() {
|
||||
<!-- 认证信息弹框 -->
|
||||
<Modal
|
||||
v-model:open="authDialogVisible"
|
||||
:footer="null"
|
||||
title="MQTT 连接参数"
|
||||
width="640px"
|
||||
:footer="null"
|
||||
>
|
||||
<Form :label-col="{ span: 6 }">
|
||||
<Form.Item label="clientId">
|
||||
@@ -180,7 +186,7 @@ function handleAuthInfoDialogClose() {
|
||||
readonly
|
||||
style="width: calc(100% - 80px)"
|
||||
/>
|
||||
<Button @click="copyToClipboard(authInfo.clientId)" type="primary">
|
||||
<Button type="primary" @click="copyToClipboard(authInfo.clientId)">
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</Button>
|
||||
</Input.Group>
|
||||
@@ -192,7 +198,7 @@ function handleAuthInfoDialogClose() {
|
||||
readonly
|
||||
style="width: calc(100% - 80px)"
|
||||
/>
|
||||
<Button @click="copyToClipboard(authInfo.username)" type="primary">
|
||||
<Button type="primary" @click="copyToClipboard(authInfo.username)">
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</Button>
|
||||
</Input.Group>
|
||||
@@ -201,19 +207,19 @@ function handleAuthInfoDialogClose() {
|
||||
<Input.Group compact>
|
||||
<Input
|
||||
v-model:value="authInfo.password"
|
||||
readonly
|
||||
:type="authPasswordVisible ? 'text' : 'password'"
|
||||
readonly
|
||||
style="width: calc(100% - 160px)"
|
||||
/>
|
||||
<Button
|
||||
@click="authPasswordVisible = !authPasswordVisible"
|
||||
type="primary"
|
||||
@click="authPasswordVisible = !authPasswordVisible"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="authPasswordVisible ? 'lucide:eye-off' : 'lucide:eye'"
|
||||
/>
|
||||
</Button>
|
||||
<Button @click="copyToClipboard(authInfo.password)" type="primary">
|
||||
<Button type="primary" @click="copyToClipboard(authInfo.password)">
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</Button>
|
||||
</Input.Group>
|
||||
@@ -0,0 +1,244 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Button, Select, Space, Switch, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePage } from '#/api/iot/device/device';
|
||||
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
}>();
|
||||
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
method: undefined,
|
||||
upstream: undefined,
|
||||
});
|
||||
|
||||
/** 自动刷新开关 */
|
||||
const autoRefresh = ref(false);
|
||||
/** 自动刷新定时器 */
|
||||
let autoRefreshTimer: any = null;
|
||||
|
||||
/** 消息方法选项 */
|
||||
const methodOptions = computed(() => {
|
||||
return Object.values(IotDeviceMessageMethodEnum).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.method,
|
||||
}));
|
||||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'ts',
|
||||
title: '时间',
|
||||
width: 160,
|
||||
slots: { default: 'ts' },
|
||||
},
|
||||
{
|
||||
field: 'upstream',
|
||||
title: '上行/下行',
|
||||
width: 100,
|
||||
slots: { default: 'upstream' },
|
||||
},
|
||||
{
|
||||
field: 'reply',
|
||||
title: '是否回复',
|
||||
width: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'requestId',
|
||||
title: '请求编号',
|
||||
width: 280,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{
|
||||
field: 'method',
|
||||
title: '请求方法',
|
||||
width: 120,
|
||||
slots: { default: 'method' },
|
||||
},
|
||||
{
|
||||
field: 'params',
|
||||
title: '请求/响应数据',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
slots: { default: 'params' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 创建 Grid 实例 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getDeviceMessagePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
deviceId: props.deviceId,
|
||||
method: queryParams.method,
|
||||
upstream: queryParams.upstream,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
search: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/** 搜索操作 */
|
||||
function handleQuery() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 监听自动刷新 */
|
||||
watch(autoRefresh, (newValue) => {
|
||||
if (newValue) {
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
gridApi.query();
|
||||
}, 5000);
|
||||
} else {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 监听设备标识变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleQuery();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 组件卸载时清除定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
if (props.deviceId) {
|
||||
handleQuery();
|
||||
}
|
||||
});
|
||||
|
||||
/** 刷新消息列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (delay > 0) {
|
||||
setTimeout(() => {
|
||||
gridApi.query();
|
||||
}, delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
}
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<Select
|
||||
v-model:value="queryParams.method"
|
||||
allow-clear
|
||||
placeholder="所有方法"
|
||||
style="width: 160px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="item in methodOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
v-model:value="queryParams.upstream"
|
||||
allow-clear
|
||||
placeholder="上行/下行"
|
||||
style="width: 160px"
|
||||
>
|
||||
<Select.Option label="上行" value="true">上行</Select.Option>
|
||||
<Select.Option label="下行" value="false">下行</Select.Option>
|
||||
</Select>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<IconifyIcon icon="ep:search" class="mr-5px" /> 搜索
|
||||
</Button>
|
||||
<Switch
|
||||
v-model:checked="autoRefresh"
|
||||
checked-children="定时刷新"
|
||||
un-checked-children="定时刷新"
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<Grid>
|
||||
<template #ts="{ row }">
|
||||
{{ formatDateTime(row.ts) }}
|
||||
</template>
|
||||
<template #upstream="{ row }">
|
||||
<Tag :color="row.upstream ? 'blue' : 'green'">
|
||||
{{ row.upstream ? '上行' : '下行' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #method="{ row }">
|
||||
{{ methodOptions.find((item) => item.value === row.method)?.label }}
|
||||
</template>
|
||||
<template #params="{ row }">
|
||||
<span v-if="row.reply">
|
||||
{{ `{"code":${row.code},"msg":"${row.msg}","data":${row.data}\}` }}
|
||||
</span>
|
||||
<span v-else>{{ row.params }}</span>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,618 @@
|
||||
<!-- 模拟设备 -->
|
||||
<script lang="ts" setup>
|
||||
import type { TableColumnType } from 'ant-design-vue';
|
||||
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { DeviceStateEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
message,
|
||||
Row,
|
||||
Table,
|
||||
Tabs,
|
||||
Textarea,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { sendDeviceMessage } from '#/api/iot/device/device';
|
||||
import {
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import DataDefinition from '../../../../thingmodel/modules/components/data-definition.vue';
|
||||
import DeviceDetailsMessage from './message.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
device: IotDeviceApi.Device;
|
||||
product: IotProductApi.Product;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
// 消息弹窗
|
||||
const activeTab = ref('upstream'); // 上行upstream、下行downstream
|
||||
const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method); // 上行子标签
|
||||
const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method); // 下行子标签
|
||||
const deviceMessageRef = ref(); // 设备消息组件引用
|
||||
const deviceMessageRefreshDelay = 2000; // 延迟 N 秒,保证模拟上行的消息被处理
|
||||
|
||||
// 折叠状态
|
||||
const debugCollapsed = ref(false); // 指令调试区域折叠状态
|
||||
const messageCollapsed = ref(false); // 设备消息区域折叠状态
|
||||
|
||||
// 表单数据:存储用户输入的模拟值
|
||||
const formData = ref<Record<string, string>>({});
|
||||
|
||||
// 根据类型过滤物模型数据
|
||||
const getFilteredThingModelList = (type: number) => {
|
||||
return props.thingModelList.filter(
|
||||
(item) => String(item.type) === String(type),
|
||||
);
|
||||
};
|
||||
|
||||
// 计算属性:属性列表
|
||||
const propertyList = computed(() =>
|
||||
getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY),
|
||||
);
|
||||
|
||||
// 计算属性:事件列表
|
||||
const eventList = computed(() =>
|
||||
getFilteredThingModelList(IoTThingModelTypeEnum.EVENT),
|
||||
);
|
||||
|
||||
// 计算属性:服务列表
|
||||
const serviceList = computed(() =>
|
||||
getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE),
|
||||
);
|
||||
|
||||
// 属性表格列定义
|
||||
const propertyColumns: TableColumnType[] = [
|
||||
{
|
||||
title: '功能名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
key: 'dataType',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '数据定义',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
key: 'value',
|
||||
width: 180,
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 事件表格列定义
|
||||
const eventColumns = [
|
||||
{
|
||||
title: '功能名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
key: 'dataType',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '数据定义',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
key: 'value',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 服务表格列定义
|
||||
const serviceColumns = [
|
||||
{
|
||||
title: '服务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '输入参数',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '参数值',
|
||||
key: 'value',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 获取表单值
|
||||
function getFormValue(identifier: string) {
|
||||
return formData.value[identifier] || '';
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
function setFormValue(identifier: string, value: string) {
|
||||
formData.value[identifier] = value;
|
||||
}
|
||||
|
||||
// 属性上报
|
||||
async function handlePropertyPost() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
propertyList.value.forEach((item) => {
|
||||
const value = formData.value[item.identifier!];
|
||||
if (value) {
|
||||
params[item.identifier!] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
message.warning({ content: '请至少输入一个属性值' });
|
||||
return;
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
|
||||
params,
|
||||
});
|
||||
|
||||
message.success({ content: '属性上报成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '属性上报失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 事件上报
|
||||
async function handleEventPost(row: ThingModelData) {
|
||||
try {
|
||||
const valueStr = formData.value[row.identifier!];
|
||||
let params: any = {};
|
||||
|
||||
if (valueStr) {
|
||||
try {
|
||||
params = JSON.parse(valueStr);
|
||||
} catch {
|
||||
message.error({ content: '事件参数格式错误,请输入有效的JSON格式' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method,
|
||||
params: {
|
||||
identifier: row.identifier,
|
||||
params,
|
||||
},
|
||||
});
|
||||
|
||||
message.success({ content: '事件上报成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '事件上报失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态变更
|
||||
async function handleDeviceState(state: number) {
|
||||
try {
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
|
||||
params: { state },
|
||||
});
|
||||
|
||||
message.success({ content: '状态变更成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '状态变更失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 属性设置
|
||||
async function handlePropertySet() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
propertyList.value.forEach((item) => {
|
||||
const value = formData.value[item.identifier!];
|
||||
if (value) {
|
||||
params[item.identifier!] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
message.warning({ content: '请至少输入一个属性值' });
|
||||
return;
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
|
||||
params,
|
||||
});
|
||||
|
||||
message.success({ content: '属性设置成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '属性设置失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 服务调用
|
||||
async function handleServiceInvoke(row: ThingModelData) {
|
||||
try {
|
||||
const valueStr = formData.value[row.identifier!];
|
||||
let params: any = {};
|
||||
|
||||
if (valueStr) {
|
||||
try {
|
||||
params = JSON.parse(valueStr);
|
||||
} catch {
|
||||
message.error({ content: '服务参数格式错误,请输入有效的JSON格式' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
|
||||
params: {
|
||||
identifier: row.identifier,
|
||||
params,
|
||||
},
|
||||
});
|
||||
|
||||
message.success({ content: '服务调用成功' });
|
||||
// 延迟刷新设备消息列表
|
||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||
} catch (error) {
|
||||
message.error({ content: '服务调用失败' });
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<Row :gutter="16">
|
||||
<!-- 左侧:指令调试区域 -->
|
||||
<Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24">
|
||||
<Card class="simulator-tabs h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>指令调试</span>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="debugCollapsed = !debugCollapsed"
|
||||
>
|
||||
<IconifyIcon v-if="!debugCollapsed" icon="lucide:chevron-up" />
|
||||
<IconifyIcon v-if="debugCollapsed" icon="lucide:chevron-down" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-show="!debugCollapsed">
|
||||
<Tabs v-model:active-key="activeTab" size="small">
|
||||
<!-- 上行指令调试 -->
|
||||
<Tabs.TabPane key="upstream" tab="上行指令调试">
|
||||
<Tabs
|
||||
v-if="activeTab === 'upstream'"
|
||||
v-model:active-key="upstreamTab"
|
||||
size="small"
|
||||
>
|
||||
<!-- 属性上报 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
|
||||
tab="属性上报"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:columns="propertyColumns"
|
||||
:data-source="propertyList"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
align="center"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ record.property?.dataType ?? '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">
|
||||
设置属性值后,点击「发送属性上报」按钮
|
||||
</span>
|
||||
<Button type="primary" @click="handlePropertyPost">
|
||||
发送属性上报
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 事件上报 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.EVENT_POST.method"
|
||||
tab="事件上报"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:columns="eventColumns"
|
||||
:data-source="eventList"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
align="center"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ record.event?.dataType ?? '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Textarea
|
||||
:rows="3"
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入事件参数(JSON格式)"
|
||||
size="small"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleEventPost(record)"
|
||||
>
|
||||
上报事件
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 状态变更 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
|
||||
tab="状态变更"
|
||||
>
|
||||
<ContentWrap>
|
||||
<div class="flex gap-4">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleDeviceState(DeviceStateEnum.ONLINE)"
|
||||
>
|
||||
设备上线
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
@click="handleDeviceState(DeviceStateEnum.OFFLINE)"
|
||||
>
|
||||
设备下线
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 下行指令调试 -->
|
||||
<Tabs.TabPane key="downstream" tab="下行指令调试">
|
||||
<Tabs
|
||||
v-if="activeTab === 'downstream'"
|
||||
v-model:active-key="downstreamTab"
|
||||
size="small"
|
||||
>
|
||||
<!-- 属性调试 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
|
||||
tab="属性设置"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:columns="propertyColumns"
|
||||
:data-source="propertyList"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
align="center"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataType'">
|
||||
{{ record.property?.dataType ?? '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">
|
||||
设置属性值后,点击「发送属性设置」按钮
|
||||
</span>
|
||||
<Button type="primary" @click="handlePropertySet">
|
||||
发送属性设置
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<!-- 服务调用 -->
|
||||
<Tabs.TabPane
|
||||
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
|
||||
tab="设备服务调用"
|
||||
>
|
||||
<ContentWrap>
|
||||
<Table
|
||||
:columns="serviceColumns"
|
||||
:data-source="serviceList"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content', y: 300 }"
|
||||
align="center"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'dataDefinition'">
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<Textarea
|
||||
:rows="3"
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入服务参数(JSON格式)"
|
||||
size="small"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleServiceInvoke(record)"
|
||||
>
|
||||
服务调用
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</ContentWrap>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧:设备消息区域 -->
|
||||
<Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>设备消息</span>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="messageCollapsed = !messageCollapsed"
|
||||
>
|
||||
<IconifyIcon
|
||||
v-if="!messageCollapsed"
|
||||
icon="lucide:chevron-down"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-if="messageCollapsed"
|
||||
icon="lucide:chevron-down"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-show="!messageCollapsed">
|
||||
<DeviceDetailsMessage
|
||||
v-if="device.id"
|
||||
ref="deviceMessageRef"
|
||||
:device-id="device.id"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
@@ -3,6 +3,8 @@ import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Card, Empty } from 'ant-design-vue';
|
||||
|
||||
// TODO @haohao:这里要实现一把么?
|
||||
|
||||
interface Props {
|
||||
deviceId: number;
|
||||
}
|
||||
@@ -35,8 +37,8 @@ onMounted(() => {
|
||||
<template>
|
||||
<Card :loading="loading" title="子设备管理">
|
||||
<Empty
|
||||
description="暂无子设备数据,此功能待实现"
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无子设备数据,此功能待实现"
|
||||
/>
|
||||
<!-- TODO: 实现子设备列表展示和管理功能 -->
|
||||
</Card>
|
||||
@@ -0,0 +1,256 @@
|
||||
<!-- 设备事件管理 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Button, RangePicker, Select, Space, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
identifier: '',
|
||||
times: undefined as [string, string] | undefined,
|
||||
});
|
||||
|
||||
/** 事件类型的物模型数据 */
|
||||
const eventThingModels = computed(() => {
|
||||
return props.thingModelList.filter(
|
||||
(item: ThingModelData) =>
|
||||
String(item.type) === String(IoTThingModelTypeEnum.EVENT),
|
||||
);
|
||||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'reportTime',
|
||||
title: '上报时间',
|
||||
width: 180,
|
||||
slots: { default: 'reportTime' },
|
||||
},
|
||||
{
|
||||
field: 'identifier',
|
||||
title: '标识符',
|
||||
width: 160,
|
||||
slots: { default: 'identifier' },
|
||||
},
|
||||
{
|
||||
field: 'eventName',
|
||||
title: '事件名称',
|
||||
width: 160,
|
||||
slots: { default: 'eventName' },
|
||||
},
|
||||
{
|
||||
field: 'eventType',
|
||||
title: '事件类型',
|
||||
width: 100,
|
||||
slots: { default: 'eventType' },
|
||||
},
|
||||
{
|
||||
field: 'params',
|
||||
title: '输入参数',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
slots: { default: 'params' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 创建 Grid 实例 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getDeviceMessagePairPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
deviceId: props.deviceId,
|
||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method,
|
||||
identifier: queryParams.identifier || undefined,
|
||||
times: queryParams.times,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
search: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryParams.identifier = '';
|
||||
queryParams.times = undefined;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 获取事件名称 */
|
||||
function getEventName(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const event = eventThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
return event?.name || identifier;
|
||||
}
|
||||
|
||||
/** 获取事件类型 */
|
||||
function getEventType(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const event = eventThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
if (!event?.event?.type) return '-';
|
||||
return getEventTypeLabel(event.event.type) || '-';
|
||||
}
|
||||
|
||||
/** 解析参数 */
|
||||
function parseParams(params: string) {
|
||||
try {
|
||||
const parsed = JSON.parse(params);
|
||||
if (parsed.params) {
|
||||
return parsed.params;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (delay > 0) {
|
||||
setTimeout(() => gridApi.query(), delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听设备标识变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleQuery();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
if (props.deviceId) {
|
||||
handleQuery();
|
||||
}
|
||||
});
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>标识符:</span>
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
allow-clear
|
||||
placeholder="请选择事件标识符"
|
||||
style="width: 240px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="event in eventThingModels"
|
||||
:key="event.identifier"
|
||||
:value="event.identifier!"
|
||||
>
|
||||
{{ event.name }}({{ event.identifier }})
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>时间范围:</span>
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
show-time
|
||||
style="width: 360px"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</div>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:search" />
|
||||
</template>
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="resetQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 事件列表 -->
|
||||
<Grid>
|
||||
<template #reportTime="{ row }">
|
||||
{{
|
||||
row.request?.reportTime ? formatDateTime(row.request.reportTime) : '-'
|
||||
}}
|
||||
</template>
|
||||
<template #identifier="{ row }">
|
||||
<Tag color="blue" size="small">
|
||||
{{ row.request?.identifier }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #eventName="{ row }">
|
||||
{{ getEventName(row.request?.identifier) }}
|
||||
</template>
|
||||
<template #eventType="{ row }">
|
||||
{{ getEventType(row.request?.identifier) }}
|
||||
</template>
|
||||
<template #params="{ row }">
|
||||
{{ parseParams(row.request?.params) }}
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -1,6 +1,5 @@
|
||||
<!-- 设备物模型 -> 运行状态 -> 查看数据(设备的属性值历史)-->
|
||||
// 重新关闭打开图表,图表不显示可能图例注销失败等大佬修复
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
@@ -11,14 +10,13 @@ import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { beginOfDay, endOfDay, formatDate, formatDateTime } from '@vben/utils';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Empty,
|
||||
message,
|
||||
Modal,
|
||||
RangePicker,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
@@ -27,6 +25,7 @@ import {
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
|
||||
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
/** IoT 设备属性历史数据详情 */
|
||||
@@ -42,52 +41,70 @@ const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
||||
const total = ref(0); // 总数据量
|
||||
const thingModelDataType = ref<string>(''); // 物模型数据类型
|
||||
const propertyIdentifier = ref<string>(''); // 属性标识符
|
||||
const dateRange = ref<[Dayjs, Dayjs]>([
|
||||
dayjs().subtract(7, 'day').startOf('day'),
|
||||
dayjs().endOf('day'),
|
||||
]);
|
||||
|
||||
const dateRange = ref<[string, string]>([
|
||||
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
||||
dayjs().format('YYYY-MM-DD'),
|
||||
]); // 时间范围(仅日期,不包含时分秒)
|
||||
|
||||
const queryParams = reactive({
|
||||
deviceId: -1,
|
||||
identifier: '',
|
||||
times: [
|
||||
formatDateTime(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||
formatDateTime(endOfDay(new Date())),
|
||||
],
|
||||
times: formatDateRangeWithTime(dateRange.value),
|
||||
});
|
||||
|
||||
// Echarts 相关
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
// 判断是否为复杂数据类型(struct 或 array)
|
||||
const isComplexDataType = computed(() => {
|
||||
/** 不支持图表展示的数据类型列表 */
|
||||
const CHART_DISABLED_DATA_TYPES = [
|
||||
IoTDataSpecsDataTypeEnum.ARRAY, // 数组
|
||||
IoTDataSpecsDataTypeEnum.STRUCT, // 结构体
|
||||
IoTDataSpecsDataTypeEnum.TEXT, // 文本型
|
||||
IoTDataSpecsDataTypeEnum.BOOL, // 布尔型
|
||||
IoTDataSpecsDataTypeEnum.ENUM, // 枚举型
|
||||
IoTDataSpecsDataTypeEnum.DATE, // 时间型
|
||||
] as const;
|
||||
|
||||
/** 判断是否支持图表展示(仅数值类型支持:int、float、double) */
|
||||
const canShowChart = computed(() => {
|
||||
if (!thingModelDataType.value) return false;
|
||||
return [
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
].includes(thingModelDataType.value as any);
|
||||
return !CHART_DISABLED_DATA_TYPES.includes(
|
||||
thingModelDataType.value as (typeof CHART_DISABLED_DATA_TYPES)[number],
|
||||
);
|
||||
});
|
||||
|
||||
// 统计数据
|
||||
/** 判断是否为复杂数据类型(用于格式化显示) */
|
||||
const isComplexDataType = computed(() => {
|
||||
if (!thingModelDataType.value) return false;
|
||||
return (
|
||||
thingModelDataType.value === IoTDataSpecsDataTypeEnum.ARRAY ||
|
||||
thingModelDataType.value === IoTDataSpecsDataTypeEnum.STRUCT
|
||||
);
|
||||
});
|
||||
|
||||
/** 最大值统计 */
|
||||
const maxValue = computed(() => {
|
||||
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||
if (!canShowChart.value || list.value.length === 0) return '-';
|
||||
const values = list.value
|
||||
.map((item) => Number(item.value))
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
return values.length > 0 ? Math.max(...values).toFixed(2) : '-';
|
||||
});
|
||||
|
||||
/** 最小值统计 */
|
||||
const minValue = computed(() => {
|
||||
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||
if (!canShowChart.value || list.value.length === 0) return '-';
|
||||
const values = list.value
|
||||
.map((item) => Number(item.value))
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
return values.length > 0 ? Math.min(...values).toFixed(2) : '-';
|
||||
});
|
||||
|
||||
/** 平均值统计 */
|
||||
const avgValue = computed(() => {
|
||||
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||
if (!canShowChart.value || list.value.length === 0) return '-';
|
||||
const values = list.value
|
||||
.map((item) => Number(item.value))
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
@@ -96,7 +113,11 @@ const avgValue = computed(() => {
|
||||
return (sum / values.length).toFixed(2);
|
||||
});
|
||||
|
||||
// 表格列配置
|
||||
/** 将日期范围转换为带时分秒的格式 */
|
||||
function formatDateRangeWithTime(dates: [string, string]): [string, string] {
|
||||
return [`${dates[0]} 00:00:00`, `${dates[1]} 23:59:59`];
|
||||
}
|
||||
|
||||
const tableColumns = computed(() => [
|
||||
{
|
||||
title: '序号',
|
||||
@@ -118,9 +139,8 @@ const tableColumns = computed(() => [
|
||||
dataIndex: 'value',
|
||||
align: 'center' as const,
|
||||
},
|
||||
]);
|
||||
]); // 表格列配置
|
||||
|
||||
// 分页配置
|
||||
const paginationConfig = computed(() => ({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
@@ -129,7 +149,7 @@ const paginationConfig = computed(() => ({
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
showTotal: (total: number) => `共 ${total} 条数据`,
|
||||
}));
|
||||
})); // 分页配置
|
||||
|
||||
/** 获得设备历史数据 */
|
||||
async function getList() {
|
||||
@@ -142,16 +162,13 @@ async function getList() {
|
||||
) as IotDeviceApi.DevicePropertyDetail[];
|
||||
total.value = list.value.length;
|
||||
|
||||
// 如果是图表模式且不是复杂数据类型,渲染图表
|
||||
// 如果是图表模式且支持图表展示,等待渲染图表
|
||||
if (
|
||||
viewMode.value === 'chart' &&
|
||||
!isComplexDataType.value &&
|
||||
canShowChart.value &&
|
||||
list.value.length > 0
|
||||
) {
|
||||
// 等待 DOM 更新完成后再渲染图表
|
||||
await nextTick();
|
||||
await nextTick(); // 双重 nextTick 确保 DOM 完全准备好
|
||||
renderChart();
|
||||
await renderChartWhenReady();
|
||||
}
|
||||
} catch {
|
||||
message.error('获取数据失败');
|
||||
@@ -162,126 +179,115 @@ async function getList() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 确保图表容器已经可见后再渲染 */
|
||||
async function renderChartWhenReady() {
|
||||
if (!list.value || list.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
// 等待 Modal、Card loading 状态、v-show 等 DOM 更新完成
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
renderChart();
|
||||
}
|
||||
|
||||
/** 渲染图表 */
|
||||
function renderChart() {
|
||||
if (!list.value || list.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chartData = list.value.map((item) => [item.updateTime, item.value]);
|
||||
const times = list.value.map((item) =>
|
||||
formatDate(new Date(item.updateTime), 'YYYY-MM-DD HH:mm:ss'),
|
||||
);
|
||||
const values = list.value.map((item) => Number(item.value));
|
||||
|
||||
// 使用 setTimeout 延迟渲染,避免 ECharts 主进程冲突
|
||||
setTimeout(() => {
|
||||
// 检查 chartRef 是否存在且已挂载
|
||||
if (!chartRef.value || !chartRef.value.$el) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderEcharts({
|
||||
title: {
|
||||
text: '属性值趋势',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
renderEcharts({
|
||||
title: {
|
||||
text: '属性值趋势',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 60,
|
||||
bottom: 100,
|
||||
top: 80,
|
||||
containLabel: true,
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 60,
|
||||
bottom: 100,
|
||||
top: 80,
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const param = params[0];
|
||||
return `
|
||||
<div style="padding: 8px;">
|
||||
<div style="margin-bottom: 4px; font-weight: bold;">
|
||||
${formatDate(new Date(param.value[0]), 'YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
<div>
|
||||
<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;background-color:${param.color};"></span>
|
||||
<span>属性值: <strong>${param.value[1]}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
name: '时间',
|
||||
nameTextStyle: {
|
||||
padding: [10, 0, 0, 0],
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
name: '时间',
|
||||
nameTextStyle: {
|
||||
padding: [10, 0, 0, 0],
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: (value: number) => {
|
||||
return String(formatDate(new Date(value), 'MM-DD HH:mm') || '');
|
||||
},
|
||||
},
|
||||
data: times,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '属性值',
|
||||
nameTextStyle: {
|
||||
padding: [0, 0, 10, 0],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '属性值',
|
||||
nameTextStyle: {
|
||||
padding: [0, 0, 10, 0],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#1890FF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#1890FF',
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(24, 144, 255, 0.3)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(24, 144, 255, 0.05)',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
data: values,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '属性值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#1890FF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#1890FF',
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(24, 144, 255, 0.3)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(24, 144, 255, 0.05)',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
data: chartData,
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
height: 30,
|
||||
bottom: 20,
|
||||
},
|
||||
],
|
||||
});
|
||||
}, 300); // 延迟300ms渲染,确保 DOM 完全准备好
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
height: 30,
|
||||
bottom: 20,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 打开弹窗 */
|
||||
@@ -294,42 +300,33 @@ async function open(deviceId: number, identifier: string, dataType: string) {
|
||||
|
||||
// 重置时间范围为最近7天
|
||||
dateRange.value = [
|
||||
dayjs().subtract(7, 'day').startOf('day'),
|
||||
dayjs().endOf('day'),
|
||||
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
||||
dayjs().format('YYYY-MM-DD'),
|
||||
];
|
||||
|
||||
// 更新查询参数的时间
|
||||
queryParams.times = [
|
||||
formatDateTime(dateRange.value[0].toDate()),
|
||||
formatDateTime(dateRange.value[1].toDate()),
|
||||
];
|
||||
queryParams.times = formatDateRangeWithTime(dateRange.value);
|
||||
|
||||
// 如果物模型是 struct、array,需要默认使用 list 模式
|
||||
viewMode.value = isComplexDataType.value ? 'list' : 'chart';
|
||||
// 如果不支持图表展示,默认使用列表模式
|
||||
viewMode.value = canShowChart.value ? 'chart' : 'list';
|
||||
|
||||
// 等待弹窗完全渲染后再获取数据
|
||||
await nextTick();
|
||||
await nextTick(); // 双重 nextTick 确保 Modal 完全渲染
|
||||
await getList();
|
||||
|
||||
// 如果是图表模式,延迟渲染图表
|
||||
if (viewMode.value === 'chart' && !isComplexDataType.value) {
|
||||
setTimeout(() => {
|
||||
renderChart();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/** 时间变化处理 */
|
||||
function handleTimeChange() {
|
||||
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||
/** 处理时间范围变化 */
|
||||
function handleDateRangeChange(times?: [Dayjs, Dayjs]) {
|
||||
if (!times || times.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
queryParams.times = [
|
||||
formatDateTime(dateRange.value[0].toDate()),
|
||||
formatDateTime(dateRange.value[1].toDate()),
|
||||
dateRange.value = [
|
||||
dayjs(times[0]).format('YYYY-MM-DD'),
|
||||
dayjs(times[1]).format('YYYY-MM-DD'),
|
||||
];
|
||||
|
||||
// 将选择的日期转换为带时分秒的格式(开始日期 00:00:00,结束日期 23:59:59)
|
||||
queryParams.times = formatDateRangeWithTime(dateRange.value);
|
||||
getList();
|
||||
}
|
||||
|
||||
@@ -403,19 +400,8 @@ function formatComplexValue(value: any) {
|
||||
|
||||
/** 监听视图模式变化,重新渲染图表 */
|
||||
watch(viewMode, async (newMode) => {
|
||||
if (
|
||||
newMode === 'chart' &&
|
||||
!isComplexDataType.value &&
|
||||
list.value.length > 0
|
||||
) {
|
||||
// 等待 DOM 显示完成
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
// 延迟渲染图表
|
||||
setTimeout(() => {
|
||||
renderChart();
|
||||
}, 300);
|
||||
if (newMode === 'chart' && canShowChart.value && list.value.length > 0) {
|
||||
await renderChartWhenReady();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -426,7 +412,6 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
v-model:open="dialogVisible"
|
||||
title="查看数据"
|
||||
width="1200px"
|
||||
:destroy-on-close="true"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<div class="property-history-container">
|
||||
@@ -434,17 +419,15 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
<div class="toolbar-wrapper mb-4">
|
||||
<Space :size="12" class="w-full" wrap>
|
||||
<!-- 时间选择 -->
|
||||
<RangePicker
|
||||
v-model:value="dateRange"
|
||||
:show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
:placeholder="['开始时间', '结束时间']"
|
||||
class="!w-[400px]"
|
||||
@change="handleTimeChange"
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="whitespace-nowrap text-sm text-gray-500">
|
||||
时间范围
|
||||
</span>
|
||||
<ShortcutDateRangePicker @change="handleDateRangeChange" />
|
||||
</div>
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<Button @click="handleRefresh" :loading="loading">
|
||||
<Button :loading="loading" @click="handleRefresh">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ant-design:reload-outlined" />
|
||||
</template>
|
||||
@@ -453,9 +436,9 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
|
||||
<!-- 导出按钮 -->
|
||||
<Button
|
||||
@click="handleExport"
|
||||
:loading="exporting"
|
||||
:disabled="list.length === 0"
|
||||
:loading="exporting"
|
||||
@click="handleExport"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ant-design:export-outlined" />
|
||||
@@ -466,9 +449,9 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
<!-- 视图切换 -->
|
||||
<Button.Group class="ml-auto">
|
||||
<Button
|
||||
:disabled="!canShowChart"
|
||||
:type="viewMode === 'chart' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'chart'"
|
||||
:disabled="isComplexDataType"
|
||||
>
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ant-design:line-chart-outlined" />
|
||||
@@ -491,7 +474,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
<div v-if="list.length > 0" class="mt-3 text-sm text-gray-600">
|
||||
<Space :size="16">
|
||||
<span>共 {{ total }} 条数据</span>
|
||||
<span v-if="viewMode === 'chart' && !isComplexDataType">
|
||||
<span v-if="viewMode === 'chart' && canShowChart">
|
||||
最大值: {{ maxValue }} | 最小值: {{ minValue }} | 平均值:
|
||||
{{ avgValue }}
|
||||
</span>
|
||||
@@ -500,16 +483,16 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
</div>
|
||||
|
||||
<!-- 数据展示区域 -->
|
||||
<Spin :spinning="loading" :delay="200">
|
||||
<!-- 图表模式 -->
|
||||
<Spin :delay="200" :spinning="loading">
|
||||
<!-- 图表模式 - 使用 v-show 确保图表组件始终挂载 -->
|
||||
<div v-show="viewMode === 'chart'" class="chart-container">
|
||||
<Empty
|
||||
v-if="list.length === 0"
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无数据"
|
||||
class="py-20"
|
||||
:description="$t('common.noData')"
|
||||
/>
|
||||
<div v-else>
|
||||
<div v-show="list.length > 0">
|
||||
<EchartsUI ref="chartRef" height="500px" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -517,8 +500,8 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
<!-- 表格模式 -->
|
||||
<div v-show="viewMode === 'list'" class="table-container">
|
||||
<Table
|
||||
:data-source="list"
|
||||
:columns="tableColumns"
|
||||
:data-source="list"
|
||||
:pagination="paginationConfig"
|
||||
:scroll="{ y: 500 }"
|
||||
row-key="updateTime"
|
||||
@@ -546,7 +529,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.property-history-container {
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
@@ -561,7 +544,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
.chart-container,
|
||||
.table-container {
|
||||
padding: 16px;
|
||||
background-color: hsl(var(--card));
|
||||
background-color: hsl(var(--card) / 100%);
|
||||
border: 1px solid hsl(var(--border) / 60%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
<!-- 设备属性管理 -->
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import {
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDate } from '@vben/utils';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -16,13 +24,13 @@ import {
|
||||
Input,
|
||||
Row,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getLatestDeviceProperties } from '#/api/iot/device/device';
|
||||
|
||||
import DeviceDetailsThingModelPropertyHistory from './device-details-thing-model-property-history.vue';
|
||||
import DeviceDetailsThingModelPropertyHistory from './thing-model-property-history.vue';
|
||||
|
||||
const props = defineProps<{ deviceId: number }>();
|
||||
|
||||
@@ -31,22 +39,146 @@ const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 显示的列表数
|
||||
const filterList = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 完整的数据列表
|
||||
const queryParams = reactive({
|
||||
keyword: '' as string,
|
||||
});
|
||||
}); // 查询参数
|
||||
const autoRefresh = ref(false); // 自动刷新开关
|
||||
let autoRefreshTimer: any = null; // 定时器
|
||||
const viewMode = ref<'card' | 'list'>('card'); // 视图模式状态
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'identifier',
|
||||
title: '属性标识符',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '属性名称',
|
||||
},
|
||||
{
|
||||
field: 'dataType',
|
||||
title: '数据类型',
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
title: '属性值',
|
||||
slots: { default: 'value' },
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
title: '更新时间',
|
||||
width: 180,
|
||||
slots: { default: 'updateTime' },
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 创建 Grid 实例 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
rowConfig: {
|
||||
keyField: 'identifier',
|
||||
isHover: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
const data = await getLatestDeviceProperties({
|
||||
deviceId: props.deviceId,
|
||||
identifier: undefined,
|
||||
name: undefined,
|
||||
});
|
||||
// 筛选数据
|
||||
let filteredData = data;
|
||||
if (queryParams.keyword.trim()) {
|
||||
const keyword = queryParams.keyword.toLowerCase();
|
||||
filteredData = data.filter(
|
||||
(item: IotDeviceApi.DevicePropertyDetail) =>
|
||||
item.identifier?.toLowerCase().includes(keyword) ||
|
||||
item.name?.toLowerCase().includes(keyword),
|
||||
);
|
||||
}
|
||||
// 更新本地列表用于卡片视图
|
||||
filterList.value = data;
|
||||
list.value = filteredData;
|
||||
return {
|
||||
list: filteredData,
|
||||
total: filteredData.length,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
search: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<IotDeviceApi.DevicePropertyDetail>,
|
||||
});
|
||||
|
||||
// 包装 gridApi.query() 方法,统一列表视图和卡片视图的查询接口
|
||||
gridApi.query = async () => {
|
||||
if (viewMode.value === 'list') {
|
||||
// 列表视图:手动获取数据并加载到 Grid
|
||||
if (!props.deviceId) {
|
||||
return;
|
||||
}
|
||||
const data = await getLatestDeviceProperties({
|
||||
deviceId: props.deviceId,
|
||||
identifier: undefined,
|
||||
name: undefined,
|
||||
});
|
||||
const dataArray = Array.isArray(data) ? data : [];
|
||||
let filteredData = dataArray;
|
||||
if (queryParams.keyword.trim()) {
|
||||
const keyword = queryParams.keyword.toLowerCase();
|
||||
filteredData = dataArray.filter(
|
||||
(item: IotDeviceApi.DevicePropertyDetail) =>
|
||||
item.identifier?.toLowerCase().includes(keyword) ||
|
||||
item.name?.toLowerCase().includes(keyword),
|
||||
);
|
||||
}
|
||||
filterList.value = dataArray;
|
||||
list.value = filteredData;
|
||||
// 直接加载数据到 Grid
|
||||
if (gridApi.grid) {
|
||||
gridApi.grid.loadData(filteredData);
|
||||
}
|
||||
} else {
|
||||
// 卡片视图:调用 getList 方法
|
||||
await getList();
|
||||
}
|
||||
};
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
deviceId: props.deviceId,
|
||||
identifier: undefined as string | undefined,
|
||||
name: undefined as string | undefined,
|
||||
};
|
||||
filterList.value = await getLatestDeviceProperties(params);
|
||||
handleFilter();
|
||||
if (viewMode.value === 'list') {
|
||||
await gridApi.query();
|
||||
} else {
|
||||
// 卡片视图:手动获取数据
|
||||
const params = {
|
||||
deviceId: props.deviceId,
|
||||
identifier: undefined as string | undefined,
|
||||
name: undefined as string | undefined,
|
||||
};
|
||||
filterList.value = await getLatestDeviceProperties(params);
|
||||
handleFilter();
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -68,7 +200,21 @@ function handleFilter() {
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
handleFilter();
|
||||
if (viewMode.value === 'list') {
|
||||
gridApi.query();
|
||||
} else {
|
||||
handleFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 视图切换 */
|
||||
async function handleViewModeChange(mode: 'card' | 'list') {
|
||||
if (viewMode.value === mode) {
|
||||
return;
|
||||
}
|
||||
viewMode.value = mode;
|
||||
await nextTick();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 历史操作 */
|
||||
@@ -90,56 +236,70 @@ function formatValueWithUnit(item: IotDeviceApi.DevicePropertyDetail) {
|
||||
watch(autoRefresh, (newValue) => {
|
||||
if (newValue) {
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
getList();
|
||||
}, 5000); // 每 5 秒刷新一次
|
||||
gridApi.query();
|
||||
}, 5000);
|
||||
} else {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 监听设备标识变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
gridApi.query();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
if (props.deviceId) {
|
||||
await nextTick();
|
||||
gridApi.query();
|
||||
}
|
||||
});
|
||||
|
||||
/** 组件卸载时清除定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索工作栏 -->
|
||||
<div class="flex items-center justify-between" style="margin-bottom: 16px">
|
||||
<div class="flex items-center" style="gap: 16px">
|
||||
<Input
|
||||
v-model:value="queryParams.keyword"
|
||||
placeholder="请输入属性名称、标识符"
|
||||
allow-clear
|
||||
placeholder="请输入属性名称、标识符"
|
||||
style="width: 240px"
|
||||
@press-enter="handleQuery"
|
||||
/>
|
||||
<Switch
|
||||
v-model:checked="autoRefresh"
|
||||
class="ml-20px"
|
||||
checked-children="定时刷新"
|
||||
class="ml-20px"
|
||||
un-checked-children="定时刷新"
|
||||
/>
|
||||
</div>
|
||||
<Button.Group>
|
||||
<Button
|
||||
:type="viewMode === 'card' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'card'"
|
||||
@click="handleViewModeChange('card')"
|
||||
>
|
||||
<IconifyIcon icon="ep:grid" />
|
||||
</Button>
|
||||
<Button
|
||||
:type="viewMode === 'list' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'list'"
|
||||
@click="handleViewModeChange('list')"
|
||||
>
|
||||
<IconifyIcon icon="ep:list" />
|
||||
</Button>
|
||||
@@ -151,19 +311,19 @@ onMounted(() => {
|
||||
|
||||
<!-- 卡片视图 -->
|
||||
<template v-if="viewMode === 'card'">
|
||||
<Row :gutter="16" v-loading="loading">
|
||||
<Row v-loading="loading" :gutter="16">
|
||||
<Col
|
||||
v-for="item in list"
|
||||
:key="item.identifier"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="6"
|
||||
:md="12"
|
||||
:sm="12"
|
||||
:xs="24"
|
||||
class="mb-4"
|
||||
>
|
||||
<Card
|
||||
class="relative h-full overflow-hidden transition-colors"
|
||||
:body-style="{ padding: '0' }"
|
||||
class="relative h-full overflow-hidden transition-colors"
|
||||
>
|
||||
<!-- 添加渐变背景层 -->
|
||||
<div
|
||||
@@ -173,12 +333,12 @@ onMounted(() => {
|
||||
<!-- 标题区域 -->
|
||||
<div class="mb-3 flex items-center">
|
||||
<div class="mr-2.5 flex items-center">
|
||||
<IconifyIcon icon="ep:cpu" class="text-lg text-primary" />
|
||||
<IconifyIcon class="text-lg text-primary" icon="ep:cpu" />
|
||||
</div>
|
||||
<div class="flex-1 text-base font-bold">{{ item.name }}</div>
|
||||
<!-- 标识符 -->
|
||||
<div class="mr-2 inline-flex items-center">
|
||||
<Tag size="small" color="blue">
|
||||
<Tag color="blue" size="small">
|
||||
{{ item.identifier }}
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -196,8 +356,8 @@ onMounted(() => {
|
||||
"
|
||||
>
|
||||
<IconifyIcon
|
||||
icon="ep:data-line"
|
||||
class="text-lg text-primary"
|
||||
icon="ep:data-line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,7 +373,9 @@ onMounted(() => {
|
||||
<div class="mb-2.5 last:mb-0">
|
||||
<span class="mr-2.5 text-muted-foreground">更新时间</span>
|
||||
<span class="text-sm text-foreground">
|
||||
{{ item.updateTime ? formatDate(item.updateTime) : '-' }}
|
||||
{{
|
||||
item.updateTime ? formatDateTime(item.updateTime) : '-'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,45 +386,29 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<!-- 列表视图 -->
|
||||
<Table v-else v-loading="loading" :data-source="list" :pagination="false">
|
||||
<Table.Column title="属性标识符" align="center" data-index="identifier" />
|
||||
<Table.Column title="属性名称" align="center" data-index="name" />
|
||||
<Table.Column title="数据类型" align="center" data-index="dataType" />
|
||||
<Table.Column title="属性值" align="center" data-index="value">
|
||||
<template #default="{ record }">
|
||||
{{ formatValueWithUnit(record) }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="更新时间"
|
||||
align="center"
|
||||
data-index="updateTime"
|
||||
:width="180"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
{{ record.updateTime ? formatDate(record.updateTime) : '-' }}
|
||||
</template>
|
||||
</Table.Column>
|
||||
<Table.Column title="操作" align="center">
|
||||
<template #default="{ record }">
|
||||
<Button
|
||||
type="link"
|
||||
@click="
|
||||
openHistory(props.deviceId, record.identifier, record.dataType)
|
||||
"
|
||||
>
|
||||
查看数据
|
||||
</Button>
|
||||
</template>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
<Grid v-show="viewMode === 'list'">
|
||||
<template #value="{ row }">
|
||||
{{ formatValueWithUnit(row) }}
|
||||
</template>
|
||||
<template #updateTime="{ row }">
|
||||
{{ row.updateTime ? formatDateTime(row.updateTime) : '-' }}
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<Button
|
||||
type="link"
|
||||
@click="openHistory(props.deviceId, row.identifier, row.dataType)"
|
||||
>
|
||||
查看数据
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DeviceDetailsThingModelPropertyHistory
|
||||
ref="historyRef"
|
||||
:device-id="props.deviceId"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 移除 a-row 的额外边距 */
|
||||
@@ -0,0 +1,281 @@
|
||||
<!-- 设备服务调用 -->
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Button, RangePicker, Select, Space, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
identifier: '',
|
||||
times: undefined as [string, string] | undefined,
|
||||
});
|
||||
|
||||
/** 服务类型的物模型数据 */
|
||||
const serviceThingModels = computed(() => {
|
||||
return props.thingModelList.filter(
|
||||
(item: ThingModelData) =>
|
||||
String(item.type) === String(IoTThingModelTypeEnum.SERVICE),
|
||||
);
|
||||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'requestTime',
|
||||
title: '调用时间',
|
||||
width: 180,
|
||||
slots: { default: 'requestTime' },
|
||||
},
|
||||
{
|
||||
field: 'responseTime',
|
||||
title: '响应时间',
|
||||
width: 180,
|
||||
slots: { default: 'responseTime' },
|
||||
},
|
||||
{
|
||||
field: 'identifier',
|
||||
title: '标识符',
|
||||
width: 160,
|
||||
slots: { default: 'identifier' },
|
||||
},
|
||||
{
|
||||
field: 'serviceName',
|
||||
title: '服务名称',
|
||||
width: 160,
|
||||
slots: { default: 'serviceName' },
|
||||
},
|
||||
{
|
||||
field: 'callType',
|
||||
title: '调用方式',
|
||||
width: 100,
|
||||
slots: { default: 'callType' },
|
||||
},
|
||||
{
|
||||
field: 'inputParams',
|
||||
title: '输入参数',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
slots: { default: 'inputParams' },
|
||||
},
|
||||
{
|
||||
field: 'outputParams',
|
||||
title: '输出参数',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
slots: { default: 'outputParams' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 创建 Grid 实例 */
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
if (!props.deviceId) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
return await getDeviceMessagePairPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
deviceId: props.deviceId,
|
||||
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
|
||||
identifier: queryParams.identifier || undefined,
|
||||
times: queryParams.times,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: false,
|
||||
search: false,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryParams.identifier = '';
|
||||
queryParams.times = undefined;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 获取服务名称 */
|
||||
function getServiceName(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const service = serviceThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
return service?.name || identifier;
|
||||
}
|
||||
|
||||
/** 获取调用方式 */
|
||||
function getCallType(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const service = serviceThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
if (!service?.service?.callType) return '-';
|
||||
return getThingModelServiceCallTypeLabel(service.service.callType) || '-';
|
||||
}
|
||||
|
||||
/** 解析参数 */
|
||||
function parseParams(params: string) {
|
||||
if (!params) return '-';
|
||||
try {
|
||||
const parsed = JSON.parse(params);
|
||||
if (parsed.params) {
|
||||
return JSON.stringify(parsed.params, null, 2);
|
||||
}
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (delay > 0) {
|
||||
setTimeout(() => gridApi.query(), delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听设备标识变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
handleQuery();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
if (props.deviceId) {
|
||||
handleQuery();
|
||||
}
|
||||
});
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 搜索区域 -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>标识符:</span>
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
allow-clear
|
||||
placeholder="请选择服务标识符"
|
||||
style="width: 240px"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="service in serviceThingModels"
|
||||
:key="service.identifier"
|
||||
:value="service.identifier!"
|
||||
>
|
||||
{{ service.name }}({{ service.identifier }})
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>时间范围:</span>
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
show-time
|
||||
style="width: 360px"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</div>
|
||||
<Space>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:search" />
|
||||
</template>
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="resetQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 服务调用列表 -->
|
||||
<Grid>
|
||||
<template #requestTime="{ row }">
|
||||
{{
|
||||
row.request?.reportTime ? formatDateTime(row.request.reportTime) : '-'
|
||||
}}
|
||||
</template>
|
||||
<template #responseTime="{ row }">
|
||||
{{ row.reply?.reportTime ? formatDateTime(row.reply.reportTime) : '-' }}
|
||||
</template>
|
||||
<template #identifier="{ row }">
|
||||
<Tag color="blue" size="small">
|
||||
{{ row.request?.identifier }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #serviceName="{ row }">
|
||||
{{ getServiceName(row.request?.identifier) }}
|
||||
</template>
|
||||
<template #callType="{ row }">
|
||||
{{ getCallType(row.request?.identifier) }}
|
||||
</template>
|
||||
<template #inputParams="{ row }">
|
||||
{{ parseParams(row.request?.params) }}
|
||||
</template>
|
||||
<template #outputParams="{ row }">
|
||||
<span v-if="row.reply">
|
||||
{{
|
||||
`{"code":${row.reply.code},"msg":"${row.reply.msg}","data":${row.reply.data}\}`
|
||||
}}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user