@steedos-labs/plugin-workflow 3.0.83 → 3.0.85
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/designer/dist/amis-renderer/amis-renderer.js +1 -1
- package/designer/dist/assets/{index-WAa42ctb.js → index-DpFVuLlI.js} +28 -28
- package/designer/dist/index.html +1 -1
- package/main/default/client/flow2_render.client.js +1 -1
- package/main/default/manager/uuflow_manager.js +61 -8
- package/main/default/objects/instances/buttons/instance_delete_many.button.yml +1 -1
- package/main/default/pages/flow_selector.page.amis.json +2 -2
- package/main/default/pages/instance_tasks_detail.page.amis.json +4 -7
- package/main/default/pages/page_instance_print.page.amis.json +2 -2
- package/main/default/routes/api_workflow_chart.router.js +384 -59
- package/main/default/routes/api_workflow_export.router.js +2 -1
- package/main/default/server/ejs/export_instances.ejs +12 -4
- package/main/default/test/test_getApproveValues.js +221 -0
- package/package.json +3 -2
- package/public/amis-renderer/amis-renderer.js +1 -1
package/designer/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="shortcut icon" type="image/svg+xml" href="/images/logo.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>designer</title>
|
|
8
|
-
<script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-DpFVuLlI.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-B56yvQDB.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -1398,23 +1398,54 @@ UUFlowManager.getApproveValues = async function (approve_values, permissions, fo
|
|
|
1398
1398
|
const form = await UUFlowManager.getForm(form_id);
|
|
1399
1399
|
const form_v = await UUFlowManager.getFormVersion(form, form_version);
|
|
1400
1400
|
|
|
1401
|
+
// 判断字段是否为公式字段。
|
|
1402
|
+
// 历史表单设计器保存时存在丢失 `formula` 属性的情况,此时公式表达式会被转存到
|
|
1403
|
+
// `default_value` 中,形如 "${...}"。这里同时兜底识别该形态,避免老数据下公式
|
|
1404
|
+
// 子字段被当成普通无权限字段误过滤,保存后刷新计算结果丢失。
|
|
1405
|
+
const isFormulaField = (f) => {
|
|
1406
|
+
if (!f) return false;
|
|
1407
|
+
if (f.formula) return true;
|
|
1408
|
+
const dv = f.default_value;
|
|
1409
|
+
return typeof dv === 'string' && /^\$\{[\s\S]+\}$/.test(dv);
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// 开启 STEEDOS_DEBUG 时,打印 getApproveValues 关键参数,便于定位线上
|
|
1413
|
+
// 「公式子字段保存后刷新丢值」类问题,运维无需临时改源码。
|
|
1414
|
+
const debugApproveValues = !!process.env.STEEDOS_DEBUG;
|
|
1415
|
+
if (debugApproveValues) {
|
|
1416
|
+
console.log('[getApproveValues] form_id=%s form_version=%s permissions=%j', form_id, form_version, permissions);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1401
1419
|
// Filter fields based on permissions
|
|
1402
1420
|
for (const field of form_v.fields || []) {
|
|
1403
1421
|
if (field.type === "section") {
|
|
1404
1422
|
// Handle section fields
|
|
1405
1423
|
for (const sectionField of field.fields || []) {
|
|
1406
|
-
if (!sectionField
|
|
1424
|
+
if (!isFormulaField(sectionField) && (permissions[sectionField.code] === null || permissions[sectionField.code] !== "editable")) {
|
|
1407
1425
|
delete approve_values[sectionField.code];
|
|
1408
1426
|
}
|
|
1409
1427
|
}
|
|
1410
|
-
} else if (field.type === "table" && !field
|
|
1428
|
+
} else if (field.type === "table" && !isFormulaField(field) && (permissions[field.code] === null || permissions[field.code] !== "editable")) {
|
|
1411
1429
|
// 子表字段处理:子表本身无编辑权限时,检查是否有子字段具有编辑权限
|
|
1412
1430
|
const childFields = field.fields || [];
|
|
1413
1431
|
const hasEditableChild = childFields.some(c => permissions[c.code] === "editable");
|
|
1432
|
+
if (debugApproveValues) {
|
|
1433
|
+
// 只摘取关键属性,避免 _amisField 等运行时副本属性刷屏
|
|
1434
|
+
const childSchemaSummary = childFields.map(c => ({
|
|
1435
|
+
code: c.code,
|
|
1436
|
+
type: c.type,
|
|
1437
|
+
formula: c.formula,
|
|
1438
|
+
default_value: c.default_value,
|
|
1439
|
+
isFormula: isFormulaField(c),
|
|
1440
|
+
permission: permissions[c.code]
|
|
1441
|
+
}));
|
|
1442
|
+
console.log('[getApproveValues] table field "%s" hasEditableChild=%s childFieldsSummary=%j approve_values_before=%j',
|
|
1443
|
+
field.code, hasEditableChild, childSchemaSummary, approve_values[field.code]);
|
|
1444
|
+
}
|
|
1414
1445
|
if (hasEditableChild && Array.isArray(approve_values[field.code])) {
|
|
1415
1446
|
// 保留可编辑子字段及公式子字段。公式字段为计算值(非用户输入),其结果依赖其他字段,
|
|
1416
|
-
//
|
|
1417
|
-
const editableChildCodes = new Set(childFields.filter(c => c
|
|
1447
|
+
// 必须随依赖字段一并持久化,否则保存后刷新会回退。与下方顶层字段 !isFormulaField(field) 保护逻辑对称一致。
|
|
1448
|
+
const editableChildCodes = new Set(childFields.filter(c => isFormulaField(c) || permissions[c.code] === "editable").map(c => c.code));
|
|
1418
1449
|
editableChildCodes.add('_id');
|
|
1419
1450
|
approve_values[field.code] = approve_values[field.code].map(row => {
|
|
1420
1451
|
const filtered = {};
|
|
@@ -1425,11 +1456,18 @@ UUFlowManager.getApproveValues = async function (approve_values, permissions, fo
|
|
|
1425
1456
|
}
|
|
1426
1457
|
return filtered;
|
|
1427
1458
|
});
|
|
1459
|
+
if (debugApproveValues) {
|
|
1460
|
+
console.log('[getApproveValues] table field "%s" kept child codes=%j approve_values_after=%j',
|
|
1461
|
+
field.code, Array.from(editableChildCodes), approve_values[field.code]);
|
|
1462
|
+
}
|
|
1428
1463
|
} else {
|
|
1429
1464
|
// 无可编辑子字段 → 整表删除
|
|
1430
1465
|
delete approve_values[field.code];
|
|
1466
|
+
if (debugApproveValues) {
|
|
1467
|
+
console.log('[getApproveValues] table field "%s" deleted (no editable child)', field.code);
|
|
1468
|
+
}
|
|
1431
1469
|
}
|
|
1432
|
-
} else if (!field
|
|
1470
|
+
} else if (!isFormulaField(field) && (permissions[field.code] === null || permissions[field.code] !== "editable")) {
|
|
1433
1471
|
// 普通字段:无编辑权限且非公式字段则删除
|
|
1434
1472
|
delete approve_values[field.code];
|
|
1435
1473
|
}
|
|
@@ -4692,12 +4730,16 @@ UUFlowManager.relocate = async function (instance_from_client, current_user_info
|
|
|
4692
4730
|
const space_id = instance.space;
|
|
4693
4731
|
const instance_id = last_trace.instance;
|
|
4694
4732
|
const inbox_users = instance.inbox_users || [];
|
|
4695
|
-
|
|
4733
|
+
let relocate_inbox_users = instance_from_client.relocate_inbox_users || [];
|
|
4696
4734
|
const relocate_comment = instance_from_client.relocate_comment;
|
|
4697
4735
|
const relocate_next_step = instance_from_client.relocate_next_step;
|
|
4698
4736
|
|
|
4699
|
-
|
|
4700
|
-
|
|
4737
|
+
if (_.isString(relocate_inbox_users)) {
|
|
4738
|
+
relocate_inbox_users = [relocate_inbox_users];
|
|
4739
|
+
} else if (!Array.isArray(relocate_inbox_users)) {
|
|
4740
|
+
relocate_inbox_users = [];
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4701
4743
|
const approve_users = [];
|
|
4702
4744
|
|
|
4703
4745
|
const flow = await UUFlowManager.getFlow(instance.flow);
|
|
@@ -4705,6 +4747,17 @@ UUFlowManager.relocate = async function (instance_from_client, current_user_info
|
|
|
4705
4747
|
const next_step_type = next_step.step_type;
|
|
4706
4748
|
const next_step_name = next_step.name;
|
|
4707
4749
|
|
|
4750
|
+
if (next_step_type === 'start' && relocate_inbox_users.length === 0) {
|
|
4751
|
+
relocate_inbox_users = await HandlersManager.getHandlers(instance_id, relocate_next_step, current_user);
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4754
|
+
if (next_step_type !== 'end' && (!relocate_inbox_users || relocate_inbox_users.length === 0)) {
|
|
4755
|
+
throw new Error('未指定下一步处理人');
|
|
4756
|
+
}
|
|
4757
|
+
|
|
4758
|
+
const not_in_inbox_users = inbox_users.filter(u => !relocate_inbox_users.includes(u));
|
|
4759
|
+
const new_inbox_users = relocate_inbox_users.filter(u => !inbox_users.includes(u));
|
|
4760
|
+
|
|
4708
4761
|
const current_setp = await UUFlowManager.getStep(instance, flow, last_trace.step);
|
|
4709
4762
|
const current_setp_type = current_setp.step_type;
|
|
4710
4763
|
|
|
@@ -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=\"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\n function escapeHtml(str) {\n return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"').replace(/'/g, ''');\n }\n const sidebarEl = document.getElementById('steedosFlowSelectorSidebarList');\n const contentEl = document.getElementById('contentContainer');\n const searchInput = document.getElementById('searchInput');\n\n // Optimization 2: lazy rendering state\n let _observer = null;\n let _currentGroups = [];\n\n // Render cards into a placeholder element for a single group (lazy)\n var ESTIMATED_CARD_HEIGHT_PX = 88;\n var LAZY_LOAD_MARGIN = '300px 0px';\n\n function fillCards(group, placeholder) {\n if (placeholder.dataset.rendered) return;\n placeholder.dataset.rendered = 'true';\n placeholder.style.minHeight = '';\n const gridFragment = document.createDocumentFragment();\n group.items.forEach(function(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-[transform,box-shadow] duration-200 ease-out 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' + (i < 12 ? ' animate-[fadeUpSpring_0.4s_cubic-bezier(0.16,1,0.3,1)_forwards]' : '');\n if (i < 12) { card.style.animationDelay = (i * 0.03) + 's'; card.style.opacity = '0'; }\n // Optimization 3: data-flow-id for event delegation\n card.dataset.flowId = flow.id;\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 card.innerHTML = '<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 + (isFav ? ' active-fav' : '') + '\" title=\"' + (isFav ? '取消收藏' : '加入收藏') + '\" data-flow-id=\"' + flow.id + '\"><svg class=\"h-5 w-5 transition-colors duration-300 ' + iconClass + '\" viewBox=\"0 0 24 24\"><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\" /></svg></div><div class=\"mr-4 flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[16px] font-bold ' + colorClass + '\">' + escapeHtml(firstChar) + '</div><div class=\"flex-1 pr-8 text-[15px] font-medium text-gray-900 line-clamp-3 leading-relaxed tracking-tight\" title=\"' + escapeHtml(flow.name) + '\">' + escapeHtml(flow.name) + '</div>';\n gridFragment.appendChild(card);\n });\n placeholder.appendChild(gridFragment);\n }\n\n // Optimization 3: single delegated click listener — handles both star-btn and card clicks\n contentEl.addEventListener('click', function(e) {\n const starBtn = e.target.closest('.star-btn');\n if (starBtn) {\n const flowId = starBtn.dataset.flowId;\n const isFav = AppState.favorites.includes(flowId);\n const newFavState = !isFav;\n if (newFavState) {\n starBtn.classList.add('active-fav', 'opacity-100');\n starBtn.querySelector('svg').setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-yellow-400 fill-current');\n starBtn.setAttribute('title', '取消收藏');\n } else {\n starBtn.classList.remove('active-fav', 'opacity-100');\n starBtn.querySelector('svg').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 starBtn.setAttribute('title', '加入收藏');\n }\n AppState.favorites = WorkflowService.toggleFavorite(flowId, newFavState);\n setTimeout(function() { renderUI(searchInput.value); }, 300);\n return;\n }\n const card = e.target.closest('[data-flow-id]');\n if (card && !card.classList.contains('star-btn')) {\n if (card.dataset.loading === 'true') return;\n card.dataset.loading = 'true';\n card.style.pointerEvents = 'none';\n card.style.opacity = '0.6';\n card.innerHTML = '<div class=\"flex items-center justify-center w-full gap-2 text-gray-400 text-sm\"><svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg><span>正在创建...</span></div>';\n setTimeout(function() {\n data._scoped.doAction([\n { \"actionType\": \"broadcast\", \"args\": { \"eventName\": \"flows.selected\" }, \"data\": { \"value\": card.dataset.flowId } }\n ]);\n }, 50);\n }\n });\n\n async function init() {\n try {\n const result = await WorkflowService.getData();\n AppState.allFlows = result.flows;\n AppState.categories = result.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 matchKeywords(name, filterText) {\n if (!filterText) return true;\n var keywords = filterText.toLowerCase().split(/\\s+/).filter(function(k) { return k.length > 0; });\n if (keywords.length === 0) return true;\n var lowerName = name.toLowerCase();\n return keywords.every(function(k) { return lowerName.includes(k); });\n }\n\n function renderUI(filterText) {\n filterText = filterText || \"\";\n // Disconnect previous IntersectionObserver before rebuilding DOM\n if (_observer) { _observer.disconnect(); _observer = null; }\n sidebarEl.innerHTML = \"\";\n contentEl.innerHTML = \"\";\n\n const isSearching = filterText.length > 0;\n const groups = [];\n\n const favFlows = AppState.allFlows.filter(function(f) {\n return AppState.favorites.includes(f.id) && matchKeywords(f.name, filterText);\n });\n if (favFlows.length > 0) {\n groups.push({ id: 'fav', name: \"我的收藏\", items: favFlows, isFav: true });\n }\n\n AppState.categories.forEach(function(cat) {\n const items = AppState.allFlows.filter(function(f) {\n return f.categoryId === cat._id && matchKeywords(f.name, filterText);\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(function(f) {\n return !AppState.categories.find(function(c) { return c._id === f.categoryId; }) &&\n matchKeywords(f.name, filterText);\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 _currentGroups = groups;\n const contentFragment = document.createDocumentFragment();\n const 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 const activeClass = \"bg-[#007AFF] text-white shadow-sm font-medium\";\n const inactiveClass = \"text-gray-700 hover:bg-black/5 active:bg-black/10\";\n\n groups.forEach(function(group, index) {\n const groupId = 'group-' + group.id;\n const navItem = document.createElement('div');\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 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 = (function(grp, gId) {\n return function() {\n Array.from(sidebarEl.children).forEach(function(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 const section = document.getElementById(gId);\n if (section) {\n // Optimization 2: force-render target section before scrolling to avoid blank placeholder\n const ph = section.querySelector('.card-placeholder');\n if (ph && !ph.dataset.rendered) {\n fillCards(grp, ph);\n if (_observer) _observer.unobserve(ph);\n }\n const container = document.getElementById('mainContentScroll');\n if (container) {\n const targetTop = section.getBoundingClientRect().top;\n const containerTop = container.getBoundingClientRect().top;\n container.scrollTo({ top: container.scrollTop + targetTop - containerTop - 16, behavior: 'smooth' });\n }\n }\n };\n })(group, groupId);\n sidebarEl.appendChild(navItem);\n\n // Optimization 2: render section header + placeholder (cards filled lazily)\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 pb-2 text-xl font-bold tracking-tight text-left border-b border-gray-100 ' + headerColor + '\">' + group.name + '</div>';\n const placeholder = document.createElement('div');\n placeholder.className = 'card-placeholder grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4';\n // Pre-size the placeholder to avoid layout shift while cards aren't rendered yet\n placeholder.style.minHeight = (Math.ceil(group.items.length / 4) * ESTIMATED_CARD_HEIGHT_PX) + 'px';\n section.appendChild(placeholder);\n contentFragment.appendChild(section);\n });\n\n contentEl.appendChild(contentFragment);\n\n // Optimization 2: set up IntersectionObserver to fill cards as groups scroll into view\n const scrollContainer = document.getElementById('mainContentScroll');\n _observer = new IntersectionObserver(function(entries) {\n entries.forEach(function(entry) {\n if (entry.isIntersecting && entry.target.isConnected && !entry.target.dataset.rendered) {\n const ph = entry.target;\n const section = ph.parentElement;\n if (section && section.id) {\n const gId = section.id.replace('group-', '');\n const grp = _currentGroups.find(function(g) { return String(g.id) === gId; });\n if (grp) {\n fillCards(grp, ph);\n if (_observer) _observer.unobserve(ph);\n }\n }\n }\n });\n }, { root: scrollContainer, rootMargin: LAZY_LOAD_MARGIN });\n\n contentEl.querySelectorAll('.card-placeholder').forEach(function(p) {\n _observer.observe(p);\n });\n }\n\n // Optimization 4: Chinese IME compositionstart/compositionend prevents stutter during composition\n let _isComposing = false;\n let _searchTimer = null;\n searchInput.addEventListener('compositionstart', function() { _isComposing = true; });\n searchInput.addEventListener('compositionend', function(e) {\n _isComposing = false;\n clearTimeout(_searchTimer);\n renderUI(e.target.value.trim());\n });\n searchInput.addEventListener('input', function(e) {\n if (_isComposing) return;\n clearTimeout(_searchTimer);\n _searchTimer = setTimeout(function() { renderUI(e.target.value.trim()); }, 300);\n });\n\n init();\n</script>\n\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=\"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 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\n function escapeHtml(str) {\n return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"').replace(/'/g, ''');\n }\n const sidebarEl = document.getElementById('steedosFlowSelectorSidebarList');\n const contentEl = document.getElementById('contentContainer');\n const searchInput = document.getElementById('searchInput');\n\n // Optimization 2: lazy rendering state\n let _observer = null;\n let _currentGroups = [];\n\n // Render cards into a placeholder element for a single group (lazy)\n var ESTIMATED_CARD_HEIGHT_PX = 88;\n var LAZY_LOAD_MARGIN = '300px 0px';\n\n function fillCards(group, placeholder) {\n if (placeholder.dataset.rendered) return;\n placeholder.dataset.rendered = 'true';\n placeholder.style.minHeight = '';\n const gridFragment = document.createDocumentFragment();\n group.items.forEach(function(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-[transform,box-shadow] duration-200 ease-out 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' + (i < 12 ? ' animate-[fadeUpSpring_0.4s_cubic-bezier(0.16,1,0.3,1)_forwards]' : '');\n if (i < 12) { card.style.animationDelay = (i * 0.03) + 's'; card.style.opacity = '0'; }\n // Optimization 3: data-flow-id for event delegation\n card.dataset.flowId = flow.id;\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 card.innerHTML = '<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 + (isFav ? ' active-fav' : '') + '\" title=\"' + (isFav ? '取消收藏' : '加入收藏') + '\" data-flow-id=\"' + flow.id + '\"><svg class=\"h-5 w-5 transition-colors duration-300 ' + iconClass + '\" viewBox=\"0 0 24 24\"><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\" /></svg></div><div class=\"mr-4 flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-[16px] font-bold ' + colorClass + '\">' + escapeHtml(firstChar) + '</div><div class=\"flex-1 pr-8 text-[15px] font-medium text-gray-900 line-clamp-3 leading-relaxed tracking-tight\" title=\"' + escapeHtml(flow.name) + '\">' + escapeHtml(flow.name) + '</div>';\n gridFragment.appendChild(card);\n });\n placeholder.appendChild(gridFragment);\n }\n\n // Optimization 3: single delegated click listener — handles both star-btn and card clicks\n contentEl.addEventListener('click', function(e) {\n const starBtn = e.target.closest('.star-btn');\n if (starBtn) {\n const flowId = starBtn.dataset.flowId;\n const isFav = AppState.favorites.includes(flowId);\n const newFavState = !isFav;\n if (newFavState) {\n starBtn.classList.add('active-fav', 'opacity-100');\n starBtn.querySelector('svg').setAttribute('class', 'h-5 w-5 transition-colors duration-300 text-yellow-400 fill-current');\n starBtn.setAttribute('title', '取消收藏');\n } else {\n starBtn.classList.remove('active-fav', 'opacity-100');\n starBtn.querySelector('svg').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 starBtn.setAttribute('title', '加入收藏');\n }\n AppState.favorites = WorkflowService.toggleFavorite(flowId, newFavState);\n setTimeout(function() { renderUI(searchInput.value); }, 300);\n return;\n }\n const card = e.target.closest('[data-flow-id]');\n if (card && !card.classList.contains('star-btn')) {\n if (card.dataset.loading === 'true') return;\n card.dataset.loading = 'true';\n card.style.pointerEvents = 'none';\n card.style.opacity = '0.6';\n card.innerHTML = '<div class=\"flex items-center justify-center w-full gap-2 text-gray-400 text-sm\"><svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg><span>正在创建...</span></div>';\n setTimeout(function() {\n data._scoped.doAction([\n { \"actionType\": \"broadcast\", \"args\": { \"eventName\": \"flows.selected\" }, \"data\": { \"value\": card.dataset.flowId } }\n ]);\n }, 50);\n }\n });\n\n async function init() {\n try {\n const result = await WorkflowService.getData();\n AppState.allFlows = result.flows;\n AppState.categories = result.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 matchKeywords(name, filterText) {\n if (!filterText) return true;\n var keywords = filterText.toLowerCase().split(/\\s+/).filter(function(k) { return k.length > 0; });\n if (keywords.length === 0) return true;\n var lowerName = name.toLowerCase();\n return keywords.every(function(k) { return lowerName.includes(k); });\n }\n\n function renderUI(filterText) {\n filterText = filterText || \"\";\n // Disconnect previous IntersectionObserver before rebuilding DOM\n if (_observer) { _observer.disconnect(); _observer = null; }\n sidebarEl.innerHTML = \"\";\n contentEl.innerHTML = \"\";\n\n const isSearching = filterText.length > 0;\n const groups = [];\n\n const favFlows = AppState.allFlows.filter(function(f) {\n return AppState.favorites.includes(f.id) && matchKeywords(f.name, filterText);\n });\n if (favFlows.length > 0) {\n groups.push({ id: 'fav', name: \"我的收藏\", items: favFlows, isFav: true });\n }\n\n AppState.categories.forEach(function(cat) {\n const items = AppState.allFlows.filter(function(f) {\n return f.categoryId === cat._id && matchKeywords(f.name, filterText);\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(function(f) {\n return !AppState.categories.find(function(c) { return c._id === f.categoryId; }) &&\n matchKeywords(f.name, filterText);\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 _currentGroups = groups;\n const contentFragment = document.createDocumentFragment();\n const navBase = \"group flex cursor-pointer items-center justify-between rounded-md px-3 py-2 text-[14px] select-none\";\n const activeClass = \"bg-[#007AFF] text-white shadow-sm font-medium\";\n const inactiveClass = \"text-gray-700 hover:bg-black/5 active:bg-black/10\";\n\n groups.forEach(function(group, index) {\n const groupId = 'group-' + group.id;\n const navItem = document.createElement('div');\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 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 = (function(grp, gId) {\n return function() {\n Array.from(sidebarEl.children).forEach(function(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 const section = document.getElementById(gId);\n if (!section) return;\n const container = document.getElementById('mainContentScroll');\n if (!container) return;\n // Plan A: defer heavy DOM work (fillCards for all groups + raf converge loop)\n // to the next animation frame so the browser first paints the new active\n // className on the clicked left-side category. Without this, hundreds of\n // card inserts happen synchronously inside the click handler and the active\n // state appears stuck on the previous item for several seconds.\n var _runHeavy = function() {\n // Plan B2: batch-render cards across frames with a per-frame time budget so the\n // main thread stays responsive. A render version token aborts any in-flight loop\n // when the user clicks a different category or re-opens the modal. The scroll\n // converge logic from v8 runs in the same loop, re-measuring each frame so the\n // target stays pinned even as before/after groups render and grow scrollHeight.\n window._flowSelectorRenderVersion = (window._flowSelectorRenderVersion || 0) + 1;\n var myVer = window._flowSelectorRenderVersion;\n\n // Fast path: if every group already has its cards rendered, skip the batched\n // render queue and the raf converge loop entirely. This handles every click\n // after the first one, eliminating the perceived lag on repeat clicks.\n var allRendered = true;\n for (var fk = 0; fk < _currentGroups.length; fk++) {\n var fgEl = document.getElementById(\"group-\" + _currentGroups[fk].id);\n if (!fgEl) continue;\n var fphEl = fgEl.querySelector(\".card-placeholder\");\n if (fphEl && !fphEl.dataset.rendered) { allRendered = false; break; }\n }\n if (allRendered) {\n var fastDelta = section.getBoundingClientRect().top - container.getBoundingClientRect().top;\n var fastMax = container.scrollHeight - container.clientHeight;\n var fastTarget = container.scrollTop + (fastDelta - 16);\n if (fastTarget < 0) fastTarget = 0;\n if (fastTarget > fastMax) fastTarget = fastMax;\n container.scrollTop = fastTarget;\n return;\n }\n\n \n if (_observer) {\n try { _observer.disconnect(); } catch (e) {}\n }\n \n // Build render queue: target first (so it has real content before scroll lands on it),\n // then before-groups in document order (these push target down, converge re-aligns),\n // then after-groups in document order (they do not affect target position).\n var targetIdx = -1;\n for (var k = 0; k < _currentGroups.length; k++) {\n if (_currentGroups[k].id === grp.id) { targetIdx = k; break; }\n }\n var queue = [];\n if (targetIdx >= 0) {\n queue.push(_currentGroups[targetIdx]);\n for (var k = 0; k < targetIdx; k++) queue.push(_currentGroups[k]);\n for (var k = targetIdx + 1; k < _currentGroups.length; k++) queue.push(_currentGroups[k]);\n }\n \n // Temporarily force scroll-behavior:auto so our direct scrollTop assignments are not\n // animated by the CSS .scroll-smooth class. Restored when the loop ends.\n var savedScrollBehavior = container.style.scrollBehavior;\n container.style.scrollBehavior = \"auto\";\n var maxFrames = 300; // ~5s ceiling\n var renderBudgetMs = 6; // per-frame DOM mutation budget\n var lastSetScrollTop = -1; // detect user manual scroll mid-loop\n \n var finish = function() {\n container.style.scrollBehavior = savedScrollBehavior;\n };\n \n var step = function() {\n // Abort if a newer click has taken over.\n if (myVer !== window._flowSelectorRenderVersion) { finish(); return; }\n if (maxFrames-- <= 0) { finish(); return; }\n \n // If user manually scrolled (wheel/touch/keyboard), stop fighting them. We keep\n // rendering remaining groups (they are still expected to appear) but stop adjusting\n // scrollTop.\n var userScrolled = (lastSetScrollTop >= 0 && Math.abs(container.scrollTop - lastSetScrollTop) > 4);\n \n // Render groups up to the per-frame budget.\n var nowFn = function() { return (typeof performance !== \"undefined\" && performance.now) ? performance.now() : 0; };\n var hasPerf = (typeof performance !== \"undefined\" && !!performance.now);\n var deadline = hasPerf ? nowFn() + renderBudgetMs : 0;\n while (queue.length > 0 && (!hasPerf || nowFn() < deadline)) {\n var g = queue.shift();\n var gEl = document.getElementById(\"group-\" + g.id);\n if (!gEl) continue;\n var phEl = gEl.querySelector(\".card-placeholder\");\n if (phEl && !phEl.dataset.rendered) {\n fillCards(g, phEl);\n }\n if (!hasPerf) break; // no perf API: 1 per frame fallback\n }\n \n // Adjust scroll toward target unless user took over.\n var doneInRange = false;\n var doneAtMax = false;\n if (!userScrolled) {\n var delta = section.getBoundingClientRect().top - container.getBoundingClientRect().top;\n var maxScroll = container.scrollHeight - container.clientHeight;\n var target = container.scrollTop + (delta - 16);\n if (target < 0) target = 0;\n doneInRange = Math.abs(delta - 16) <= 1;\n doneAtMax = target > maxScroll && container.scrollTop >= maxScroll - 1;\n if (!doneInRange && !doneAtMax) {\n if (target > maxScroll) container.scrollTop = maxScroll;\n else container.scrollTop = target;\n lastSetScrollTop = container.scrollTop;\n }\n }\n \n // Loop end condition: queue drained AND scroll converged (or user took control).\n if (queue.length === 0 && (userScrolled || doneInRange || doneAtMax)) {\n finish();\n return;\n }\n \n if (typeof requestAnimationFrame === \"function\") requestAnimationFrame(step);\n else setTimeout(step, 16);\n };\n \n if (typeof requestAnimationFrame === \"function\") requestAnimationFrame(step);\n else setTimeout(step, 16);\n };\n if (typeof requestAnimationFrame === \"function\") {\n requestAnimationFrame(_runHeavy);\n } else {\n setTimeout(_runHeavy, 0);\n }\n };\n })(group, groupId);\n sidebarEl.appendChild(navItem);\n\n // Optimization 2: render section header + placeholder (cards filled lazily)\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 pb-2 text-xl font-bold tracking-tight text-left border-b border-gray-100 ' + headerColor + '\">' + group.name + '</div>';\n const placeholder = document.createElement('div');\n placeholder.className = 'card-placeholder grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4';\n // Pre-size the placeholder to avoid layout shift while cards aren't rendered yet\n placeholder.style.minHeight = (Math.ceil(group.items.length / 4) * ESTIMATED_CARD_HEIGHT_PX) + 'px';\n section.appendChild(placeholder);\n contentFragment.appendChild(section);\n });\n\n contentEl.appendChild(contentFragment);\n\n // Optimization 2: set up IntersectionObserver to fill cards as groups scroll into view\n const scrollContainer = document.getElementById('mainContentScroll');\n _observer = new IntersectionObserver(function(entries) {\n entries.forEach(function(entry) {\n if (entry.isIntersecting && entry.target.isConnected && !entry.target.dataset.rendered) {\n const ph = entry.target;\n const section = ph.parentElement;\n if (section && section.id) {\n const gId = section.id.replace('group-', '');\n const grp = _currentGroups.find(function(g) { return String(g.id) === gId; });\n if (grp) {\n fillCards(grp, ph);\n if (_observer) _observer.unobserve(ph);\n }\n }\n }\n });\n }, { root: scrollContainer, rootMargin: LAZY_LOAD_MARGIN });\n\n contentEl.querySelectorAll('.card-placeholder').forEach(function(p) {\n _observer.observe(p);\n });\n }\n\n // Optimization 4: Chinese IME compositionstart/compositionend prevents stutter during composition\n let _isComposing = false;\n let _searchTimer = null;\n searchInput.addEventListener('compositionstart', function() { _isComposing = true; });\n searchInput.addEventListener('compositionend', function(e) {\n _isComposing = false;\n clearTimeout(_searchTimer);\n renderUI(e.target.value.trim());\n });\n searchInput.addEventListener('input', function(e) {\n if (_isComposing) return;\n clearTimeout(_searchTimer);\n _searchTimer = setTimeout(function() { renderUI(e.target.value.trim()); }, 300);\n });\n\n init();\n</script>\n\n",
|
|
4
4
|
"className": "h-full"
|
|
5
|
-
}
|
|
5
|
+
}
|
|
@@ -25,19 +25,17 @@
|
|
|
25
25
|
"type": "service",
|
|
26
26
|
"id": "u:d6db0c84f150",
|
|
27
27
|
"body": {},
|
|
28
|
-
"messages": {
|
|
29
|
-
},
|
|
28
|
+
"messages": {},
|
|
30
29
|
"schemaApi": {
|
|
31
30
|
"url": "${context.rootUrl}/graphql?recordId=${recordId}",
|
|
32
31
|
"method": "post",
|
|
33
|
-
"messages": {
|
|
34
|
-
},
|
|
32
|
+
"messages": {},
|
|
35
33
|
"dataType": "json",
|
|
36
34
|
"headers": {
|
|
37
35
|
"Authorization": "Bearer ${context.tenantId},${context.authToken}"
|
|
38
36
|
},
|
|
39
37
|
"requestAdaptor": "\nconst { recordId } = api.body;\napi.data = {\n query: `{instance_task:instance_tasks__findOne(id: \"${recordId}\"){_id,instance,is_read}}`\n};\n\nreturn api;",
|
|
40
|
-
"adaptor": "if(!payload.data.instance_task){
|
|
38
|
+
"adaptor": "if(!payload.data.instance_task){\n // 同步清掉 PageRecordDetail.tsx 加上的 loading 遮罩和滑动动画\n try{\n document.body.classList.remove('steedos-detail-loading');\n if(window.$){window.$('.page-object-detail-wrapper').removeClass('slide-out-bottom').addClass('slide-in-top');}\n }catch(e){}\n // 简易 i18n:plugins 端 adaptor 无法 import i18next,按浏览器语言切换中英文\n var isZh=(navigator.language||'').toLowerCase().indexOf('zh')===0;\n var notFoundText=isZh?'\\u65e0\\u6cd5\\u627e\\u5230\\u8bb0\\u5f55':'No records found.';\n var backText=isZh?'\\u8fd4\\u56de\\u5217\\u8868':'Back to list';\n // 警示三角图标(Heroicons ExclamationTriangle,amber-500 描边)\n var svgHtml=\"<span class='empty-record-icon' style='display:block;margin-bottom:16px'><svg xmlns='http://www.w3.org/2000/svg' width='72' height='72' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='#f59e0b'><path stroke-linecap='round' stroke-linejoin='round' d='M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.732 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z'/></svg></span>\";\n // 返回按钮统一调用全局 window.goBack(),参考 header.js 内置详情返回按钮\n var nfBody=[\n {type:'html',html:svgHtml},\n {type:'tpl',tpl:notFoundText,className:'empty-record-title text-xl text-gray-800 font-medium mb-4'},\n {type:'button',label:backText,level:'primary',className:'empty-record-back-btn',onEvent:{click:{actions:[{actionType:'custom',script:'window.goBack && window.goBack()'}]}}}\n ];\n // isRead:true 防止下游 set_have_read ajax 触发\n return {data:{type:'wrapper',className:'flex flex-col items-center justify-center bg-white p-8 text-center',style:{minHeight:'100vh'},body:nfBody,id:'u:notfound-instance-task',isRead:true}};\n}\nlet boxName = api.body.side_listview_id === 'inbox' ? 'inbox' : 'outbox';\npayload.data = {\n instanceId: payload.data.instance_task.instance,\n boxName: boxName,\n isRead: payload.data.instance_task.is_read\n};\nreturn {data: {'type':'wrapper','className':'p-0 h-full','body':[{'type':'steedos-instance-detail','id':'u:40052b3812c1','label':'Instance Detail','instanceId':payload.data.instanceId,'boxName':context.side_listview_id}],'id':'u:829a40757f0a'}};"
|
|
41
39
|
},
|
|
42
40
|
"onEvent": {
|
|
43
41
|
"fetchSchemaInited": {
|
|
@@ -64,8 +62,7 @@
|
|
|
64
62
|
"regions": [
|
|
65
63
|
"body"
|
|
66
64
|
],
|
|
67
|
-
"data": {
|
|
68
|
-
},
|
|
65
|
+
"data": {},
|
|
69
66
|
"className": "steedos-instance-wrapper",
|
|
70
67
|
"id": "u:d37465183f56",
|
|
71
68
|
"bodyClassName": "p-0 flex flex-1 overflow-hidden h-full",
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
{
|
|
202
202
|
"id": "u:259e5e73a3e2",
|
|
203
203
|
"type": "tpl",
|
|
204
|
-
"tpl": "无权限",
|
|
204
|
+
"tpl": "${recordNotFound ? '无法找到记录' : '无权限'}",
|
|
205
205
|
"visibleOn": "${permission === false}"
|
|
206
206
|
}
|
|
207
207
|
],
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
"method": "get",
|
|
223
223
|
"data": {},
|
|
224
224
|
"requestAdaptor": "",
|
|
225
|
-
"adaptor": "",
|
|
225
|
+
"adaptor": "if (payload && payload.errors && payload.errors.length) { return {data: {permission: false, recordNotFound: true}}; } return payload;",
|
|
226
226
|
"messages": {},
|
|
227
227
|
"sendOn": "${recordId}"
|
|
228
228
|
},
|