// ==UserScript== // @name 纷享iPaaS增强工具 // @namespace https://docs.scriptcat.org/ // @version 0.3.5 // @description 批量删除|新标签页打开 // @author Kayu Tse // @match https://crm.ceshi112.com/XV/UI/manage* // @match https://www.fxiaoke.com/XV/UI/manage* // @run-at document-idle // @grant none // ==/UserScript== (function () { 'use strict'; const PAGE_HASH = '#admin/erp-ipaas/workflow'; const API_BASE = '/FHH/EM1HERPIPAAS/cep'; const POSITION_KEY = 'ipaas-plus-toolbar-position-v4'; const OLD_POSITION_KEY = 'ipaas-plus-toolbar-position'; const OLD_POSITION_KEY_V2 = 'ipaas-plus-toolbar-position-v2'; const OLD_POSITION_KEY_V3 = 'ipaas-plus-toolbar-position-v3'; const SELECTED_CLASS = 'ipaas-plus-row-selected'; const CHECKBOX_CLASS = 'ipaas-plus-row-checkbox'; const API_NAME_CLASS = 'ipaas-plus-api-name'; const NAME_WRAP_CLASS = 'ipaas-plus-name-wrap'; const EDIT_LINK_CLASS = 'ipaas-plus-edit-link'; const REFRESH_BUTTON_ID = 'ipaas-plus-refresh-button'; const OPEN_EDIT_QUERY_KEY = 'openEditApiName'; const ORIGINAL_TITLE_KEY = 'ipaas-plus-original-title'; const state = { enabled: false, deleting: false, stopped: false, selectedApiNames: new Set(), message: '', customToolbarPosition: false, observer: null, renderTimer: null, }; function isWorkflowPage() { return location.href.includes('/XV/UI/manage') && location.hash.startsWith(PAGE_HASH); } function getRows() { return Array.from(document.querySelectorAll('tr.el-table__row')); } function getWorkflowTableVm() { const root = document.querySelector('.workflow_table'); if (!root) return null; const stack = [root]; while (stack.length > 0) { const element = stack.shift(); if (element.__vue__?.streamList) return element.__vue__; stack.push(...element.children); } return null; } function getWorkflowPageVm() { let vm = getWorkflowTableVm(); while (vm) { if (typeof vm.queryIntegrationStreamList === 'function') return vm; vm = vm.$parent; } const root = document.querySelector('.workflow__page'); if (!root) return null; const stack = [root]; while (stack.length > 0) { const element = stack.shift(); let current = element.__vue__; while (current) { if (typeof current.queryIntegrationStreamList === 'function') return current; current = current.$parent; } stack.push(...element.children); } return null; } function getWorkflowList() { return getWorkflowTableVm()?.streamList || []; } function getRowData(row) { const rows = getRows(); const list = getWorkflowList(); const index = rows.indexOf(row); const current = list[index]; if (current && getRowName(row) === (current.name || '')) return current; const name = getRowName(row); return list.find((item) => item.name === name && item.statusName === getRowStatus(row)) || null; } function getRowName(row) { return row.querySelector('.el-link--inner')?.textContent?.trim() || ''; } function getRowStatus(row) { return row.querySelector('.status_tag')?.textContent?.trim() || ''; } function getSelectableRows() { return getRows().filter((row) => getRowData(row)?.apiName); } function getSelectedItems() { return getSelectableRows() .map((row) => getRowData(row)) .filter((item) => item?.apiName && state.selectedApiNames.has(item.apiName)); } function getFsToken() { const resource = performance .getEntriesByType('resource') .map((entry) => entry.name) .find((url) => url.includes('_fs_token=')); if (resource) return new URL(resource).searchParams.get('_fs_token') || ''; return window.FS?.config?._fs_token || window.FS?._fs_token || window.CRM?.token || ''; } function buildApiUrl(path) { const url = new URL(`${API_BASE}${path}`, location.origin); const token = getFsToken(); if (token) url.searchParams.set('_fs_token', token); url.searchParams.set('traceId', `IPAAS-PLUS-${Date.now()}-${Math.floor(Math.random() * 100000)}`); return url.toString(); } async function requestCep(path, data) { const response = await fetch(buildApiUrl(path), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json;charset=UTF-8', Accept: 'application/json, text/plain, */*', }, body: JSON.stringify(data || {}), }); const text = await response.text(); const payload = text ? JSON.parse(text) : {}; if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`); return payload.Value || payload.value || payload; } async function deleteWorkflow(item) { if (!item?.apiName) throw new Error(`apiName not found: ${item?.name || 'unknown workflow'}`); const result = await requestCep('/flowSource/delete', { apiName: item.apiName }); if (result.code !== 's106240000') throw new Error(result.message || `Unexpected response code: ${result.code}`); return result; } function refreshWorkflowList() { const vm = getWorkflowPageVm(); if (vm) { vm.queryIntegrationStreamList(vm.searchQuery || {}); return true; } const searchButton = Array.from(document.querySelectorAll('.workflow_table_search__content button')).find((button) => button.textContent.trim() === '搜索'); searchButton?.click(); return !!searchButton; } function scheduleRender() { clearTimeout(state.renderTimer); state.renderTimer = setTimeout(render, 120); } function ensureStyle() { if (document.getElementById('ipaas-plus-style')) return; const style = document.createElement('style'); style.id = 'ipaas-plus-style'; style.textContent = ` .ipaas-plus-toolbar { position: fixed; z-index: 9999; display: flex; align-items: center; gap: 6px; padding: 8px; min-height: 28px; background: #ffffff; border: 1px solid #dcdfe6; border-radius: 6px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); color: #303133; font: 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; user-select: none; cursor: grab; } .ipaas-plus-toolbar.dragging { cursor: grabbing; } .ipaas-plus-toolbar:not(.expanded) { padding: 4px 6px; opacity: 0.78; } .ipaas-plus-toolbar:not(.expanded):hover { opacity: 1; } .ipaas-plus-toolbar button { height: 24px; padding: 0 8px; border: 1px solid #dcdfe6; border-radius: 4px; background: #ffffff; color: #303133; cursor: pointer; } .ipaas-plus-toolbar button.entry { border-color: transparent; background: transparent; color: #606266; } .ipaas-plus-toolbar button.entry:hover { color: #409eff; } .ipaas-plus-toolbar button.primary { border-color: #f56c6c; background: #f56c6c; color: #ffffff; } .ipaas-plus-toolbar button:disabled { opacity: 0.45; cursor: not-allowed; } .ipaas-plus-summary { min-width: 120px; white-space: nowrap; color: #606266; } .ipaas-plus-row-checkbox { flex: 0 0 auto; width: 14px; height: 14px; margin: 0; cursor: pointer; } .ipaas-plus-name-wrap { display: inline-flex; flex-direction: column; min-width: 0; } .ipaas-plus-api-name { margin-top: 3px; color: #909399; font-size: 11px; line-height: 1.3; font-family: Consolas, "SFMono-Regular", Menlo, Monaco, monospace; word-break: break-all; } .ipaas-plus-api-name .ipaas-plus-edit-link { display: inline-flex; align-items: center; justify-content: center; margin-left: 6px; width: 18px; height: 18px; border: 1px solid transparent; border-radius: 4px; background: transparent; color: #909399; font-size: 12px; font-weight: bold; line-height: 1; cursor: pointer; text-decoration: none; vertical-align: middle; opacity: 0; transition: opacity 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; } .ipaas-plus-api-name:hover .ipaas-plus-edit-link { opacity: 1; } .ipaas-plus-edit-link:hover { background: #f5f7fa; border-color: #e4e7ed; color: #606266; } .ipaas-plus-row-selected > td { background: #fff1f0 !important; } #ipaas-plus-refresh-button { flex: 0 0 auto; margin: 0 0 0 6px; width: 28px; height: 28px; padding: 0; border: 1px solid #dcdfe6; border-radius: 4px; background: #ffffff; color: #606266; cursor: pointer; font-size: 16px; line-height: 26px; vertical-align: middle; } #ipaas-plus-refresh-button:hover { color: #409eff; border-color: #c6e2ff; background: #ecf5ff; } #ipaas-plus-refresh-button.loading { opacity: 0.65; cursor: wait; } `; document.head.appendChild(style); } function ensureToolbar() { let toolbar = document.getElementById('ipaas-plus-toolbar'); if (toolbar) return toolbar; toolbar = document.createElement('div'); toolbar.id = 'ipaas-plus-toolbar'; toolbar.className = 'ipaas-plus-toolbar'; toolbar.innerHTML = ` `; toolbar.addEventListener('click', (event) => { const action = event.target?.dataset?.action; if (!action) return; if (action === 'toggle') { state.enabled = !state.enabled; if (!state.enabled) { state.selectedApiNames.clear(); state.message = ''; } render(); } if (action === 'select-all') { getSelectableRows().forEach((row) => { const data = getRowData(row); if (data?.apiName) state.selectedApiNames.add(data.apiName); }); render(); } if (action === 'clear') { state.selectedApiNames.clear(); render(); } if (action === 'delete') { startBatchDelete(); } if (action === 'stop') { state.stopped = true; updateSummary('当前项完成后停止...'); } }); document.body.appendChild(toolbar); if (!restoreToolbarPosition(toolbar)) { setDefaultToolbarPosition(toolbar); window.requestAnimationFrame(() => setDefaultToolbarPosition(toolbar)); } enableToolbarDrag(toolbar); return toolbar; } function restoreToolbarPosition(toolbar) { try { localStorage.removeItem(OLD_POSITION_KEY); localStorage.removeItem(OLD_POSITION_KEY_V2); localStorage.removeItem(OLD_POSITION_KEY_V3); const position = JSON.parse(localStorage.getItem(POSITION_KEY) || 'null'); if (!position || typeof position.left !== 'number' || typeof position.top !== 'number') return false; toolbar.style.left = `${Math.max(8, position.left)}px`; toolbar.style.top = `${Math.max(8, position.top)}px`; toolbar.style.right = 'auto'; state.customToolbarPosition = true; return true; } catch (error) { console.warn('[CRM iPaaS Plus] Failed to restore toolbar position', error); return false; } } function setDefaultToolbarPosition(toolbar) { const anchor = document.querySelector('.workflow__header_content') || document.querySelector('.workflow__page .page_head') || document.querySelector('.workflow__content') || document.querySelector('.app-wrapper'); const rect = anchor?.getBoundingClientRect(); const fallbackLeft = window.innerWidth - toolbar.offsetWidth - 24; const fallbackTop = 92; const left = rect ? rect.right + 10 : fallbackLeft; const top = rect ? rect.top - Math.max(0, (toolbar.offsetHeight - rect.height) / 2) : fallbackTop; toolbar.style.left = `${Math.min(window.innerWidth - toolbar.offsetWidth - 8, Math.max(8, left))}px`; toolbar.style.top = `${Math.min(window.innerHeight - toolbar.offsetHeight - 8, Math.max(8, top))}px`; toolbar.style.right = 'auto'; } function enableToolbarDrag(toolbar) { let startX = 0; let startY = 0; let originLeft = 0; let originTop = 0; let dragging = false; toolbar.addEventListener('mousedown', (event) => { if (event.target.closest('button, input')) return; dragging = true; startX = event.clientX; startY = event.clientY; const rect = toolbar.getBoundingClientRect(); originLeft = rect.left; originTop = rect.top; toolbar.classList.add('dragging'); event.preventDefault(); }); document.addEventListener('mousemove', (event) => { if (!dragging) return; const nextLeft = Math.min(window.innerWidth - toolbar.offsetWidth - 8, Math.max(8, originLeft + event.clientX - startX)); const nextTop = Math.min(window.innerHeight - toolbar.offsetHeight - 8, Math.max(8, originTop + event.clientY - startY)); toolbar.style.left = `${nextLeft}px`; toolbar.style.top = `${nextTop}px`; toolbar.style.right = 'auto'; }); document.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; toolbar.classList.remove('dragging'); const rect = toolbar.getBoundingClientRect(); state.customToolbarPosition = true; localStorage.setItem(POSITION_KEY, JSON.stringify({ left: rect.left, top: rect.top })); }); } function updateSummary(text) { const summary = document.querySelector('#ipaas-plus-toolbar .ipaas-plus-summary'); if (summary) summary.textContent = text; } function ensureRefreshButton() { const searchInput = Array.from(document.querySelectorAll('.workflow_table_search__content input')).find((input) => input.placeholder === '搜索集成流名称'); const searchItem = searchInput?.closest('.search_item'); const searchCol = searchItem?.parentElement; if (!searchItem || !searchCol) return; let refreshButton = document.getElementById(REFRESH_BUTTON_ID); if (!refreshButton) { refreshButton = document.createElement('button'); refreshButton.id = REFRESH_BUTTON_ID; refreshButton.type = 'button'; refreshButton.title = '刷新列表'; refreshButton.textContent = '⟳'; refreshButton.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); refreshButton.classList.add('loading'); refreshButton.disabled = true; const refreshed = refreshWorkflowList(); if (!refreshed) console.warn('[CRM iPaaS Plus] Refresh entry not found'); window.setTimeout(() => { refreshButton.classList.remove('loading'); refreshButton.disabled = false; }, 800); }); } searchCol.style.display = 'flex'; searchCol.style.alignItems = 'center'; searchItem.style.flex = '1 1 auto'; searchItem.style.minWidth = '0'; if (refreshButton.parentElement !== searchCol || refreshButton.previousElementSibling !== searchItem) { searchItem.insertAdjacentElement('afterend', refreshButton); } } function syncRowCheckbox(row) { const firstCell = row.querySelector('td:first-child .cell'); if (!firstCell) return; const data = getRowData(row); const apiName = data?.apiName; let checkbox = firstCell.querySelector(`.${CHECKBOX_CLASS}`); if (!state.enabled || !apiName) { checkbox?.remove(); row.classList.remove(SELECTED_CLASS); return; } if (!checkbox) { checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = CHECKBOX_CLASS; checkbox.title = '选择后批量删除'; checkbox.addEventListener('click', (event) => event.stopPropagation()); checkbox.addEventListener('change', () => { const current = getRowData(row); if (!current?.apiName) return; if (checkbox.checked) state.selectedApiNames.add(current.apiName); else state.selectedApiNames.delete(current.apiName); render(); }); firstCell.insertBefore(checkbox, firstCell.firstChild); } checkbox.disabled = !apiName; checkbox.checked = !!apiName && state.selectedApiNames.has(apiName); row.classList.toggle(SELECTED_CLASS, checkbox.checked); } function syncRowApiName(row) { const firstCell = row.querySelector('td:first-child .cell'); if (!firstCell) return; const data = getRowData(row); const apiName = data?.apiName; ensureNameLayout(firstCell); let apiNameNode = firstCell.querySelector(`.${API_NAME_CLASS}`); if (!apiName) { apiNameNode?.remove(); firstCell.querySelector(`.${EDIT_LINK_CLASS}`)?.remove(); return; } if (!apiNameNode) { apiNameNode = document.createElement('div'); apiNameNode.className = API_NAME_CLASS; firstCell.querySelector(`.${NAME_WRAP_CLASS}`)?.appendChild(apiNameNode); } const apiNameTextNode = apiNameNode.firstChild; if (!apiNameTextNode || apiNameTextNode.nodeType !== Node.TEXT_NODE || apiNameTextNode.textContent !== apiName) { apiNameNode.textContent = apiName; } // 在 apiName 右侧添加“新标签页编辑”按钮 let editLink = apiNameNode.querySelector(`.${EDIT_LINK_CLASS}`); if (!editLink) { editLink = document.createElement('a'); editLink.className = EDIT_LINK_CLASS; editLink.title = '在新标签页中打开编辑'; editLink.textContent = '↗'; editLink.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); const current = getRowData(row); if (!current?.apiName) return; const url = new URL(location.href); url.searchParams.set(OPEN_EDIT_QUERY_KEY, current.apiName); window.open(url.toString(), '_blank'); }); apiNameNode.appendChild(editLink); } } function ensureNameLayout(firstCell) { if (firstCell.querySelector(`.${NAME_WRAP_CLASS}`)) return; const checkbox = firstCell.querySelector(`.${CHECKBOX_CLASS}`); const nameBox = firstCell.querySelector('.integration_streamname__box'); if (!nameBox) return; const wrap = document.createElement('span'); wrap.className = NAME_WRAP_CLASS; nameBox.parentNode.insertBefore(wrap, nameBox); wrap.appendChild(nameBox); firstCell.style.display = 'flex'; firstCell.style.alignItems = 'center'; firstCell.style.columnGap = '12px'; firstCell.style.minHeight = '42px'; if (checkbox && checkbox.nextSibling !== wrap) firstCell.insertBefore(checkbox, wrap); } function getEditDialogTitle() { const dialog = document.querySelector('.el-dialog.is-fullscreen'); if (!dialog) return ''; const header = dialog.querySelector('.el-dialog__header'); if (!header) return ''; // 只取标题文本,过滤操作按钮文字 const walker = document.createTreeWalker(header, NodeFilter.SHOW_TEXT, null); const texts = []; let node; while ((node = walker.nextNode())) { const text = node.textContent.trim(); if (!text) continue; if (['草稿', '保存', '发布', '退出'].includes(text)) continue; texts.push(text); } return texts[0] || ''; } function updateTabTitleForEdit() { if (!isWorkflowPage()) return; const title = getEditDialogTitle(); if (!title) { // 没有编辑对话框时恢复原标题 const original = sessionStorage.getItem(ORIGINAL_TITLE_KEY); if (original && document.title !== original) { document.title = original; } return; } // 首次打开时保存原标题 if (!sessionStorage.getItem(ORIGINAL_TITLE_KEY)) { sessionStorage.setItem(ORIGINAL_TITLE_KEY, document.title); } const newTitle = `流-${title}`; if (document.title !== newTitle) { document.title = newTitle; } } function openEditByQueryParam() { if (!isWorkflowPage()) return false; const url = new URL(location.href); const apiName = url.searchParams.get(OPEN_EDIT_QUERY_KEY); if (!apiName) return false; // 移除参数避免重复触发 url.searchParams.delete(OPEN_EDIT_QUERY_KEY); history.replaceState(null, '', url.toString()); const tryOpen = () => { const rows = getRows(); const targetRow = rows.find((row) => getRowData(row)?.apiName === apiName); if (!targetRow) return false; const editButton = Array.from(targetRow.querySelectorAll('button')).find( (button) => button.textContent.trim() === '编辑' ); if (!editButton) return false; editButton.click(); return true; }; // 列表可能未渲染完成,轮询尝试 let attempts = 0; const timer = setInterval(() => { attempts += 1; if (tryOpen() || attempts >= 60) { clearInterval(timer); if (attempts >= 60) console.warn('[CRM iPaaS Plus] Failed to auto-open edit for', apiName); } }, 500); return true; } function render() { if (!isWorkflowPage()) { document.getElementById('ipaas-plus-toolbar')?.remove(); state.enabled = false; state.selectedApiNames.clear(); return; } ensureStyle(); ensureRefreshButton(); const toolbar = ensureToolbar(); if (!toolbar) return; if (!state.customToolbarPosition) setDefaultToolbarPosition(toolbar); const selectableRows = getSelectableRows(); getRows().forEach((row) => { syncRowApiName(row); syncRowCheckbox(row); }); const visibleApiNames = new Set(selectableRows.map((row) => getRowData(row)?.apiName).filter(Boolean)); state.selectedApiNames.forEach((apiName) => { if (!visibleApiNames.has(apiName)) state.selectedApiNames.delete(apiName); }); const selectedCount = state.selectedApiNames.size; const entryButton = toolbar.querySelector('[data-action="toggle"]'); const panelControls = Array.from(toolbar.querySelectorAll('[data-action]')).filter((button) => button.dataset.action !== 'toggle'); toolbar.classList.toggle('expanded', state.enabled); entryButton.textContent = state.enabled ? '收起' : '工具'; panelControls.forEach((button) => { button.style.display = state.enabled ? '' : 'none'; }); updateSummary(state.enabled ? state.message || `已选 ${selectedCount} / ${selectableRows.length}` : ''); toolbar.querySelector('.ipaas-plus-summary').style.display = state.enabled ? '' : 'none'; toolbar.querySelector('[data-action="select-all"]').disabled = state.deleting || selectableRows.length === 0; toolbar.querySelector('[data-action="clear"]').disabled = state.deleting || selectedCount === 0; toolbar.querySelector('[data-action="delete"]').disabled = state.deleting || selectedCount === 0; toolbar.querySelector('[data-action="stop"]').style.display = state.enabled && state.deleting ? '' : 'none'; } async function startBatchDelete() { if (state.deleting) return; const items = getSelectedItems(); if (items.length === 0) return; const preview = items.map((item) => item.name || item.apiName).slice(0, 8).join('\n'); const suffix = items.length > 8 ? `\n...另有 ${items.length - 8} 个` : ''; if (!window.confirm(`确定删除 ${items.length} 个集成流?\n\n${preview}${suffix}`)) return; state.deleting = true; state.stopped = false; state.message = ''; render(); let deleted = 0; const failed = []; for (const item of items) { if (state.stopped) break; const label = item.name || item.apiName; state.message = `删除中 ${deleted + 1} / ${items.length}: ${label}`; updateSummary(state.message); try { await deleteWorkflow(item); deleted += 1; state.selectedApiNames.delete(item.apiName); } catch (error) { failed.push(`${label}: ${error.message}`); console.error('[CRM iPaaS Plus] Delete failed', error); } render(); } state.deleting = false; state.stopped = false; refreshWorkflowList(); if (failed.length > 0) { state.message = `已删除 ${deleted} 个,失败 ${failed.length} 个`; console.warn('[CRM iPaaS Plus] Delete failures', failed); } else { state.message = `已删除 ${deleted} 个集成流`; } render(); } function boot() { if (state.observer) return; state.observer = new MutationObserver(() => { scheduleRender(); updateTabTitleForEdit(); }); state.observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('hashchange', scheduleRender); scheduleRender(); // 处理通过跳转按钮带过来的自动编辑参数 openEditByQueryParam(); } boot(); })();