@steedos-labs/plugin-workflow 3.0.20 → 3.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main/default/applications/approve_workflow.app.yml +11 -2
- package/main/default/manager/handlers_manager.js +11 -12
- package/main/default/manager/workflow_manager.js +12 -0
- package/main/default/objects/instances/buttons/instance_cc.button.yml +4 -0
- package/main/default/objects/instances/buttons/instance_reassign.button.yml +4 -0
- package/main/default/pages/flow_selector.page.amis.json +1 -1
- package/main/default/pages/page_instance_print.page.amis.json +1 -1
- package/main/default/routes/api_workflow_chart.router.js +41 -9
- package/main/default/routes/api_workflow_next_step.router.js +1 -1
- package/main/default/routes/api_workflow_next_step_users.router.js +7 -2
- package/main/default/routes/flow_form_design.ejs +3 -1
- package/main/default/routes/office_preview.router.js +73 -0
- package/main/default/triggers/amis_form_design.trigger.js +2 -3
- package/package.json +1 -1
- package/public/office/preview.html +33 -0
- package/public/workflow/index.css +8 -0
- package/src/util/templateConverter.js +8 -2
|
@@ -34,6 +34,15 @@ nav_schema: {
|
|
|
34
34
|
"onEvent": {
|
|
35
35
|
"click": {
|
|
36
36
|
"actions": [
|
|
37
|
+
{
|
|
38
|
+
"actionType": "setValue",
|
|
39
|
+
"componentId": "u:instanceNav",
|
|
40
|
+
"args": {
|
|
41
|
+
"value": {
|
|
42
|
+
"nav_reload_token": "${_|now|date:x}"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
37
46
|
{
|
|
38
47
|
"actionType": "rebuild",
|
|
39
48
|
"componentId": "u:instanceNav"
|
|
@@ -59,12 +68,12 @@ nav_schema: {
|
|
|
59
68
|
],
|
|
60
69
|
"schemaApi": {
|
|
61
70
|
"method": "get",
|
|
62
|
-
"url": "${context.rootUrl}/api/${appId}/workflow/nav",
|
|
71
|
+
"url": "${context.rootUrl}/api/${appId}/workflow/nav?reload=${nav_reload_token}",
|
|
63
72
|
"headers": {
|
|
64
73
|
"Authorization": "Bearer ${context.tenantId},${context.authToken}"
|
|
65
74
|
},
|
|
66
75
|
"messages": {},
|
|
67
|
-
"adaptor": "payload.data.value = window.location.pathname + decodeURIComponent(window.location.search);\n// return payload;\n\n\nreturn {\n \"type\": \"service\",\n \"data\": payload.data,\n \"body\": [{\n \"type\": \"input-tree\",\n \"name\": \"
|
|
76
|
+
"adaptor": "payload.data.value = window.location.pathname + decodeURIComponent(window.location.search);\n// return payload;\n\n\nreturn {\n \"type\": \"service\",\n \"data\": payload.data,\n \"body\": [{\n \"type\": \"input-tree\",\n \"name\": \"tree_\" + new Date().getTime(),\n \"treeContainerClassName\": \"h-full bg-white\",\n \"className\": \"instance-box-tree h-full w-full p-0 bg-white\",\n \"id\": \"u:9f3dd961ca12_\" + new Date().getTime(),\n \"stacked\": true,\n \"multiple\": false,\n \"enableNodePath\": false,\n \"hideRoot\": true,\n \"showIcon\": true,\n \"initiallyOpen\": false,\n \"virtualThreshold\": 100000,\n \"value\": \"${value}\",\n \"size\": \"md\",\n \"onEvent\": {\n \"change\": {\n \"actions\": [\n {\n \"actionType\": \"setValue\",\n \"componentId\": \"instances_list_service\",\n \"args\": {\n \"value\": {\n \"isFlowDataDone\": false\n }\n }\n },\n {\n \"actionType\": \"custom\",\n \"script\": \"//获取上一次的flowId和categoryId\\nconst lastFlowId = event.data.flowId;\\nconst lastCategoryId = event.data.categoryId;\\n//从value中获取最新的flowId\\nvar flowIdRegex = /&flowId=([^&]+)/;\\nvar flowIdMatch = event.data.value.match(flowIdRegex);\\nconst flowId = flowIdMatch && flowIdMatch.length > 0 ? flowIdMatch[1] : \\\"\\\";\\n//从value中获取最新的categoryId\\nvar categoryIdRegex = /&categoryId=([^&]+)/;\\nvar categoryIdMatch = event.data.value.match(categoryIdRegex);\\nconst categoryId = categoryIdMatch && categoryIdMatch.length > 0 ? categoryIdMatch[1] : \\\"\\\";\\n//获取上一次的listname和最新的listname\\nconst lastListName = event.data.listName;\\nconst listName = event.data.value.split('?')[0].split('instances/grid/')[1];\\n//切换流程时清除过滤条件\\nif (lastListName == \\\"monitor\\\" && listName == \\\"monitor\\\" && (flowId != lastFlowId || categoryId != lastCategoryId)) {\\n listViewPropsStoreKey = window.location.pathname + \\\"/crud\\\";\\n sessionStorage.removeItem(listViewPropsStoreKey);\\n sessionStorage.removeItem(listViewPropsStoreKey + \\\"/query\\\");\\n}\"\n },\n {\n \"actionType\": \"setValue\",\n \"componentId\": \"instances_list_service\",\n \"args\": {\n \"value\": {\n \"additionalFilters\": [\n \"${event.data.options.name}\",\n \"=\",\n \"${event.data.options.value}\"\n ]\n }\n },\n \"expression\": \"${event.data.options.level>=10}\"\n },\n {\n \"args\": {\n \"link\": \"${event.data.value}\",\n \"blank\": false\n },\n \"actionType\": \"link\"\n }\n ]\n }\n },\n \"menuTpl\": {\n \"type\": \"wrapper\",\n \"className\": \"flex items-center py-1.5 px-3 m-0 rounded-md transition-colors duration-150\",\n \"body\": [\n {\n \"type\": \"tpl\",\n \"className\": \"flex-1 leading-6 truncate instance-menu-label\",\n \"tpl\": \"${label}\",\n \"id\": \"u:9dee51f00db4\"\n },\n {\n \"type\": \"tpl\",\n \"className\": \"ml-auto\",\n \"tpl\": \"\",\n \"badge\": {\n \"className\": \"h-0\",\n \"offset\": [\n -5,\n 0\n ],\n \"mode\": \"text\",\n \"text\": \"${tag | toInt}\",\n \"overflowCount\": 999\n },\n \"id\": \"u:2329cd1fecc2\"\n }\n ],\n \"id\": \"u:545154bcc334\"\n },\n \"unfoldedLevel\": 2,\n \"options\": payload.data.options\n }]\n}"
|
|
68
77
|
# "adaptor": "payload.data.value = window.location.pathname + decodeURIComponent(window.location.search); return payload;"
|
|
69
78
|
},
|
|
70
79
|
"messages": {},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
1
|
const _ = require('underscore');
|
|
3
2
|
|
|
4
3
|
const { getCollection } = require('../utils/collection');
|
|
5
4
|
const { excuteTriggers } = require('../utils/trigger')
|
|
6
5
|
const WorkflowManager = require('./workflow_manager');
|
|
6
|
+
// const UUFlowManager = require('./uuflow_manager'); // Moved inside function to avoid circular dependency
|
|
7
7
|
|
|
8
8
|
const getHandlersManager = {};
|
|
9
9
|
|
|
@@ -20,7 +20,7 @@ getHandlersManager.getHandlersByUsersAndRoles = async function (user_ids, role_i
|
|
|
20
20
|
approve_users = approve_users.concat(users);
|
|
21
21
|
}
|
|
22
22
|
} else {
|
|
23
|
-
throw new Error('error! user_id
|
|
23
|
+
throw new Error('error! user_id不合法');
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -147,6 +147,7 @@ getHandlersManager.getHandlersByOrgAndRole = async function (org_id, role_id, sp
|
|
|
147
147
|
};
|
|
148
148
|
|
|
149
149
|
getHandlersManager.getHandlers = async function (instance_id, step_id, login_user_id) {
|
|
150
|
+
const UUFlowManager = require('./uuflow_manager');
|
|
150
151
|
const instancesCollection = await getCollection('instances');
|
|
151
152
|
const flowsCollection = await getCollection('flows');
|
|
152
153
|
const formsCollection = await getCollection('forms');
|
|
@@ -287,7 +288,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
const newest_values = await
|
|
291
|
+
const newest_values = await UUFlowManager.getUpdatedValues(instance);
|
|
291
292
|
let org_ids = [];
|
|
292
293
|
let org_ids_names = [];
|
|
293
294
|
|
|
@@ -363,7 +364,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
363
364
|
}
|
|
364
365
|
}
|
|
365
366
|
|
|
366
|
-
const newest_values = await
|
|
367
|
+
const newest_values = await UUFlowManager.getUpdatedValues(instance);
|
|
367
368
|
let org_ids = [];
|
|
368
369
|
let org_ids_names = [];
|
|
369
370
|
|
|
@@ -468,7 +469,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
468
469
|
}
|
|
469
470
|
}
|
|
470
471
|
|
|
471
|
-
const newest_values = await
|
|
472
|
+
const newest_values = await UUFlowManager.getUpdatedValues(instance);
|
|
472
473
|
let user_ids_names = [];
|
|
473
474
|
|
|
474
475
|
if (newest_values[field_code]) {
|
|
@@ -484,14 +485,14 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
484
485
|
for (const user of user_ids_names) {
|
|
485
486
|
const check_user_count = await spaceUsersCollection.find({
|
|
486
487
|
space: space_id,
|
|
487
|
-
user: user["id"]
|
|
488
|
+
user: _.isString(user) ? user : user["id"]
|
|
488
489
|
}).count();
|
|
489
490
|
|
|
490
491
|
if (check_user_count === 0) {
|
|
491
492
|
throw new Error('error! 人员ID不合法');
|
|
492
493
|
}
|
|
493
494
|
|
|
494
|
-
user_ids.push(user["id"]);
|
|
495
|
+
user_ids.push(_.isString(user) ? user : user["id"]);
|
|
495
496
|
}
|
|
496
497
|
|
|
497
498
|
user_ids = _.uniq(user_ids);
|
|
@@ -548,7 +549,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
548
549
|
}
|
|
549
550
|
}
|
|
550
551
|
|
|
551
|
-
const newest_values = await
|
|
552
|
+
const newest_values = await UUFlowManager.getUpdatedValues(instance);
|
|
552
553
|
let user_ids_names = [];
|
|
553
554
|
|
|
554
555
|
if (newest_values[field_code]) {
|
|
@@ -560,18 +561,16 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
560
561
|
}
|
|
561
562
|
|
|
562
563
|
let user_ids = [];
|
|
563
|
-
|
|
564
564
|
for (const user of user_ids_names) {
|
|
565
565
|
const check_user_count = await spaceUsersCollection.find({
|
|
566
566
|
space: space_id,
|
|
567
|
-
user: user["id"]
|
|
567
|
+
user: _.isString(user) ? user : user["id"]
|
|
568
568
|
}).count();
|
|
569
|
-
|
|
570
569
|
if (check_user_count === 0) {
|
|
571
570
|
throw new Error('error! 人员ID不合法');
|
|
572
571
|
}
|
|
573
572
|
|
|
574
|
-
user_ids.push(user["id"]);
|
|
573
|
+
user_ids.push(_.isString(user) ? user : user["id"]);
|
|
575
574
|
}
|
|
576
575
|
|
|
577
576
|
users = _.uniq(user_ids);
|
|
@@ -116,6 +116,18 @@ WorkflowManager.getRole = async function (spaceId, roleId) {
|
|
|
116
116
|
});
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
+
WorkflowManager.getRoles = async function (spaceId, roleIds) {
|
|
120
|
+
if (!roleIds || !spaceId) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const flowRolesCollection = await getCollection('flow_roles');
|
|
125
|
+
return await flowRolesCollection.find({
|
|
126
|
+
_id: { $in: roleIds },
|
|
127
|
+
space: spaceId
|
|
128
|
+
}).toArray();
|
|
129
|
+
};
|
|
130
|
+
|
|
119
131
|
WorkflowManager.getSpacePositions = async function (spaceId) {
|
|
120
132
|
const flowPositionsCollection = await getCollection('flow_positions');
|
|
121
133
|
return await flowPositionsCollection.find({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "liquid",
|
|
3
|
-
"template": "<style>\n @keyframes fadeUpSpring {\n 0% { opacity: 0; transform: translateY(10px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n \n /* Make scrollbars standardized and visible */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: rgba(0, 0, 0, 0.25); /* Darker for visibility on gray bg */\n border-radius: 4px;\n border: 2px solid transparent; /* Creates padding effect */\n background-clip: content-box;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: rgba(0, 0, 0, 0.4);\n }\n\n /* \n Fix outer modal scrollbar - SAFER VERSION \n Only apply these aggressive overrides (no padding, hidden overflow)\n to the specific modal that contains our component (identified by #steedosFlowSelectorSidebarList).\n This prevents breaking other stacked modals like 'Confirm Dialogs'.\n */\n .antd-Modal-body:has(#steedosFlowSelectorSidebarList) {\n overflow: hidden !important;\n padding: 0 !important; /* Optional: maximize space */\n display: flex;\n flex-direction: column;\n }\n\n /* Ensure the AMIS container fills height if needed */\n .antd-Service, .liquid-amis-container {\n height: 100%;\n }\n</style>\n\n<!-- Main Container: Fixed Height 70vh. -->\n<div class=\"flex h-[70vh] max-h-[800px] w-full overflow-hidden font-sans text-gray-900 bg-white\" style=\"min-height: 0;\">\n\n <!-- Left Sidebar -->\n <!-- flex-col, h-full, overflow-hidden -->\n <div class=\"flex flex-col w-[260px] h-full border-r border-gray-200 bg-[#F2F2F7] shrink-0 overflow-hidden\">\n <!-- Header -->\n <div class=\"shrink-0 pt-4 pb-2 px-3\">\n <div class=\"px-2 mb-3 text-2xl font-bold tracking-tight text-black\">流程</div>\n <div class=\"relative group\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5 text-gray-500\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </div>\n <input type=\"text\" id=\"searchInput\" placeholder=\"搜索\" class=\"w-full rounded-[10px] border-none bg-[#767680]/10 py-1.5 pl-9 pr-3 text-[14px] text-gray-900 placeholder:text-gray-500 outline-none transition-all duration-200 focus:bg-white focus:shadow-sm focus:ring-2 focus:ring-blue-500/20\">\n </div>\n </div>\n \n <!-- List Container -->\n <!-- min-h-0 is CRITICAL for flex child scrolling -->\n <div class=\"flex-1 min-h-0 overflow-y-auto px-2 pb-4 space-y-0.5 scroll-smooth\" id=\"steedosFlowSelectorSidebarList\">\n </div>\n </div>\n\n <!-- Right Content -->\n <!-- flex-1 fills remaining width -->\n <div class=\"flex-1 h-full relative bg-white overflow-hidden\">\n <!-- Absolute inset-0 locks the scroll container size -->\n <div id=\"mainContentScroll\" class=\"absolute inset-0 overflow-y-auto scroll-smooth p-6\">\n <div id=\"contentContainer\" class=\"w-full h-auto min-h-full\">\n <div class=\"flex h-full w-full flex-col items-center justify-center pt-20\">\n <div class=\"inline-flex items-center gap-2 text-gray-400 text-sm animate-pulse\">\n <svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path>\n </svg>\n <span>正在加载资源...</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n const WorkflowService = {\n apiBase: \"\", \n getHeaders: function() { return { 'Content-Type': 'application/json' }; },\n getData: async function() {\n try {\n const appId = (typeof data !== 'undefined' && data.context && data.context.app_id) ? data.context.app_id : \"\";\n const url = this.apiBase + \"/service/api/flows/getList?action=new&appId=\" + encodeURIComponent(appId);\n const res = await fetch(url, { headers: this.getHeaders() });\n const treeData = await res.json();\n const categories = [];\n const parsedFlows = [];\n if (Array.isArray(treeData)) {\n treeData.forEach(cat => {\n categories.push({ _id: cat._id, name: cat.name });\n if (Array.isArray(cat.flows)) {\n cat.flows.forEach(f => {\n parsedFlows.push({\n id: f._id, name: f.name, categoryId: cat._id, categoryName: cat.name || \"其他流程\" \n });\n });\n }\n });\n }\n return { categories: categories, flows: parsedFlows };\n } catch (e) { \n console.error(\"WorkflowService Error:\", e);\n return { categories: [], flows: [] }; \n }\n },\n getFavorites: function() {\n const saved = localStorage.getItem('steedos_fav_ids');\n return saved ? JSON.parse(saved) : [];\n },\n toggleFavorite: function(flowId, isFav) {\n let favs = this.getFavorites();\n if (isFav) { if (!favs.includes(flowId)) favs.push(flowId); } \n else { favs = favs.filter(id => id !== flowId); }\n localStorage.setItem('steedos_fav_ids', JSON.stringify(favs));\n return favs;\n }\n };\n\n const AppState = { allFlows: [], categories: [], favorites: [] };\n const sidebarEl = document.getElementById('steedosFlowSelectorSidebarList');\n const contentEl = document.getElementById('contentContainer');\n const searchInput = document.getElementById('searchInput');\n\n async function init() {\n try {\n const data = await WorkflowService.getData();\n AppState.allFlows = data.flows;\n AppState.categories = data.categories;\n AppState.favorites = WorkflowService.getFavorites();\n renderUI();\n } catch (e) {\n contentEl.innerHTML = `<div class=\"text-gray-400 text-sm\">加载失败,请检查网络</div>`;\n }\n }\n\n function renderUI(filterText = \"\") {\n sidebarEl.innerHTML = \"\";\n contentEl.innerHTML = \"\";\n\n const isSearching = filterText.length > 0;\n let groups = [];\n\n const favFlows = AppState.allFlows.filter(f => \n AppState.favorites.includes(f.id) && \n (isSearching ? f.name.includes(filterText) : true)\n );\n if (favFlows.length > 0) {\n groups.push({ id: 'fav', name: \"我的收藏\", items: favFlows, isFav: true });\n }\n\n AppState.categories.forEach(cat => {\n const items = AppState.allFlows.filter(f => \n f.categoryId === cat._id &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (items.length > 0) {\n groups.push({ id: cat._id, name: cat.name, items: items, isFav: false });\n }\n });\n\n const otherItems = AppState.allFlows.filter(f => \n !AppState.categories.find(c => c._id === f.categoryId) &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (otherItems.length > 0) {\n groups.push({ id: 'other', name: \"其他流程\", items: otherItems, isFav: false });\n }\n\n if (groups.length === 0) {\n contentEl.innerHTML = `<div class=\"animate-[fadeUpSpring_0.5s_ease-out] text-center pt-20\"><div class=\"text-gray-200 text-7xl mb-4\">∅</div><div class=\"text-gray-400 text-sm\">未找到匹配流程</div></div>`;\n return;\n }\n\n groups.forEach((group, index) => {\n const groupId = `group-\\${group.id}`;\n const navItem = document.createElement('div');\n let navBase = \"group flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-[14px] transition-all duration-200 ease-out select-none\";\n let activeClass = \"bg-[#007AFF] text-white shadow-sm font-medium\";\n let inactiveClass = \"text-gray-700 hover:bg-black/5 active:bg-black/10\";\n \n navItem.className = `\\${navBase} \\${index === 0 ? activeClass : inactiveClass}`;\n const badgeClass = index === 0 ? \"text-white/80\" : \"text-gray-400 group-hover:text-gray-500\";\n \n navItem.innerHTML = `<span class=\"truncate\">\\${group.isFav ? '★ ' : ''}\\${group.name}</span><span class=\"\\${badgeClass} text-[12px] font-medium transition-colors\">\\${group.items.length}</span>`;\n \n navItem.onclick = () => {\n Array.from(sidebarEl.children).forEach(el => {\n el.className = `\\${navBase} \\${inactiveClass}`;\n el.querySelector('span:last-child').className = \"text-gray-400 group-hover:text-gray-500 text-[12px] font-medium transition-colors\";\n });\n navItem.className = `\\${navBase} \\${activeClass}`;\n navItem.querySelector('span:last-child').className = \"text-white/80 text-[12px] font-medium transition-colors\";\n \n const target = document.getElementById(groupId);\n const container = document.getElementById('mainContentScroll');\n if(target && container) {\n const targetTop = target.getBoundingClientRect().top; \n const containerTop = container.getBoundingClientRect().top; \n container.scrollTo({ top: container.scrollTop + targetTop - containerTop - 16, behavior: 'smooth' });\n }\n };\n sidebarEl.appendChild(navItem);\n\n const section = document.createElement('div');\n section.id = groupId;\n section.className = \"mb-10\";\n const headerColor = group.isFav ? 'text-amber-500' : 'text-gray-900';\n section.innerHTML = `<div class=\"sticky top-0 z-20 mb-4 bg-white/95 pb-2 text-xl font-bold tracking-tight backdrop-blur-xl text-left border-b border-gray-100 \\${headerColor}\">\\${group.name}</div>`;\n\n const grid = document.createElement('div');\n grid.className = 'grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4';\n\n group.items.forEach((flow, i) => {\n const isFav = AppState.favorites.includes(flow.id);\n const colorMap = ['bg-blue-50 text-blue-600', 'bg-orange-50 text-orange-600', 'bg-emerald-50 text-emerald-600', 'bg-indigo-50 text-indigo-600'];\n const colorClass = colorMap[(flow.name.length + i) % 4];\n const firstChar = flow.name.replace(/【.*?】/g, '').charAt(0) || flow.name.charAt(0);\n const card = document.createElement('div');\n card.className = 'group relative flex h-auto min-h-[72px] cursor-pointer items-center rounded-2xl border border-gray-100 bg-white p-3 text-left shadow-[0_2px_8px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.02] transition-all duration-300 ease-out animate-[fadeUpSpring_0.6s_cubic-bezier(0.16,1,0.3,1)_forwards] hover:-translate-y-1 hover:border-gray-200 hover:shadow-[0_12px_24px_rgba(0,0,0,0.08)] active:scale-[0.98] active:bg-gray-50';\n card.style.animationDelay = `\\${Math.min(i * 0.04, 0.6)}s`;\n card.style.opacity = '0';\n const iconClass = isFav ? 'text-yellow-400 fill-current' : 'text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]';\n const btnBgClass = isFav ? 'opacity-100 hover:scale-110' : 'opacity-0 group-hover:opacity-100 hover:bg-gray-100 hover:scale-110';\n\n card.innerHTML = `\n <div class=\"star-btn group/btn absolute right-2 top-1/2 -translate-y-1/2 z-20 flex h-8 w-8 items-center justify-center rounded-full transition-all duration-200 \\${btnBgClass}\" title=\"\\${isFav ? '取消收藏' : '加入收藏'}\">\n <svg class=\"h-5 w-5 transition-colors duration-300 \\${iconClass}\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z\" />\n </svg>\n </div>\n <div class=\"mr-4 flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[16px] font-bold \\${colorClass}\">\\${firstChar}</div>\n <div class=\"flex-1 pr-8 text-[15px] font-medium text-gray-900 line-clamp-2 leading-relaxed tracking-tight\" title=\"\\${flow.name}\">\\${flow.name}</div>\n `;\n card.onclick = () => {\n setTimeout(() => {\n data._scoped.doAction([\n { \"actionType\": \"broadcast\", \"args\": { \"eventName\": \"flows.selected\" }, \"data\": { \"value\": flow.id } }\n ])\n }, 50);\n };\n const starBtn = card.querySelector('.star-btn');\n const starIcon = starBtn.querySelector('svg');\n starBtn.onclick = (e) => {\n e.stopPropagation();\n const newFavState = !starBtn.classList.contains('active-fav');\n if (newFavState) {\n starBtn.classList.add('active-fav', 'opacity-100');\n starBtn.classList.add('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-yellow-400 fill-current');\n } else {\n starBtn.classList.remove('active-fav', 'opacity-100');\n starBtn.classList.remove('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]');\n }\n AppState.favorites = WorkflowService.toggleFavorite(flow.id, newFavState);\n setTimeout(() => renderUI(searchInput.value), 300);\n };\n if (isFav) starBtn.classList.add('active-fav');\n grid.appendChild(card);\n });\n section.appendChild(grid);\n contentEl.appendChild(section);\n });\n }\n\n searchInput.addEventListener('input', (e) => renderUI(e.target.value.trim()));\n init();\n</script>\n",
|
|
3
|
+
"template": "<style>\n @keyframes fadeUpSpring {\n 0% { opacity: 0; transform: translateY(10px); }\n 100% { opacity: 1; transform: translateY(0); }\n }\n \n /* Make scrollbars standardized and visible */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n ::-webkit-scrollbar-thumb {\n background-color: rgba(0, 0, 0, 0.25); /* Darker for visibility on gray bg */\n border-radius: 4px;\n border: 2px solid transparent; /* Creates padding effect */\n background-clip: content-box;\n }\n ::-webkit-scrollbar-thumb:hover {\n background-color: rgba(0, 0, 0, 0.4);\n }\n\n /* \n Fix outer modal scrollbar - SAFER VERSION \n Only apply these aggressive overrides (no padding, hidden overflow)\n to the specific modal that contains our component (identified by #steedosFlowSelectorSidebarList).\n This prevents breaking other stacked modals like 'Confirm Dialogs'.\n */\n .antd-Modal-body:has(#steedosFlowSelectorSidebarList) {\n overflow: hidden !important;\n padding: 0 !important; /* Optional: maximize space */\n display: flex;\n flex-direction: column;\n }\n\n /* Ensure the AMIS container fills height if needed */\n .antd-Service, .liquid-amis-container {\n height: 100%;\n }\n</style>\n\n<!-- Main Container: Fixed Height 70vh. -->\n<div class=\"flex h-[70vh] max-h-[800px] w-full overflow-hidden font-sans text-gray-900 bg-white\" style=\"min-height: 0;\">\n\n <!-- Left Sidebar -->\n <!-- flex-col, h-full, overflow-hidden -->\n <div class=\"flex flex-col w-[260px] h-full border-r border-gray-200 bg-[#F2F2F7] shrink-0 overflow-hidden\">\n <!-- Header -->\n <div class=\"shrink-0 pt-4 pb-2 px-3\">\n <div class=\"px-2 mb-3 text-2xl font-bold tracking-tight text-black\">流程</div>\n <div class=\"relative group\">\n <div class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5 text-gray-500\">\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </div>\n <input type=\"text\" id=\"searchInput\" placeholder=\"搜索\" class=\"w-full rounded-[10px] border-none bg-[#767680]/10 py-1.5 pl-9 pr-3 text-[14px] text-gray-900 placeholder:text-gray-500 outline-none transition-all duration-200 focus:bg-white focus:shadow-sm focus:ring-2 focus:ring-blue-500/20\">\n </div>\n </div>\n \n <!-- List Container -->\n <!-- min-h-0 is CRITICAL for flex child scrolling -->\n <div class=\"flex-1 min-h-0 overflow-y-auto px-2 pb-4 space-y-0.5 scroll-smooth\" id=\"steedosFlowSelectorSidebarList\">\n </div>\n </div>\n\n <!-- Right Content -->\n <!-- flex-1 fills remaining width -->\n <div class=\"flex-1 h-full relative bg-white overflow-hidden\">\n <!-- Absolute inset-0 locks the scroll container size -->\n <div id=\"mainContentScroll\" class=\"absolute inset-0 overflow-y-auto scroll-smooth p-6\">\n <div id=\"contentContainer\" class=\"w-full h-auto min-h-full\">\n <div class=\"flex h-full w-full flex-col items-center justify-center pt-20\">\n <div class=\"inline-flex items-center gap-2 text-gray-400 text-sm animate-pulse\">\n <svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle>\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path>\n </svg>\n <span>正在加载资源...</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n const WorkflowService = {\n apiBase: \"\", \n getHeaders: function() { return { 'Content-Type': 'application/json' }; },\n getData: async function() {\n try {\n const appId = (typeof data !== 'undefined' && data.context && data.context.app_id) ? data.context.app_id : \"\";\n const url = this.apiBase + \"/service/api/flows/getList?action=new&appId=\" + encodeURIComponent(appId);\n const res = await fetch(url, { headers: this.getHeaders() });\n const treeData = await res.json();\n const categories = [];\n const parsedFlows = [];\n if (Array.isArray(treeData)) {\n treeData.forEach(cat => {\n categories.push({ _id: cat._id, name: cat.name });\n if (Array.isArray(cat.flows)) {\n cat.flows.forEach(f => {\n parsedFlows.push({\n id: f._id, name: f.name, categoryId: cat._id, categoryName: cat.name || \"其他流程\" \n });\n });\n }\n });\n }\n return { categories: categories, flows: parsedFlows };\n } catch (e) { \n console.error(\"WorkflowService Error:\", e);\n return { categories: [], flows: [] }; \n }\n },\n getFavorites: function() {\n const saved = localStorage.getItem('steedos_fav_ids');\n return saved ? JSON.parse(saved) : [];\n },\n toggleFavorite: function(flowId, isFav) {\n let favs = this.getFavorites();\n if (isFav) { if (!favs.includes(flowId)) favs.push(flowId); } \n else { favs = favs.filter(id => id !== flowId); }\n localStorage.setItem('steedos_fav_ids', JSON.stringify(favs));\n return favs;\n }\n };\n\n const AppState = { allFlows: [], categories: [], favorites: [] };\n const sidebarEl = document.getElementById('steedosFlowSelectorSidebarList');\n const contentEl = document.getElementById('contentContainer');\n const searchInput = document.getElementById('searchInput');\n\n async function init() {\n try {\n const data = await WorkflowService.getData();\n AppState.allFlows = data.flows;\n AppState.categories = data.categories;\n AppState.favorites = WorkflowService.getFavorites();\n renderUI();\n } catch (e) {\n contentEl.innerHTML = `<div class=\"text-gray-400 text-sm\">加载失败,请检查网络</div>`;\n }\n }\n\n function renderUI(filterText = \"\") {\n sidebarEl.innerHTML = \"\";\n contentEl.innerHTML = \"\";\n\n const isSearching = filterText.length > 0;\n let groups = [];\n\n const favFlows = AppState.allFlows.filter(f => \n AppState.favorites.includes(f.id) && \n (isSearching ? f.name.includes(filterText) : true)\n );\n if (favFlows.length > 0) {\n groups.push({ id: 'fav', name: \"我的收藏\", items: favFlows, isFav: true });\n }\n\n AppState.categories.forEach(cat => {\n const items = AppState.allFlows.filter(f => \n f.categoryId === cat._id &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (items.length > 0) {\n groups.push({ id: cat._id, name: cat.name, items: items, isFav: false });\n }\n });\n\n const otherItems = AppState.allFlows.filter(f => \n !AppState.categories.find(c => c._id === f.categoryId) &&\n (isSearching ? f.name.includes(filterText) : true)\n );\n if (otherItems.length > 0) {\n groups.push({ id: 'other', name: \"其他流程\", items: otherItems, isFav: false });\n }\n\n if (groups.length === 0) {\n contentEl.innerHTML = `<div class=\"animate-[fadeUpSpring_0.5s_ease-out] text-center pt-20\"><div class=\"text-gray-200 text-7xl mb-4\">∅</div><div class=\"text-gray-400 text-sm\">未找到匹配流程</div></div>`;\n return;\n }\n\n groups.forEach((group, index) => {\n const groupId = `group-\\${group.id}`;\n const navItem = document.createElement('div');\n let navBase = \"group flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-[14px] transition-all duration-200 ease-out select-none\";\n let activeClass = \"bg-[#007AFF] text-white shadow-sm font-medium\";\n let inactiveClass = \"text-gray-700 hover:bg-black/5 active:bg-black/10\";\n \n navItem.className = `\\${navBase} \\${index === 0 ? activeClass : inactiveClass}`;\n const badgeClass = index === 0 ? \"text-white/80\" : \"text-gray-400 group-hover:text-gray-500\";\n \n navItem.innerHTML = `<span class=\"truncate\">\\${group.isFav ? '★ ' : ''}\\${group.name}</span><span class=\"\\${badgeClass} text-[12px] font-medium transition-colors\">\\${group.items.length}</span>`;\n \n navItem.onclick = () => {\n Array.from(sidebarEl.children).forEach(el => {\n el.className = `\\${navBase} \\${inactiveClass}`;\n el.querySelector('span:last-child').className = \"text-gray-400 group-hover:text-gray-500 text-[12px] font-medium transition-colors\";\n });\n navItem.className = `\\${navBase} \\${activeClass}`;\n navItem.querySelector('span:last-child').className = \"text-white/80 text-[12px] font-medium transition-colors\";\n \n const target = document.getElementById(groupId);\n const container = document.getElementById('mainContentScroll');\n if(target && container) {\n const targetTop = target.getBoundingClientRect().top; \n const containerTop = container.getBoundingClientRect().top; \n container.scrollTo({ top: container.scrollTop + targetTop - containerTop - 16, behavior: 'smooth' });\n }\n };\n sidebarEl.appendChild(navItem);\n\n const section = document.createElement('div');\n section.id = groupId;\n section.className = \"mb-10\";\n const headerColor = group.isFav ? 'text-amber-500' : 'text-gray-900';\n section.innerHTML = `<div class=\"sticky top-0 z-20 mb-4 bg-white/95 pb-2 text-xl font-bold tracking-tight backdrop-blur-xl text-left border-b border-gray-100 \\${headerColor}\">\\${group.name}</div>`;\n\n const grid = document.createElement('div');\n grid.className = 'grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4';\n\n group.items.forEach((flow, i) => {\n const isFav = AppState.favorites.includes(flow.id);\n const colorMap = ['bg-blue-50 text-blue-600', 'bg-orange-50 text-orange-600', 'bg-emerald-50 text-emerald-600', 'bg-indigo-50 text-indigo-600'];\n const colorClass = colorMap[(flow.name.length + i) % 4];\n const firstChar = flow.name.replace(/【.*?】/g, '').charAt(0) || flow.name.charAt(0);\n const card = document.createElement('div');\n card.className = 'group relative flex h-auto min-h-[72px] cursor-pointer items-center rounded-2xl border border-gray-100 bg-white p-3 text-left shadow-[0_2px_8px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.02] transition-all duration-300 ease-out animate-[fadeUpSpring_0.6s_cubic-bezier(0.16,1,0.3,1)_forwards] hover:-translate-y-1 hover:border-gray-200 hover:shadow-[0_12px_24px_rgba(0,0,0,0.08)] active:scale-[0.98] active:bg-gray-50';\n card.style.animationDelay = `\\${Math.min(i * 0.04, 0.6)}s`;\n card.style.opacity = '0';\n const iconClass = isFav ? 'text-yellow-400 fill-current' : 'text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]';\n const btnBgClass = isFav ? 'opacity-100 hover:scale-110' : 'opacity-0 group-hover:opacity-100 hover:bg-gray-100 hover:scale-110';\n\n card.innerHTML = `\n <div class=\"star-btn group/btn absolute right-2 top-1/2 -translate-y-1/2 z-20 flex h-8 w-8 items-center justify-center rounded-full transition-all duration-200 \\${btnBgClass}\" title=\"\\${isFav ? '取消收藏' : '加入收藏'}\">\n <svg class=\"h-5 w-5 transition-colors duration-300 \\${iconClass}\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z\" />\n </svg>\n </div>\n <div class=\"mr-4 flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[16px] font-bold \\${colorClass}\">\\${firstChar}</div>\n <div class=\"flex-1 pr-8 text-[15px] font-medium text-gray-900 line-clamp-3 leading-relaxed tracking-tight\" title=\"\\${flow.name}\">\\${flow.name}</div>\n `;\n card.onclick = () => {\n setTimeout(() => {\n data._scoped.doAction([\n { \"actionType\": \"broadcast\", \"args\": { \"eventName\": \"flows.selected\" }, \"data\": { \"value\": flow.id } }\n ])\n }, 50);\n };\n const starBtn = card.querySelector('.star-btn');\n const starIcon = starBtn.querySelector('svg');\n starBtn.onclick = (e) => {\n e.stopPropagation();\n const newFavState = !starBtn.classList.contains('active-fav');\n if (newFavState) {\n starBtn.classList.add('active-fav', 'opacity-100');\n starBtn.classList.add('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-yellow-400 fill-current');\n } else {\n starBtn.classList.remove('active-fav', 'opacity-100');\n starBtn.classList.remove('animate-[starPop_0.4s_ease-out]');\n starIcon.setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-gray-300 group-hover/btn:text-gray-400 fill-none stroke-current stroke-[1.5]');\n }\n AppState.favorites = WorkflowService.toggleFavorite(flow.id, newFavState);\n setTimeout(() => renderUI(searchInput.value), 300);\n };\n if (isFav) starBtn.classList.add('active-fav');\n grid.appendChild(card);\n });\n section.appendChild(grid);\n contentEl.appendChild(section);\n });\n }\n\n searchInput.addEventListener('input', (e) => renderUI(e.target.value.trim()));\n init();\n</script>\n",
|
|
4
4
|
"className": "h-full"
|
|
5
5
|
}
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
"body": [
|
|
183
183
|
{
|
|
184
184
|
"type": "wrapper",
|
|
185
|
-
"className": "steedos-instance-detail-wrapper m-0 p-0 flex-1 focus:outline-none lg:order-last sm:m-4 shadow sm:rounded",
|
|
185
|
+
"className": "steedos-instance-print-wrapper steedos-instance-detail-wrapper m-0 p-0 flex-1 focus:outline-none lg:order-last sm:m-4 shadow sm:rounded",
|
|
186
186
|
"body": [
|
|
187
187
|
{
|
|
188
188
|
"type": "service",
|
|
@@ -39,7 +39,7 @@ const FlowversionAPI = {
|
|
|
39
39
|
return str.replace(/\"/g, """).replace(/\n/g, "<br/>");
|
|
40
40
|
},
|
|
41
41
|
|
|
42
|
-
getStepHandlerName: async function (step, insId) {
|
|
42
|
+
getStepHandlerName: async function (step, insId, instance) {
|
|
43
43
|
const db = {
|
|
44
44
|
users: await getCollection('users'),
|
|
45
45
|
// flow_roles: await getCollection('flow_roles')
|
|
@@ -53,9 +53,22 @@ const FlowversionAPI = {
|
|
|
53
53
|
return stepHandlerName;
|
|
54
54
|
}
|
|
55
55
|
let userIds = await getHandlersManager.getHandlers(insId, stepId, loginUserId);
|
|
56
|
+
|
|
57
|
+
// 如果没有找到处理人,从instance的step_approve中查找
|
|
58
|
+
if ((!userIds || userIds.length === 0) && instance?.step_approve && stepId) {
|
|
59
|
+
let stepApproveUserId = instance.step_approve[stepId];
|
|
60
|
+
if (stepApproveUserId) {
|
|
61
|
+
if (typeof stepApproveUserId === 'string') {
|
|
62
|
+
userIds = [stepApproveUserId];
|
|
63
|
+
} else if (Array.isArray(stepApproveUserId)) {
|
|
64
|
+
userIds = stepApproveUserId;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
56
69
|
let usersCol = db.users;
|
|
57
70
|
let approverNames = [];
|
|
58
|
-
for (let userId of userIds) {
|
|
71
|
+
for (let userId of userIds || []) {
|
|
59
72
|
let user = await usersCol.findOne({ _id: userId }, { projection: { name: 1 } });
|
|
60
73
|
if (user) {
|
|
61
74
|
approverNames.push(user.name);
|
|
@@ -111,21 +124,22 @@ const FlowversionAPI = {
|
|
|
111
124
|
return nodeStr;
|
|
112
125
|
},
|
|
113
126
|
|
|
114
|
-
getStepName: async function (step, cachedStepNames, instance_id) {
|
|
127
|
+
getStepName: async function (step, cachedStepNames, instance_id, instance) {
|
|
115
128
|
// 返回step节点名称,优先从缓存cachedStepNames中取,否则调用getStepLabel生成
|
|
116
129
|
let cachedStepName = cachedStepNames[step._id];
|
|
117
130
|
if (cachedStepName) {
|
|
118
131
|
return cachedStepName;
|
|
119
132
|
}
|
|
120
|
-
let stepHandlerName = await FlowversionAPI.getStepHandlerName(step, instance_id);
|
|
133
|
+
let stepHandlerName = await FlowversionAPI.getStepHandlerName(step, instance_id, instance);
|
|
121
134
|
let stepName = FlowversionAPI.getStepLabel(step.name, stepHandlerName);
|
|
122
135
|
cachedStepNames[step._id] = stepName;
|
|
123
136
|
return stepName;
|
|
124
137
|
},
|
|
125
138
|
|
|
126
|
-
generateStepsGraphSyntax: async function (steps, currentStepId, isConvertToString, direction, instance_id) {
|
|
139
|
+
generateStepsGraphSyntax: async function (steps, currentStepId, isConvertToString, direction, instance_id, instance) {
|
|
127
140
|
let nodes = [`graph ${direction}`];
|
|
128
141
|
let cachedStepNames = {};
|
|
142
|
+
let skipSteps = new Set(instance?.skip_steps || []);
|
|
129
143
|
for (let step of steps) {
|
|
130
144
|
let lines = step.lines;
|
|
131
145
|
if (lines?.length) {
|
|
@@ -136,13 +150,17 @@ const FlowversionAPI = {
|
|
|
136
150
|
if (step.step_type === "condition") {
|
|
137
151
|
nodes.push(` class ${step._id} condition;`);
|
|
138
152
|
}
|
|
139
|
-
stepName = await FlowversionAPI.getStepName(step, cachedStepNames, instance_id);
|
|
153
|
+
stepName = await FlowversionAPI.getStepName(step, cachedStepNames, instance_id, instance);
|
|
140
154
|
} else {
|
|
141
155
|
stepName = "";
|
|
142
156
|
}
|
|
157
|
+
// Mark skipped steps
|
|
158
|
+
if (skipSteps.has(step._id)) {
|
|
159
|
+
nodes.push(` class ${step._id} skip-step;`);
|
|
160
|
+
}
|
|
143
161
|
// 原findPropertyByPK("_id",line.to_step),转为find
|
|
144
162
|
let toStep = steps.find(s => s._id === line.to_step);
|
|
145
|
-
let toStepName = await FlowversionAPI.getStepName(toStep, cachedStepNames, instance_id);
|
|
163
|
+
let toStepName = await FlowversionAPI.getStepName(toStep, cachedStepNames, instance_id, instance);
|
|
146
164
|
nodes.push(` ${step._id}("${stepName}")-->${line.to_step}("${toStepName}")`);
|
|
147
165
|
}
|
|
148
166
|
}
|
|
@@ -487,13 +505,13 @@ const FlowversionAPI = {
|
|
|
487
505
|
break;
|
|
488
506
|
}
|
|
489
507
|
default: {
|
|
490
|
-
let instance = await db.instances.findOne({ _id: instance_id }, { projection: { flow_version: 1, flow: 1, traces: { $slice: -1 } } });
|
|
508
|
+
let instance = await db.instances.findOne({ _id: instance_id }, { projection: { flow_version: 1, flow: 1, step_approve: 1, skip_steps: 1, traces: { $slice: -1 } } });
|
|
491
509
|
if (instance) {
|
|
492
510
|
let currentStepId = instance.traces?.[0]?.step;
|
|
493
511
|
let flowversion = await WorkflowManager.getInstanceFlowVersion(instance);
|
|
494
512
|
let steps = flowversion?.steps;
|
|
495
513
|
if (steps?.length) {
|
|
496
|
-
graphSyntax = await FlowversionAPI.generateStepsGraphSyntax(steps, currentStepId, false, direction, instance_id);
|
|
514
|
+
graphSyntax = await FlowversionAPI.generateStepsGraphSyntax(steps, currentStepId, false, direction, instance_id, instance);
|
|
497
515
|
} else {
|
|
498
516
|
error_msg = "没有找到当前申请单的流程步骤数据";
|
|
499
517
|
}
|
|
@@ -557,6 +575,20 @@ const FlowversionAPI = {
|
|
|
557
575
|
stroke: rgb(204, 204, 255);
|
|
558
576
|
stroke-width: 1px;
|
|
559
577
|
}
|
|
578
|
+
#flow-steps-svg .node.skip-step rect{
|
|
579
|
+
fill: #e8e8e8;
|
|
580
|
+
stroke: #999999;
|
|
581
|
+
stroke-width: 2px;
|
|
582
|
+
stroke-dasharray: 5, 5;
|
|
583
|
+
opacity: 0.7;
|
|
584
|
+
}
|
|
585
|
+
#flow-steps-svg .node.skip-step .step-name{
|
|
586
|
+
color: #666666;
|
|
587
|
+
text-decoration: line-through;
|
|
588
|
+
}
|
|
589
|
+
#flow-steps-svg .node.skip-step .step-handler-name{
|
|
590
|
+
color: #999999;
|
|
591
|
+
}
|
|
560
592
|
#flow-steps-svg .node .trace-handler-name{
|
|
561
593
|
color: #777;
|
|
562
594
|
}
|
|
@@ -401,7 +401,7 @@ router.post('/api/workflow/v2/set_instance_steps', requireAuthentication, async
|
|
|
401
401
|
skip_steps: stepId
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
|
-
await objectql.getObject('instances').
|
|
404
|
+
await objectql.getObject('instances').directUpdate(instanceId, doc);
|
|
405
405
|
|
|
406
406
|
return res.status(200).send({ status: 0 });
|
|
407
407
|
} catch (error) {
|
|
@@ -14,6 +14,7 @@ const { excuteTriggers } = require('../utils/trigger');
|
|
|
14
14
|
const objectql = require('@steedos/objectql');
|
|
15
15
|
const WorkflowManager = require('../manager/workflow_manager');
|
|
16
16
|
const UUFlowManager = require('../manager/uuflow_manager');
|
|
17
|
+
const { t } = require('@steedos/i18n');
|
|
17
18
|
|
|
18
19
|
const getFieldName = (fields, fieldId)=>{
|
|
19
20
|
const field = _.find(fields, (item)=>{
|
|
@@ -167,7 +168,8 @@ router.post('/api/workflow/v2/nextStepUsers', requireAuthentication, async funct
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
if (!nextStepUsers || nextStepUsers.length < 1) {
|
|
170
|
-
|
|
171
|
+
const roles = await WorkflowManager.getRoles(instance.space, approverRoleIds);
|
|
172
|
+
error = t('next_step_users_not_found.applicant_role', {role_name: _.join(_.map(roles, 'name'), ', ')}, userSession.language)
|
|
171
173
|
}
|
|
172
174
|
} else {
|
|
173
175
|
error = "请先填写表单值";
|
|
@@ -189,7 +191,10 @@ router.post('/api/workflow/v2/nextStepUsers', requireAuthentication, async funct
|
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
if (!nextStepUsers || nextStepUsers.length < 1) {
|
|
192
|
-
|
|
194
|
+
|
|
195
|
+
const roles = await WorkflowManager.getRoles(instance.space, approverRoleIds);
|
|
196
|
+
console.log(`approverRoleIds`, approverRoleIds, roles)
|
|
197
|
+
error = t('next_step_users_not_found.applicant_role', {role_name: _.join(_.map(roles, 'name'), ', ')}, userSession.language)
|
|
193
198
|
}
|
|
194
199
|
} else {
|
|
195
200
|
error = "请先填写表单值";
|
|
@@ -276,11 +276,13 @@
|
|
|
276
276
|
if(field.steedos_field){
|
|
277
277
|
return {
|
|
278
278
|
type: `sfield-${_.toLower(_.camelCase(field.steedos_field.type))}`,
|
|
279
|
-
config: field.steedos_field
|
|
279
|
+
config: field.steedos_field,
|
|
280
|
+
_id: field._id
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
const tpl = {
|
|
285
|
+
_id: field._id,
|
|
284
286
|
label: field.name || field.code,
|
|
285
287
|
name: field.code,
|
|
286
288
|
required: field.is_required,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const router = express.Router();
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// 根据后缀名判断文档类型
|
|
8
|
+
function getDocumentType(ext) {
|
|
9
|
+
if (['doc', 'docx', 'rtf', 'txt'].includes(ext)) return 'word';
|
|
10
|
+
if (['xls', 'xlsx', 'csv'].includes(ext)) return 'cell';
|
|
11
|
+
if (['ppt', 'pptx'].includes(ext)) return 'slide';
|
|
12
|
+
return 'word';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// startup
|
|
16
|
+
router.get('/api/workflow/office/preview', async function (req, res) {
|
|
17
|
+
const fileUrl = req.query.src; // 外部传入的文件下载直链
|
|
18
|
+
if (!fileUrl) return res.status(400).send("Missing src parameter");
|
|
19
|
+
|
|
20
|
+
const fileName = decodeURIComponent(path.basename(new URL(fileUrl).pathname));
|
|
21
|
+
const fileType = fileName.split('.').pop();
|
|
22
|
+
|
|
23
|
+
// 构建 ONLYOFFICE 配置对象
|
|
24
|
+
const config = {
|
|
25
|
+
document: {
|
|
26
|
+
fileType: fileType,
|
|
27
|
+
key: Buffer.from(fileUrl).toString('base64').substring(0, 20), // 生成唯一key
|
|
28
|
+
title: fileName,
|
|
29
|
+
url: fileUrl,
|
|
30
|
+
permissions: {
|
|
31
|
+
edit: false, // 禁止编辑
|
|
32
|
+
download: true, // 是否允许下载
|
|
33
|
+
print: true, // 是否允许打印
|
|
34
|
+
fillForms: false // 禁止表单填写
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
documentType: getDocumentType(fileType),
|
|
38
|
+
editorConfig: {
|
|
39
|
+
mode: 'view', // 强制只读模式
|
|
40
|
+
lang: 'zh-CN',
|
|
41
|
+
canAutosave: false,
|
|
42
|
+
canShare: false,
|
|
43
|
+
customization: {
|
|
44
|
+
about: false, // 隐藏右键菜单中的“关于”
|
|
45
|
+
comments: false, // 隐藏评论
|
|
46
|
+
feedback: {
|
|
47
|
+
visible: false // 隐藏反馈按钮
|
|
48
|
+
},
|
|
49
|
+
forcesave: false,
|
|
50
|
+
help: false, // 隐藏帮助按钮
|
|
51
|
+
hideRightMenu: true, // 隐藏右侧菜单栏
|
|
52
|
+
logo: {
|
|
53
|
+
image: "", // 清空 Logo 图片地址
|
|
54
|
+
imageEmbedded: "", // 清空嵌入式 Logo 地址
|
|
55
|
+
url: "" // 点击 Logo 跳转的链接清空
|
|
56
|
+
},
|
|
57
|
+
toolbarNoTabs: true // 让工具栏更紧凑
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// 使用环境变量中的 Secret 生成 Token
|
|
63
|
+
const token = jwt.sign(config, process.env.B6_ONLYOFFICE_JWT_SECRET);
|
|
64
|
+
config.token = token;
|
|
65
|
+
|
|
66
|
+
// 返回配置和 API 地址
|
|
67
|
+
res.json({
|
|
68
|
+
config: config,
|
|
69
|
+
apiUrl: `${process.env.B6_ONLYOFFICE_SERVER_URL}web-apps/apps/api/documents/api.js`
|
|
70
|
+
});
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
exports.default = router;
|
|
@@ -139,7 +139,6 @@ function getFinalFormFields(inputFields) {
|
|
|
139
139
|
const temp = transformFormFields(amisField);
|
|
140
140
|
finalFormFields.push(temp)
|
|
141
141
|
})
|
|
142
|
-
|
|
143
142
|
return finalFormFields
|
|
144
143
|
}
|
|
145
144
|
|
|
@@ -160,7 +159,7 @@ function transformFormFields(amisField) {
|
|
|
160
159
|
|
|
161
160
|
if(amisField.type === 'steedos-field'){
|
|
162
161
|
const sfield = {
|
|
163
|
-
_id: amisField.config.name,
|
|
162
|
+
_id: amisField._id || amisField.config.name,
|
|
164
163
|
code: amisField.config.name,
|
|
165
164
|
name: amisField.config.label,
|
|
166
165
|
is_wide: amisField.config.is_wide,
|
|
@@ -188,7 +187,7 @@ function transformFormFields(amisField) {
|
|
|
188
187
|
};
|
|
189
188
|
|
|
190
189
|
let formFieldsItem = {
|
|
191
|
-
_id: new ObjectId().toHexString(),
|
|
190
|
+
_id: amisField._id || new ObjectId().toHexString(),
|
|
192
191
|
code: amisField.name || amisField.title,
|
|
193
192
|
name: amisField.label || amisField.title,
|
|
194
193
|
is_wide: _.includes(amisField.className, "is_wide"),
|
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>文档预览</title>
|
|
6
|
+
<style>
|
|
7
|
+
body, html { margin: 0; padding: 0; height: 100%; overflow: hidden; }
|
|
8
|
+
#placeholder { height: 100%; }
|
|
9
|
+
</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="placeholder"></div>
|
|
13
|
+
<script>
|
|
14
|
+
async function loadEditor() {
|
|
15
|
+
// 获取当前 URL 里的 src 参数
|
|
16
|
+
const src = new URLSearchParams(window.location.search).get('src');
|
|
17
|
+
|
|
18
|
+
// 请求后端获取签名配置
|
|
19
|
+
const response = await fetch(`/api/workflow/office/preview?src=${encodeURIComponent(src)}`);
|
|
20
|
+
const data = await response.json();
|
|
21
|
+
|
|
22
|
+
// 动态加载 API 脚本
|
|
23
|
+
const script = document.createElement("script");
|
|
24
|
+
script.src = data.apiUrl;
|
|
25
|
+
script.onload = () => {
|
|
26
|
+
new DocsAPI.DocEditor("placeholder", data.config);
|
|
27
|
+
};
|
|
28
|
+
document.head.appendChild(script);
|
|
29
|
+
}
|
|
30
|
+
loadEditor();
|
|
31
|
+
</script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -505,6 +505,9 @@ tbody .color-priority-muted *{
|
|
|
505
505
|
color: #40a9ff
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
.steedos-instance-print-wrapper .liquid-amis-container{
|
|
509
|
+
width: 190mm;
|
|
510
|
+
}
|
|
508
511
|
|
|
509
512
|
@media (max-width: 768px){
|
|
510
513
|
/* 审批单详细页手机端附件预览按钮样式 */
|
|
@@ -717,6 +720,11 @@ tbody .color-priority-muted *{
|
|
|
717
720
|
border: none !important;
|
|
718
721
|
}
|
|
719
722
|
|
|
723
|
+
|
|
724
|
+
.page-page_instance_print .instance-approve-history{
|
|
725
|
+
width: 190mm;
|
|
726
|
+
}
|
|
727
|
+
|
|
720
728
|
/* 公共打印隐藏样式 */
|
|
721
729
|
@media print {
|
|
722
730
|
.no-print {
|
|
@@ -290,15 +290,21 @@ function convertTemplate(content, fields, isPrint) {
|
|
|
290
290
|
newContent = newContent.replace(/\{\{afFieldLabelText\s+name=["'](.+?)["']\}\}/g, '$1');
|
|
291
291
|
|
|
292
292
|
// --- Sub-phase 2d: Convert instanceSignText ---
|
|
293
|
-
newContent = newContent.replace(/\{\{>\s*instanceSignText\s+([
|
|
293
|
+
newContent = newContent.replace(/\{\{>\s*instanceSignText\s+([\s\S]+?)\}\}/g, (match, args) => {
|
|
294
294
|
const argMap = parseArgs(args);
|
|
295
295
|
const fieldName = argMap.name;
|
|
296
296
|
if (!fieldName) return match;
|
|
297
297
|
|
|
298
298
|
const fieldDef = findFieldDef(fields, fieldName);
|
|
299
299
|
let stepName = "";
|
|
300
|
+
|
|
301
|
+
let formula = argMap.field_formula;
|
|
300
302
|
if (fieldDef && fieldDef.formula) {
|
|
301
|
-
|
|
303
|
+
formula = fieldDef.formula;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (formula) {
|
|
307
|
+
const formulaMatch = formula.match(/signature\.traces\.([^}]+)/);
|
|
302
308
|
if (formulaMatch) {
|
|
303
309
|
stepName = formulaMatch[1].replace('}', '');
|
|
304
310
|
}
|