// ==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();
})();