// ==UserScript==
// @name 数字供应链框架协议助手
// @namespace http://tampermonkey.net/
// @version 2026年7月17日
// @description 批量编制框架协议并提交审批 (嵌入修正+上传重试版)
// @author Assistant
// @match https://dsc.cscec8b.com.cn:20025/*
// @grant GM_addStyle
// @require https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
apiBase: 'https://dsc.cscec8b.com.cn:20025',
endpoints: {
searchWinBid: '/api-gateway/dsc-contract-manager/yunZhuProvider/findYunZhuWinBidResults',
searchTemplate: '/api-gateway/dsc-contract/template/searchByPage',
getTemplateElements: '/api-gateway/dsc-contract/templateElement/findByTemplateId',
searchContract: '/api-gateway/dsc-contract-manager/contract/searchByPage',
createDraft: '/api-gateway/dsc-contract-manager/contract/createdYunZhuNetWorkContract',
getContractDetail: '/api-gateway/dsc-contract-manager/contract/findOne',
updateContract: '/api-gateway/dsc-contract-manager/contract/updateContract',
savePaymentPlan: '/api-gateway/dsc-contract-manager/paymentPlan/savePaymentPlans',
uploadFile: '/api-gateway/dsc-contract-manager/contract/uploadWithWaterMark',
saveArchive: '/api-gateway/dsc-contract-manager/contractArchive/saveApproveReferenceArchives',
saveArchiveBatch: '/api-gateway/dsc-contract-manager/contractArchive/saveArchiveBatch',
getTaxRates: '/api-gateway/bidb-dms/taxRate/findByPage',
getDict: '/api-gateway/bidb-dms/dataDict/getCanUseDataDictValues',
createRequest: '/api-gateway/dsc-contract-manager/draftRequest/createRequestAndGetStartParam',
startFlow: '/api-gateway/dsc-contract-manager/draftRequest/startFlow',
startFlowNew: '/api-gateway/flow-service/defaultFlowBase/startFlowNew',
getEntity: '/api-gateway/flow-service/design/getEntity',
saveSolidify: '/api-gateway/flow-service/flowSolidifyExecutor/saveSolidifyInfoByExecutorVos',
getAreas: '/api-gateway/bidb-dms/area/findListWithRegionTree?filterFrozen=true',
getCalculatedUsers: '/api-gateway/flow-service/flowTask/getExecutorsByExecutorsVos',
getOrgs: '/api-gateway/bidb-basic/corporation/getUserAuthorizedEntitiesWithoutFullColumns',
getAllCorporations: '/api-gateway/bidb-basic/corporation/findAll',
getSignInfo: '/api-gateway/dsc-contract-manager/contractSign/findSignInfoDetailByContractId',
offlineSign: '/api-gateway/dsc-contract-manager/contractSign/offlineSign'
},
implicitFields: [
{
name: "税率",
match: (code, name) => name.includes("税率"),
domId: 'yz-dynamic-tax',
api: {
dataSourceUrl: '/api-gateway/bidb-dms/taxRate/findByPage',
requestMethod: 'POST',
params: JSON.stringify({ "quickSearchValue": "", "quickSearchProperties": ["code", "name"], "pageInfo": { "page": 1, "rows": 1000 } })
}
},
{
name: "国际贸易条款",
match: (code, name) => (code === 'internationalTradeTerms' || name === "国际贸易条款") && !code.includes("Additional") && !name.includes("补充"),
api: {
dataSourceUrl: '/api-gateway/bidb-dms/dataDict/getCanUseDataDictValues',
requestMethod: 'GET',
params: JSON.stringify({ dictCode: 'priceClause' })
}
}
],
uiInjections: [
{
triggerCode: 'internationalTradeTerms',
targetField: {
elementName: "国际贸易条款补充内容",
elementCode: "internationalTradeTermsAdditional",
showType: "text",
required: false,
}
}
],
};
let state = {
xsid: '',
isMinimized: false,
templates: [],
searchResults: [],
loadingDicts: {},
paymentRowCounter: 0,
exportData: [],
areaTreeData: [],
orgTreeData: [],
userScopes: [],
templateFieldMaps: {},
templateElementsCache: {},
currentTemplateFields: [],
defaultPartyA: null
};
const STYLES = `
.yz-launcher-tab { width: 120px; cursor: pointer; display: inline-block; height: 100%; vertical-align: top; position: relative; color: rgba(0,0,0,0.65); transition: color 0.3s; float: left; }
.yz-launcher-tab:hover { color: #1890ff; }
.yz-launcher-tab .tab-top-line { position: absolute; top: 0; left: 0; width: 100%; height: 2px; background-color: transparent; }
.yz-launcher-tab:hover .tab-top-line { background-color: #1890ff; }
.yz-launcher-tab .tab-title { height: 40px; margin-top: 4px; display: flex; justify-content: center; align-items: center; font-size: 14px; font-weight: 400; }
.yz-launcher-tab a { color: inherit; }
#yz-helper-panel { position: fixed; top: 90px; right: 30px; width: 840px; background: #fff; box-shadow: 0 8px 30px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.08); border-radius: 8px; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; border: 1px solid #e8e8e8; display: none; flex-direction: column; max-height: 85vh; transition: all 0.3s ease; }
.yz-header { background: linear-gradient(135deg, #fafafa 0%, #fff 100%); color: #1a1a1a; padding: 18px 24px; display: flex; justify-content: space-between; align-items: center; cursor: move; border-bottom: 1px solid #e8e8e8; border-radius: 8px 8px 0 0; font-size: 16px; font-weight: 600; user-select: none; }
.yz-body { padding: 20px; overflow-y: auto; background: #f5f6f8; flex: 1; display: flex; flex-direction: column; gap: 14px; }
.yz-body::-webkit-scrollbar { width: 6px; }
.yz-body::-webkit-scrollbar-track { background: transparent; }
.yz-body::-webkit-scrollbar-thumb { background: #d9d9d9; border-radius: 3px; }
.yz-body::-webkit-scrollbar-thumb:hover { background: #b3b3b3; }
.yz-section { background: #fff; padding: 18px; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0; transition: box-shadow 0.2s; }
.yz-section:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.yz-section-title { font-weight: 600; font-size: 15px; color: #1a1a1a; margin-bottom: 14px; display: flex; align-items: center; letter-spacing: 0.3px; }
.yz-section-title::before { content: ''; display: inline-block; width: 3px; height: 18px; background: linear-gradient(180deg, #1890ff, #69c0ff); margin-right: 10px; border-radius: 3px; }
.yz-input-group { display: flex; gap: 8px; margin-bottom: 8px; }
.yz-input { flex: 1; padding: 8px 12px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 14px; outline: none; transition: all 0.2s ease; color: rgba(0,0,0,0.65); background: #fafafa; }
.yz-input:hover { border-color: #91d5ff; background: #fff; }
.yz-input:focus { border-color: #1890ff; background: #fff; box-shadow: 0 0 0 3px rgba(24,144,255,0.12); }
.yz-input:disabled { background: #f5f5f5; cursor: not-allowed; color: rgba(0,0,0,0.25); }
.yz-btn { padding: 8px 20px; background: #1890ff; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; white-space: nowrap; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(24,144,255,0.2); }
.yz-btn:hover { background: #40a9ff; box-shadow: 0 4px 8px rgba(24,144,255,0.3); transform: translateY(-1px); }
.yz-btn:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(24,144,255,0.2); }
.yz-btn-green { background: #52c41a; box-shadow: 0 2px 4px rgba(82,196,26,0.2); }
.yz-btn-green:hover { background: #73d13d; box-shadow: 0 4px 8px rgba(82,196,26,0.3); }
.yz-btn-orange { background: #faad14; color: #fff; box-shadow: 0 2px 4px rgba(250,173,20,0.2); }
.yz-btn-orange:hover { background: #ffc53d; box-shadow: 0 4px 8px rgba(250,173,20,0.3); }
.yz-btn-purple { background: #722ed1; box-shadow: 0 2px 4px rgba(114,46,209,0.2); }
.yz-btn-purple:hover { background: #9254de; box-shadow: 0 4px 8px rgba(114,46,209,0.3); }
.yz-btn-danger { background: #ff4d4f; padding: 4px 10px; font-size: 12px; box-shadow: 0 2px 4px rgba(255,77,79,0.2); }
.yz-btn-danger:hover { background: #ff7875; box-shadow: 0 4px 8px rgba(255,77,79,0.3); }
.yz-btn-add { width: 100%; background: #fff; margin-top: 5px; border: 1px dashed #d9d9d9; color: rgba(0,0,0,0.65); box-shadow: none; }
.yz-btn-add:hover { border-color: #1890ff; color: #1890ff; transform: none; }
.yz-form-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px 16px; }
.yz-form-item { display: flex; flex-direction: column; }
.yz-form-item.dependent-field { margin-left: 15px; border-left: 4px solid #faad14; padding: 8px 12px; background: #fffbe6; border-radius: 0 6px 6px 0; grid-column: 1/-1 !important; margin-bottom: 8px; }
.yz-form-label { font-size: 14px; color: rgba(0,0,0,0.85); margin-bottom: 6px; }
.yz-payment-row { background: #fafafa; border: 1px solid #e8e8e8; padding: 12px; margin-bottom: 12px; border-radius: 6px; }
.yz-payment-header { display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 14px; font-weight: 500; color: #1890ff; }
.yz-table-wrapper { overflow-x: auto; border: 1px solid #f0f0f0; border-radius: 6px; height: 150px; overflow-y: auto; }
.yz-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.yz-table th { background: #fafafa; position: sticky; top: 0; padding: 10px 8px; text-align: left; border-bottom: 1px solid #f0f0f0; font-weight: 500; color: rgba(0,0,0,0.85); }
.yz-table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; color: rgba(0,0,0,0.65); }
.yz-status { font-size: 13px; color: #52c41a; display: flex; align-items: center; gap: 6px; padding: 8px 12px; background: #f6ffed; border: 1px solid #b7eb8f; border-radius: 6px; margin-top: 0; }
.yz-status.error { color: #ff4d4f; background: #fff2f0; border-color: #ffccc7; }
.yz-status.processing { color: #1890ff; background: #e6f7ff; border-color: #91d5ff; }
.required::after { content: '*'; color: #ff4d4f; margin-left: 2px; }
.yz-file-input { border: 1px dashed #d9d9d9; padding: 8px; width: 100%; border-radius: 6px; background: #fafafa; transition: border-color 0.3s; }
.yz-file-input:hover { border-color: #1890ff; }
.yz-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.45); z-index: 10000; display: flex; justify-content: center; align-items: center; }
.yz-modal { background: #fff; width: 650px; border-radius: 8px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); display: flex; flex-direction: column; max-height: 85vh; }
.yz-modal-header { padding: 16px 24px; border-bottom: 1px solid #f0f0f0; font-weight: 600; font-size: 16px; color: #1a1a1a; }
.yz-modal-body { padding: 24px; overflow-y: auto; flex: 1; }
.yz-modal-footer { padding: 10px 16px; border-top: 1px solid #f0f0f0; text-align: right; display: flex; justify-content: flex-end; gap: 8px; }
.yz-node-group { margin-bottom: 15px; border: 1px solid #e8e8e8; border-radius: 6px; padding: 12px; background: #fff; }
.yz-node-title { font-weight: bold; color: #1890ff; margin-bottom: 8px; font-size: 14px; display: flex; justify-content: space-between; }
.yz-node-title.error-node { color: #ff4d4f; }
.yz-user-item { display: flex; align-items: center; margin-bottom: 5px; font-size: 14px; cursor: pointer; padding: 4px 0; border-bottom: 1px dashed #f0f0f0; }
.yz-user-meta { color: rgba(0,0,0,0.45); font-size: 12px; margin-left: 5px; }
.yz-user-pos { color: rgba(0,0,0,0.45); font-size: 12px; margin-left: 5px; }
.yz-tree-search { margin-bottom: 10px; width: 100%; box-sizing: border-box; }
.yz-tree-ul { list-style: none; padding-left: 20px; margin: 0; }
.yz-tree-ul.root { padding-left: 0; }
.yz-tree-node { margin: 2px 0; }
.yz-tree-content { display: flex; align-items: center; cursor: pointer; padding: 4px 6px; border-radius: 6px; }
.yz-tree-content:hover { background-color: #e6f7ff; }
.yz-tree-toggle { display: inline-block; width: 16px; height: 16px; line-height: 14px; text-align: center; margin-right: 4px; color: rgba(0,0,0,0.45); font-family: monospace; border: 1px solid #d9d9d9; border-radius: 4px; background: #fff; }
.yz-tree-toggle:hover { border-color: #1890ff; color: #1890ff; }
.yz-tree-label { font-size: 14px; color: rgba(0,0,0,0.85); }
.yz-tree-children { display: none; }
.yz-tree-children.expanded { display: block; }
.yz-scope-list { margin-top: 10px; border: 1px solid #f0f0f0; border-radius: 6px; max-height: 120px; overflow-y: auto; }
.yz-scope-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid #f0f0f0; background: #fff; font-size: 13px; }
.yz-scope-text { flex: 1; }
.yz-del-btn { color: #ff4d4f; cursor: pointer; margin-left: 10px; font-weight: 500; }
.yz-checkbox-wrapper { display: flex; align-items: center; font-size: 14px; color: rgba(0,0,0,0.85); margin-top: 5px; }
.yz-checkbox-wrapper input { margin-right: 6px; }
.yz-flow-radio-item { padding: 8px; border-bottom: 1px solid #eee; display: flex; align-items: center; cursor: pointer; }
.yz-flow-radio-item:hover { background: #e6f7ff; }
.yz-flow-radio-item input { margin-right: 10px; }
.yz-error-tip { font-size: 12px; color: #ff4d4f; margin-bottom: 4px; background: #fff2f0; padding: 6px; border-radius: 6px; border: 1px solid #ffccc7; }
.yz-sub-title { font-size: 12px; font-weight: 600; color: #8c8c8c; margin-bottom: 10px; padding-left: 10px; border-left: 3px solid #1890ff; text-transform: uppercase; letter-spacing: 0.5px; }
.yz-batch-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.yz-batch-card { background: #fff; border: 1px solid #e8e8e8; border-radius: 8px; padding: 16px; display: flex; flex-direction: column; transition: box-shadow 0.2s, border-color 0.2s; }
.yz-batch-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.08); border-color: #d0d0d0; }
.yz-batch-card-title { font-size: 14px; font-weight: 600; color: #1a1a1a; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 2px solid #1890ff; }
.yz-batch-card-title.accent-green { border-bottom-color: #52c41a; }
.yz-batch-card-title.accent-orange { border-bottom-color: #faad14; }
.yz-batch-card-title.accent-purple { border-bottom-color: #722ed1; }
.yz-preset-toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; flex-wrap: wrap; }
.yz-preset-toolbar select { flex: 1; min-width: 120px; padding: 7px 10px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 13px; background: #fff; color: rgba(0,0,0,0.85); outline: none; }
.yz-preset-toolbar select:focus { border-color: #1890ff; }
.yz-preset-btn { padding: 6px 14px; border: 1px solid #d9d9d9; border-radius: 6px; cursor: pointer; font-size: 13px; background: #fff; color: rgba(0,0,0,0.65); transition: all 0.2s; white-space: nowrap; }
.yz-preset-btn:hover { border-color: #1890ff; color: #1890ff; }
.yz-preset-btn.danger:hover { border-color: #ff4d4f; color: #ff4d4f; }
`;
const utils = {
decodeSidCandidate: (encodedStr) => {
try {
const decodedBase64 = atob(encodedStr);
let decodedUrl = decodedBase64;
try { decodedUrl = decodeURIComponent(decodedBase64); } catch (e) { }
return decodedUrl.replace(/^["']|["']$/g, '');
} catch (e) { return null; }
},
autoGetXsid: () => {
const guidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
const val = sessionStorage.getItem(key);
const candidates = [val, key];
for (const candidate of candidates) {
if (!candidate || candidate.length < 20) continue;
const result = utils.decodeSidCandidate(candidate);
if (result && guidRegex.test(result)) return result;
}
}
return '';
},
generateUUID: () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
}).toUpperCase();
},
parseJSONSafe: (str) => {
try { return JSON.parse(str); } catch (e) { return null; }
},
request: async (method, url, data, callback) => {
if (!state.xsid) state.xsid = utils.autoGetXsid();
let finalUrl = CONFIG.apiBase + (url.startsWith('/') ? '' : '/') + url;
const headers = {
'x-sid': state.xsid || '',
'neverCancel': 'true'
};
const options = {
method: method,
headers: headers
};
if (method === 'GET' && data) {
const params = Object.keys(data).map(key => `${key}=${encodeURIComponent(data[key])}`).join('&');
if (params) finalUrl += (finalUrl.includes('?') ? '&' : '?') + params;
headers['Content-Type'] = 'application/json;charset=UTF-8';
} else if (data instanceof FormData) {
options.body = data;
} else if (data) {
headers['Content-Type'] = 'application/json;charset=UTF-8';
options.body = JSON.stringify(data);
} else {
headers['Content-Type'] = 'application/json;charset=UTF-8';
}
console.log(`[YzHelper] Fetch: ${method} ${finalUrl}`, data);
try {
const response = await fetch(finalUrl, options);
const text = await response.text();
let json = null;
try { json = JSON.parse(text); } catch (e) { console.warn("JSON Parse Failed", text); }
if (!response.ok) {
callback(new Error(`HTTP Error ${response.status}: ${response.statusText}`), json);
} else if (json) {
callback(null, json);
} else {
callback(new Error("Empty response"), null);
}
} catch (err) {
console.error("Fetch Network Error", err);
callback(err, null);
}
},
requestPromise: (method, url, data, desc = "API请求") => {
return new Promise((resolve, reject) => {
utils.request(method, url, data, (err, res) => {
if (err) return reject(new Error(`${desc}失败: ${err.message || err}`));
if (!res) return reject(new Error(`${desc}无响应`));
if (!res.success && res.status !== 200) {
const errorMsg = res.message || (res.data ? JSON.stringify(res.data) : "未知错误");
return reject(new Error(`${desc}失败: ${errorMsg}`));
}
resolve(res);
});
});
},
sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
getNowStr: () => {
const d = new Date();
const p = n => n < 10 ? '0' + n : n;
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
parseCodeList: (text) => text.split(/[\n,,]/).map(k => k.trim()).filter(k => k.length > 0),
buildFileFormData: (file) => { const fd = new FormData(); fd.append('file', file); return fd; },
searchContractByCode: async (code) => {
const res = await utils.requestPromise('POST', CONFIG.endpoints.searchContract, {
"quickSearchValue": code,
"quickSearchProperties": ["code"],
"pageInfo": { "page": 1, "rows": 100 }
});
return res.data?.rows?.find(r => r.code === code) || null;
},
showStatus: (msg, type = 'success') => {
const el = document.getElementById('yz-status-msg');
if (el) { el.innerHTML = msg; el.className = `yz-status ${type}`; }
},
readExcelFile: (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const json = XLSX.utils.sheet_to_json(worksheet);
resolve(json);
} catch (error) {
reject(error);
}
};
reader.onerror = (error) => reject(error);
reader.readAsArrayBuffer(file);
});
},
getFormData: () => {
const getVal = (id) => { const el = document.getElementById(id); return el ? el.value.trim() : ''; };
const fileInput = document.getElementById('yz-in-file');
const files = fileInput && fileInput.files.length > 0 ? fileInput.files : [];
const excelInput = document.getElementById('yz-in-data-excel');
const excelFile = excelInput && excelInput.files.length > 0 ? excelInput.files[0] : null;
const overwriteScope = document.getElementById('yz-overwrite-scope') ? document.getElementById('yz-overwrite-scope').checked : false;
const paymentRows = document.querySelectorAll('.yz-payment-row');
const payments = Array.from(paymentRows).map(row => {
const getOptData = (sel) => {
if (sel && sel.selectedIndex >= 0) {
const opt = sel.options[sel.selectedIndex];
return { value: opt.value, name: opt.getAttribute('data-name') || opt.text };
}
return { value: '', name: '' };
};
return {
windUp: getOptData(row.querySelector('[data-type="windUp"]')),
stage: getOptData(row.querySelector('[data-type="stage"]')),
method: getOptData(row.querySelector('[data-type="method"]')),
premise: row.querySelector('[id^="yz-in-pay-premise-"]').value.trim(),
period: row.querySelector('[id^="yz-in-pay-period-"]').value.trim(),
percent: row.querySelector('[id^="yz-in-pay-percent-"]').value.trim(),
remark: row.querySelector('[id^="yz-in-pay-remark-"]').value.trim()
};
});
const dynamicFields = {};
const dynContainers = ['yz-partya-form-area', 'yz-dynamic-form-area'];
dynContainers.forEach(containerId => {
const dynContainer = document.getElementById(containerId);
if (dynContainer) {
const inputs = dynContainer.querySelectorAll('input, select');
inputs.forEach(input => {
if (input.tagName === 'SELECT' && input.dataset.label) {
dynamicFields[input.dataset.label] = input.value;
if (input.selectedIndex >= 0) {
const opt = input.options[input.selectedIndex];
if (opt.dataset.rate) dynamicFields[input.dataset.label + "_rate"] = opt.dataset.rate;
if (opt.dataset.code) dynamicFields[input.dataset.label + "_code"] = opt.dataset.code;
if (opt.dataset.name) dynamicFields[input.dataset.label + "_name"] = opt.dataset.name;
}
}
else if (input.dataset.label) {
dynamicFields[input.dataset.label] = input.value;
}
});
}
});
const flowDefKey = getVal('yz-in-flowKey');
const autoSubmit = document.getElementById('yz-in-auto-submit').checked;
return {
dynamicFields: dynamicFields,
payments: payments,
files: files,
excelFile: excelFile,
flow: { key: flowDefKey, autoSubmit: autoSubmit },
userScopes: state.userScopes,
overwriteScope: overwriteScope,
tax: { code: null }
};
},
transformExcelDataToStructure: (res, formData, excelRow, fieldMap) => {
if (!res || !res.data) return null;
if (Array.isArray(res.data)) throw new Error("获取的合同详情数据异常(返回了数组)");
const data = res.data;
const additional = data.additional || {};
const parties = data.contractParties || [];
const partyA = parties.find(p => p.partyType === 'A') || {};
const partyB = parties.find(p => p.partyType === 'B') || {};
const rowName = excelRow['合同名称'] || data.name;
const rowSignAddr = excelRow['合同签订地点'] || additional.signAddress || '';
const rowLegal = excelRow['乙方法人代表'] || '';
const rowAgent = excelRow['乙方代理人姓名'] || '';
const rowTaxType = excelRow['乙方纳税人身份'] || '';
const rowTaxCode = excelRow['乙方纳税人识别号'] || '';
const rowBank = excelRow['乙方开户行'] || '';
const rowAccount = excelRow['乙方开户行账号'] || '';
const rowPhone = excelRow['乙方联系电话'] || '';
const rowEmail = excelRow['乙方联系邮箱'] || '';
const rowAddr = excelRow['乙方地址'] || '';
let finalScopes = additional.contractScopes || [];
if (formData.userScopes && formData.userScopes.length > 0) {
const newScopes = formData.userScopes.map(scope => ({
contractCode: data.code,
useAreaCode: scope.area.code,
useAreaName: scope.area.name,
useOriginalCode: scope.org.code,
useOriginalName: scope.org.name,
id: null,
contractId: data.id
}));
finalScopes = formData.overwriteScope ? newScopes : [...finalScopes, ...newScopes];
}
const dynamicElm = {};
const dynamicValueMap = {};
const dynamicTextMap = {};
let taxCodeVal = null, taxNameVal = null, taxRateVal = null;
let tradeCodeVal = null, tradeNameVal = null, tradeAddVal = null;
let transCodeVal = null;
const getExcelVal = (keywords) => {
const key = Object.keys(excelRow).find(k => keywords.some(kw => k.includes(kw)));
return key ? excelRow[key] : undefined;
};
let tradeAddContent = getExcelVal(['国际贸易条款补充', '贸易条款补充', '补充内容']);
if (tradeAddContent) {
tradeAddVal = String(tradeAddContent);
}
for (const [label, code] of Object.entries(fieldMap)) {
let val = undefined;
if (excelRow.hasOwnProperty(label)) {
val = excelRow[label];
}
if (val === undefined || val === null || val === '') {
val = formData.dynamicFields[label];
}
if (label.includes("税率")) {
taxCodeVal = formData.dynamicFields[label + "_code"] || val;
taxNameVal = formData.dynamicFields[label + "_name"];
taxRateVal = formData.dynamicFields[label + "_rate"];
}
if (label.includes("贸易条款") && !label.includes("补充")) {
tradeCodeVal = val;
tradeNameVal = formData.dynamicFields[label + "_name"];
}
if (label.includes("补充条款") || (label.includes("贸易") && label.includes("补充"))) {
tradeAddVal = val;
}
if (label.includes("运输条款")) {
transCodeVal = val;
}
if (val !== undefined && val !== null) {
val = String(val);
dynamicElm[code] = val;
dynamicValueMap[code] = val;
dynamicTextMap[code] = val;
}
}
return {
contract: {
...data, name: rowName, contractId: data.id, additional: null,
partyAiCode: partyA.partyCode, partyBiCode: partyB.partyCode,
elm: {
...dynamicElm,
signAddress: rowSignAddr, partyBi: partyB.partyName, mysideName: partyA.partyName,
partyBiCode: partyB.partyCode, mysideCode: partyA.partyCode, mysideId: partyA.partyId,
partyBiName: partyB.partyName, partyBiId: partyB.partyId, myside: partyA.partyName
},
contractParties: null, contractScopes: null, contractProjects: null
},
elementValueMap: dynamicValueMap,
elementTextMap: dynamicTextMap,
contractParties: parties.map(p => p.partyType === 'B' ? {
...p, legalPerson: rowLegal, agentName: rowAgent,
taxpayerType: rowTaxType, taxpayerCode: rowTaxCode, openedBankName: rowBank,
openedBankAccount: rowAccount, telPhone: rowPhone, email: rowEmail, partyAddress: rowAddr
} : p),
contractAdditional: {
...additional, signAddress: rowSignAddr, agreementSignAddress: rowSignAddr,
transportationType: transCodeVal || additional.transportationType,
taxCode: taxCodeVal || additional.taxCode,
taxName: taxNameVal || additional.taxName,
taxRate: taxRateVal ? parseFloat(taxRateVal) : additional.taxRate,
internationalTradeTermsCode: tradeCodeVal || additional.internationalTradeTermsCode,
internationalTradeTermsName: tradeNameVal || additional.internationalTradeTermsName,
internationalTradeTermsAdditional: tradeAddVal || additional.internationalTradeTermsAdditional,
contractScopes: finalScopes
},
temporary: true
};
},
toggleInputs: (disabled) => {
const ids = ['yz-template-select', 'yz-search-input', 'yz-btn-search', 'yz-btn-update-excel', 'yz-btn-add-payment', 'yz-in-auto-submit', 'yz-in-flowKey', 'yz-manual-contract-code', 'yz-btn-manual-submit'];
ids.forEach(id => {
const el = document.getElementById(id);
if (el) el.disabled = disabled;
});
['yz-dynamic-form-area', 'yz-partya-form-area'].forEach(areaId => {
const dynArea = document.getElementById(areaId);
if (dynArea) {
dynArea.querySelectorAll('input, select').forEach(el => el.disabled = disabled);
}
});
}
};
const actions = {
init: () => {
state.xsid = utils.autoGetXsid();
console.log("YunZhuHelper: Init V9.60 (Fix Upload Retry)...");
actions.fetchTreeData();
actions.fetchDefaultPartyA();
actions.injectLauncher();
},
injectLauncher: () => {
let tryCount = 0;
const checkExist = setInterval(() => {
tryCount++;
const headerRight = document.querySelector('.header-layout-right');
if (headerRight) {
clearInterval(checkExist);
const launcher = document.createElement('div');
launcher.className = 'custom-tabs-item___hCi_u ant-dropdown-trigger yz-launcher-tab';
launcher.style.marginRight = '15px';
launcher.innerHTML = `
`;
launcher.onclick = () => {
const panel = document.getElementById('yz-helper-panel');
if (panel) {
panel.style.display = 'flex';
}
};
headerRight.insertBefore(launcher, headerRight.firstChild);
console.log("[YzHelper] 助手按钮已成功注入到右上角导航栏");
} else if (tryCount >= 10) {
clearInterval(checkExist);
console.warn("[YzHelper] 找不到顶部菜单定位点,启用全局悬浮按钮兜底");
const floatBtn = document.createElement('div');
floatBtn.innerHTML = '助手';
floatBtn.style.cssText = `
position: fixed; top: 50%; right: 0; transform: translateY(-50%);
background: #1890ff; color: white; padding: 15px 8px;
border-radius: 8px 0 0 8px; cursor: pointer; z-index: 99999;
box-shadow: -2px 0 12px rgba(0,0,0,0.15); font-size: 14px;
writing-mode: vertical-rl; text-orientation: upright; font-weight: bold;
transition: background-color 0.3s;
`;
floatBtn.onmouseover = () => floatBtn.style.backgroundColor = '#40a9ff';
floatBtn.onmouseout = () => floatBtn.style.backgroundColor = '#1890ff';
floatBtn.onclick = () => {
const panel = document.getElementById('yz-helper-panel');
if (panel) {
panel.style.display = 'flex';
}
};
document.body.appendChild(floatBtn);
}
}, 1000);
},
fetchDefaultPartyA: () => {
utils.request('GET', CONFIG.endpoints.getAllCorporations, null, (err, data) => {
if (err || !data || !data.success) {
console.error("Fetch PartyA Error", err, data);
return;
}
if (Array.isArray(data.data)) {
const target = data.data.find(c => c.code === "100");
if (target) {
state.defaultPartyA = target;
console.log("Default PartyA Loaded:", target.name);
}
}
});
},
fetchTreeData: () => {
utils.request('GET', CONFIG.endpoints.getAreas, null, (err, data) => {
if (!err && data.success) state.areaTreeData = data.data || [];
});
utils.request('GET', CONFIG.endpoints.getOrgs, null, (err, data) => {
if (!err && data.success) state.orgTreeData = data.data || [];
});
},
getAvailableFlows: async (contractId, contractName) => {
utils.showStatus("正在获取可用审批流...", "processing");
try {
const reqRes = await utils.requestPromise('POST', CONFIG.endpoints.createRequest, {
contractId: contractId,
name: contractName || "FlowQuery",
flowTypeId: null,
requestId: null
}, "创建流程请求(用于获取列表)");
if (reqRes.success && reqRes.data && reqRes.data.requestId) {
const requestId = reqRes.data.requestId;
const dynModelCode = reqRes.data.businessModelCode;
const flowRes = await utils.requestPromise('POST', CONFIG.endpoints.startFlow, {
businessKey: requestId,
businessModelCode: dynModelCode,
typeId: null,
flowDefKey: null
}, "获取审批流列表");
if (flowRes.success && flowRes.data) {
const raw = flowRes.data;
let list = [];
if (raw.flowTypeList && Array.isArray(raw.flowTypeList)) list = raw.flowTypeList;
else if (Array.isArray(raw)) list = raw;
return list;
}
}
} catch (e) {
console.error("Get Flows Error", e);
utils.showStatus("获取审批流列表失败", "error");
}
return [];
},
promptFlowSelection: (contractName, flowList) => {
return new Promise((resolve) => {
if (!flowList || flowList.length === 0) {
alert("未找到可用的审批流,无法自动发起审批。");
resolve(null);
return;
}
const overlay = document.createElement('div'); overlay.className = 'yz-modal-overlay';
const modal = document.createElement('div'); modal.className = 'yz-modal';
const header = document.createElement('div'); header.className = 'yz-modal-header'; header.innerText = `🔄 选择审批流程 - ${contractName}`;
const body = document.createElement('div'); body.className = 'yz-modal-body';
const tip = document.createElement('div');
tip.style.marginBottom = '10px';
tip.style.color = '#666';
tip.style.fontSize = '12px';
tip.innerText = '请确认本次发起审批使用的流程定义。系统默认选中 UI 上选择的或第一个可用流程。';
body.appendChild(tip);
const uiSelect = document.getElementById('yz-in-flowKey');
const userPrefKey = uiSelect ? uiSelect.value : null;
let selectedKey = null;
flowList.forEach((f, idx) => {
const key = f.flowDefKey || f.key;
const name = f.flowDefName || f.name;
const row = document.createElement('label');
row.className = 'yz-flow-radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'yz_flow_choice';
radio.value = key;
if (userPrefKey && key === userPrefKey) {
radio.checked = true;
selectedKey = key;
} else if (!selectedKey && idx === 0) {
radio.checked = true;
selectedKey = key;
}
radio.onclick = () => { selectedKey = key; };
const text = document.createElement('span');
text.innerText = `${name} (${key})`;
row.appendChild(radio);
row.appendChild(text);
body.appendChild(row);
});
const footer = document.createElement('div'); footer.className = 'yz-modal-footer';
const cancelBtn = document.createElement('button'); cancelBtn.className = 'yz-btn'; cancelBtn.innerText = '跳过审批';
cancelBtn.onclick = () => { document.body.removeChild(overlay); resolve(null); };
const confirmBtn = document.createElement('button'); confirmBtn.className = 'yz-btn yz-btn-green'; confirmBtn.innerText = '确认流程并继续';
confirmBtn.onclick = () => {
if (!selectedKey) return alert("请选择一个流程");
document.body.removeChild(overlay);
resolve(selectedKey);
};
footer.appendChild(cancelBtn); footer.appendChild(confirmBtn);
modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer);
overlay.appendChild(modal); document.body.appendChild(overlay);
});
},
generateMappingTable: async () => {
try {
console.log("[YzHelper] === 开始执行智能匹配 ===");
const fileInput = document.getElementById('yz-in-sign-file');
if (!fileInput) return alert("❌ 找不到 PDF 上传框,请检查 UI 代码是否替换完整!");
const files = fileInput.files && fileInput.files.length > 0 ? Array.from(fileInput.files) : [];
if (files.length === 0) return alert("请先在上方 ① 处选择已盖章的 PDF 文件集合!");
console.log("[YzHelper] 已选择的 PDF 文件名:", files.map(f => f.name));
let rowData = [];
const excelInput = document.getElementById('yz-in-sign-excel');
const textInput = document.getElementById('yz-manual-sign-code');
const rawText = textInput ? textInput.value.trim() : "";
if (excelInput && excelInput.files && excelInput.files.length > 0) {
const btn = document.getElementById('yz-btn-generate-map');
const originalText = btn.innerText;
btn.innerText = "解析 Excel 中...";
try {
console.log("[YzHelper] 正在解析 Excel 文件...");
const xlsData = await utils.readExcelFile(excelInput.files[0]);
console.log("[YzHelper] Excel 解析成功,共读取到 " + xlsData.length + " 行数据");
if (!xlsData || xlsData.length === 0) throw new Error("Excel 数据为空");
xlsData.forEach(row => {
const code = (row['合同编号'] || row['合同编号/名称'] || row['编号'] || '');
const attachName = (row['合同附件名称'] || '');
if (code && String(code).trim()) {
rowData.push({
code: String(code).trim(),
attachName: String(attachName).trim()
});
}
});
console.log("[YzHelper] Excel 提取的列:", Object.keys(xlsData[0] || {}));
} catch (e) {
btn.innerText = originalText;
console.error("[YzHelper] Excel 解析失败:", e);
return alert("解析 Excel 失败:" + e.message + "\n(请确保文件没有损坏,且含有表头数据)");
}
btn.innerText = originalText;
}
else if (rawText) {
console.log("[YzHelper] 没有上传 Excel,正在读取文本框内容...");
const codes = utils.parseCodeList(rawText);
rowData = codes.map(c => ({ code: c, attachName: '' }));
}
if (rowData.length === 0) {
return alert("未获取到有效的合同编号!\n请确保上传的 Excel 中包含【合同编号】这一列,或者展开下方折叠面板手动粘贴编号。");
}
console.log("[YzHelper] 最终提取到的合同数据:", rowData.map(r => ({ code: r.code, attachName: r.attachName })));
const tbody = document.getElementById('yz-mapping-tbody');
if (!tbody) return alert("❌ 找不到表格容器 (yz-mapping-tbody),请检查 HTML!");
tbody.innerHTML = '';
let optionsHtml = ``;
files.forEach((f, idx) => { optionsHtml += ``; });
let autoMatchCount = 0;
rowData.forEach((row, rowIndex) => {
const tr = document.createElement('tr');
tr.dataset.code = row.code;
tr.id = `yz-map-row-${rowIndex}`;
let matchedIndex = -1;
const matchKey = row.attachName || row.code;
if (matchKey) {
for (let i = 0; i < files.length; i++) {
if (files[i].name.includes(matchKey)) {
matchedIndex = i; break;
}
}
}
if (matchedIndex === -1) {
const numericMatch = row.code.match(/\d{10,}/);
if (numericMatch) {
for (let i = 0; i < files.length; i++) {
if (files[i].name.includes(numericMatch[0])) {
matchedIndex = i; break;
}
}
}
}
const selectEl = document.createElement('select');
selectEl.className = 'yz-input';
selectEl.style.width = '100%';
selectEl.innerHTML = optionsHtml;
if (matchedIndex !== -1) {
selectEl.value = matchedIndex;
selectEl.style.backgroundColor = '#f6ffed';
autoMatchCount++;
console.log(`[YzHelper] ✓ 已匹配: [${row.code}] 附件名[${row.attachName}] → [${files[matchedIndex].name}]`);
} else {
console.log(`[YzHelper] ✗ 未匹配: [${row.code}] 附件名[${row.attachName}], 文件名: [${files.map(f => f.name).join(', ')}]`);
}
const tdCode = document.createElement('td'); tdCode.innerText = row.code;
const tdSelect = document.createElement('td'); tdSelect.appendChild(selectEl);
const tdStatus = document.createElement('td'); tdStatus.className = 'res-cell'; tdStatus.style.textAlign = 'center'; tdStatus.innerText = '待确认';
tr.appendChild(tdCode); tr.appendChild(tdSelect); tr.appendChild(tdStatus);
tbody.appendChild(tr);
});
const container = document.getElementById('yz-mapping-container');
if (container) container.style.display = 'block';
console.log(`[YzHelper] === 智能匹配清单渲染完成 (自动匹配: ${autoMatchCount}/${rowData.length}) ===`);
} catch (globalError) {
console.error("[YzHelper] 生成清单时发生未知错误:", globalError);
alert("发生系统错误:" + globalError.message + "\n请按 F12 查看控制台红字报错信息。");
}
},
executeMappedUpload: async () => {
const tbody = document.getElementById('yz-mapping-tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
if (rows.length === 0) return;
const fileInput = document.getElementById('yz-in-sign-file');
const files = Array.from(fileInput.files);
const tasks = [];
for (let tr of rows) {
const code = tr.dataset.code;
const select = tr.querySelector('select');
const fileIndex = select.value;
if (!fileIndex) {
return alert(`合同 [${code}] 未选择对应的文件,请检查!`);
}
tasks.push({ rowId: tr.id, code: code, file: files[fileIndex] });
}
if (!confirm(`即将开始回传 ${tasks.length} 个合同,请确认映射关系无误。`)) return;
const btn = document.getElementById('yz-btn-execute-map');
const originalText = btn.innerText;
btn.disabled = true;
let successCount = 0, failCount = 0;
const updateRowStatus = (rowId, st, msg) => {
const td = document.getElementById(rowId).querySelector('.res-cell');
td.innerText = st === 'success' ? '✅ 成功' : `❌ ${msg}`;
td.style.color = st === 'success' ? '#52c41a' : '#ff4d4f';
};
try {
for (let i = 0; i < tasks.length; i++) {
const task = tasks[i];
btn.innerText = `处理中 (${i + 1}/${tasks.length})...`;
document.getElementById(task.rowId).querySelector('.res-cell').innerText = '⏳ 上传中';
try {
const c = await utils.searchContractByCode(task.code);
if (!c) throw new Error(`查无此合同 [${task.code}]`);
const signInfoRes = await utils.requestPromise('GET', `${CONFIG.endpoints.getSignInfo}?contractId=${c.id}`);
const signInfo = signInfoRes.data?.signInfo;
if (!signInfo || !signInfo.id) throw new Error("节点未达");
const f1 = utils.buildFileFormData(task.file);
const uRes = await utils.requestPromise('POST', `${CONFIG.endpoints.uploadFile}?companyCode=100&waterMark=false&toPdf=true`, f1);
const docId = uRes.data?.docId;
if (!docId) throw new Error("上传失败");
const payload = {
...signInfo,
signType: "OFFLINE_SIGN",
signDate: utils.getNowStr(),
officialAnnex: true,
signFileIdList: [docId],
signFileList: [{
uid: "rc-upload-" + Date.now(),
name: task.file.name,
size: task.file.size,
type: task.file.type || "application/pdf",
status: "done",
docId: docId,
response: { data: { docId: docId, fileName: task.file.name, documentType: "Pdf" }, success: true }
}]
};
await utils.requestPromise('POST', CONFIG.endpoints.offlineSign, payload);
updateRowStatus(task.rowId, 'success', '');
successCount++;
} catch (e) {
console.error(`[Map Upload] Failed: ${task.code}`, e);
updateRowStatus(task.rowId, 'error', e.message);
failCount++;
}
await utils.sleep(800);
}
alert(`回传完毕!\n成功: ${successCount} \n失败: ${failCount}`);
} finally {
btn.innerText = originalText;
btn.disabled = false;
}
},
fetchTemplateFields: async (templateId) => {
if (state.templateElementsCache && state.templateElementsCache[templateId]) {
state.currentTemplateFields = state.templateElementsCache[templateId];
return state.templateFieldMaps[templateId];
}
utils.showStatus(`加载模板定义...`, 'processing');
try {
let res = await utils.requestPromise('GET', CONFIG.endpoints.getTemplateElements, { templateId: templateId }, "获取模板字段");
let elements = null;
if (res.success) {
if (Array.isArray(res.data)) elements = res.data;
else if (res.data && Array.isArray(res.data.rows)) elements = res.data.rows;
else if (res.data && Array.isArray(res.data.elements)) elements = res.data.elements;
}
if (elements && elements.length > 0) {
const filteredElements = elements.filter(el => {
const n = el.elementName ? el.elementName.trim() : '';
const cls = el.elementClassifyCode;
const type = el.elementType;
if (cls === 'YFXX') return false;
if (type === 'ContractPartyA' || type === 'ContractPartyB') return false;
if (type === 'ContractBaseElement') {
const excludedNames = [
'合同名称', '合同编号', '甲方经办人', '合同签订地点', '合同签订日期', '合同使用主体',
'合同使用范围', '厂家直采', '合同经销商', '所属机构'
];
if (excludedNames.some(ex => n.includes(ex))) return false;
}
return true;
});
filteredElements.sort((a, b) => (a.sort || 0) - (b.sort || 0));
const map = {};
state.currentTemplateFields = filteredElements;
state.templateElementsCache[templateId] = filteredElements;
elements.forEach(el => {
if (el.elementName) {
map[el.elementName.trim()] = el.elementCode;
}
});
state.templateFieldMaps[templateId] = map;
return map;
} else {
throw new Error("无字段定义");
}
} catch (e) {
console.error("Fetch Error", e);
return {};
}
},
renderDynamicForm: async () => {
const tId = document.getElementById('yz-template-select').value;
const container = document.getElementById('yz-dynamic-form-area');
const partyAContainer = document.getElementById('yz-partya-form-area');
if (!container || !partyAContainer) return;
container.innerHTML = '';
partyAContainer.innerHTML = '';
if (!tId) {
container.innerHTML = '请先选择模板
';
state.currentTemplateFields = [];
return;
}
await actions.fetchTemplateFields(tId);
const elements = state.currentTemplateFields || [];
if (CONFIG.uiInjections && CONFIG.uiInjections.length > 0) {
CONFIG.uiInjections.forEach(rule => {
const triggerElement = elements.find(e => e.elementCode === rule.triggerCode);
const alreadyExists = elements.some(e => e.elementCode === rule.targetField.elementCode);
if (triggerElement && !alreadyExists) {
const newField = { ...rule.targetField };
newField.sort = (triggerElement.sort || 0) + 0.1;
elements.push(newField);
state.templateFieldMaps[tId][rule.targetField.elementName] = rule.targetField.elementCode;
}
});
elements.sort((a, b) => (a.sort || 0) - (b.sort || 0));
}
if (elements.length === 0) {
container.innerHTML = '无需要手动填写的字段
';
return;
}
const linkageMap = {};
const dependentSet = new Set();
elements.forEach(el => {
if (el.customData) {
const opts = utils.parseJSONSafe(el.customData);
if (opts && Array.isArray(opts)) {
opts.forEach(opt => {
const regex = /\$\{([\w\-]+)\}/g;
let match;
while ((match = regex.exec(opt.name)) !== null) {
const depCode = match[1];
if (!linkageMap[el.elementCode]) linkageMap[el.elementCode] = {};
if (!linkageMap[el.elementCode][opt.value]) linkageMap[el.elementCode][opt.value] = [];
linkageMap[el.elementCode][opt.value].push(depCode);
dependentSet.add(depCode);
}
});
}
}
});
const renderField = (el, parentContainer) => {
const name = el.elementName.trim();
const code = el.elementCode;
const wrapper = document.createElement('div');
wrapper.className = 'yz-form-item';
if (dependentSet.has(code)) {
wrapper.classList.add('dependent-field');
wrapper.style.display = 'none';
wrapper.dataset.dependent = code;
} else if (name.length > 8) {
wrapper.style.gridColumn = "span 2";
}
const isReq = el.required === true || el.required === 'true';
const label = document.createElement('span');
label.className = `yz-form-label ${isReq ? 'required' : ''}`;
label.innerText = name;
wrapper.appendChild(label);
let input;
const createSelect = () => {
const sel = document.createElement('select'); sel.className = 'yz-input';
sel.innerHTML = '';
return sel;
};
let implicitMatch = CONFIG.implicitFields.find(f => f.match(code, name));
if (implicitMatch) {
input = createSelect();
if (implicitMatch.domId) input.id = implicitMatch.domId;
input.addEventListener('focus', () => actions.loadFromInterface(name, {
dataSourceUrl: implicitMatch.api.dataSourceUrl,
requestMethod: implicitMatch.api.requestMethod,
params: implicitMatch.api.params
}, input));
}
else if (el.customData && (el.sourceType === 'CUSTOM' || el.showType === 'selectAndInput' || el.showType === 'select' || el.showType === 'radio')) {
input = createSelect();
const opts = utils.parseJSONSafe(el.customData);
if (opts && Array.isArray(opts)) {
opts.forEach(o => {
const option = document.createElement('option');
option.value = o.value;
option.text = o.name.replace(/\$\{[^}]+\}/g, '').replace(/""/g, '').trim();
input.appendChild(option);
});
const def = opts.find(o => o.isDefault); if (def) input.value = def.value;
}
}
else if (el.sourceType === 'INTERFACE') {
input = createSelect();
input.addEventListener('focus', () => actions.loadFromInterface(name, el, input));
}
else if (name.includes("运输条款") && !el.customData) {
input = createSelect(); input.innerHTML = '';
}
else if (el.dictionaryCode) {
input = createSelect();
input.addEventListener('focus', () => actions.loadFromInterface(name, { params: JSON.stringify({ dictCode: el.dictionaryCode }) }, input));
}
else if (el.showType === 'select' || el.showType === 'radio') {
input = createSelect();
}
else {
input = document.createElement('input'); input.className = 'yz-input'; input.type = 'text';
}
input.dataset.label = name;
input.dataset.code = code;
if (state.defaultPartyA) {
const pa = state.defaultPartyA;
if (code === 'partyAiName') { /* 通常是选择框,暂不处理文本值以免冲突 */ }
else if (code === 'partyAiLegalPerson') input.value = pa.legalRepresentative || '';
else if (code === 'partyAiTelPhone') input.value = pa.telephone || '';
else if (code === 'partyAiTaxpayerCode') input.value = pa.businessLicence || '';
else if (code === 'partyAiOpenedBankName') input.value = pa.bankName || '';
else if (code === 'partyAiOpenedBankAccount') input.value = pa.ownerAccount || '';
else if (code === 'partyAiPartyAddress') input.value = pa.registrationAddress || '';
}
if (el.showValue && !input.value) {
if (input.tagName === 'SELECT') {
input.value = el.showValue;
if (!input.value && el.showValue) {
for (let i = 0; i < input.options.length; i++) {
if (input.options[i].text === el.showValue || input.options[i].text.includes(el.showValue)) {
input.selectedIndex = i;
break;
}
}
}
if (!input.value) input.dataset.pendingDefault = el.showValue;
} else {
input.value = el.showValue;
}
}
wrapper.appendChild(input);
parentContainer.appendChild(wrapper);
if (linkageMap[code]) {
input.addEventListener('change', (e) => {
const val = e.target.value;
const targetCodes = linkageMap[code][val] || [];
let allPotential = [];
Object.values(linkageMap[code]).forEach(arr => allPotential = allPotential.concat(arr));
allPotential.forEach(dep => {
const depWrapper = document.querySelector(`.yz-form-item[data-dependent="${dep}"]`);
if (depWrapper) {
depWrapper.style.display = targetCodes.includes(dep) ? 'flex' : 'none';
}
});
});
setTimeout(() => input.dispatchEvent(new Event('change')), 100);
}
if (el.mapRelation) {
try {
const mapRel = JSON.parse(el.mapRelation);
if (mapRel && mapRel.map && mapRel.map.length > 0) {
input.addEventListener('change', function () {
if (this.tagName === 'SELECT' && this.selectedIndex >= 0) {
const opt = this.options[this.selectedIndex];
if (opt && opt.dataset.fullData) {
const data = JSON.parse(opt.dataset.fullData);
mapRel.map.forEach(mapping => {
const sourceKey = mapping.apiCode;
const targetCode = mapping.businessCode;
const targetInput = document.getElementById('yz-dynamic-form-area').querySelector(`[data-code="${targetCode}"]`);
if (targetInput) {
const val = data[sourceKey];
targetInput.value = val || '';
targetInput.dispatchEvent(new Event('change'));
targetInput.style.transition = 'background-color 0.3s';
targetInput.style.backgroundColor = '#d9f7be';
setTimeout(() => targetInput.style.backgroundColor = '', 800);
}
});
}
}
});
}
} catch (e) { console.warn("MapRelation parse error", e); }
}
};
const elementMap = {};
elements.forEach(e => elementMap[e.elementCode] = e);
elements.forEach(el => {
if (dependentSet.has(el.elementCode)) return;
const isPartyA = el.elementClassifyCode === 'JFXX' || el.elementCode.startsWith('partyAi');
const targetContainer = isPartyA ? partyAContainer : container;
renderField(el, targetContainer);
if (linkageMap[el.elementCode]) {
const myDependents = new Set();
Object.values(linkageMap[el.elementCode]).forEach(arr => arr.forEach(c => myDependents.add(c)));
myDependents.forEach(depCode => {
const depEl = elementMap[depCode];
if (depEl) {
const isDepPartyA = depEl.elementClassifyCode === 'JFXX' || depEl.elementCode.startsWith('partyAi');
renderField(depEl, isDepPartyA ? partyAContainer : container);
}
});
}
});
utils.showStatus(`动态表单就绪`);
},
loadFromInterface: (key, elDef, inputEl) => {
if (inputEl.options.length > 1) return;
if (state.loadingDicts[key]) return; state.loadingDicts[key] = true;
inputEl.innerHTML = '';
const method = elDef.requestMethod || 'GET';
let url = elDef.dataSourceUrl;
let params = typeof elDef.params === 'string' ? (utils.parseJSONSafe(elDef.params) || {}) : (elDef.params || {});
if (url) {
if (!url.startsWith('/')) url = '/' + url;
if (!url.startsWith('/api-gateway') && !url.startsWith('http')) {
url = '/api-gateway' + url;
}
utils.request(method, url, params, (err, data) => {
actions.handleInterfaceResponse(key, inputEl, err, data);
});
}
else if (params.dictCode) {
utils.request('GET', CONFIG.endpoints.getDict, { dictCode: params.dictCode }, (err, data) => {
actions.handleInterfaceResponse(key, inputEl, err, data);
});
} else {
state.loadingDicts[key] = false;
inputEl.innerHTML = '';
}
},
handleInterfaceResponse: (key, inputEl, err, data) => {
state.loadingDicts[key] = false;
if (!err && data.success) {
inputEl.innerHTML = '';
let list = [];
if (Array.isArray(data.data)) list = data.data;
else if (data.data && Array.isArray(data.data.rows)) list = data.data.rows;
else if (data.data && Array.isArray(data.data.list)) list = data.data.list;
list.forEach(i => {
const opt = document.createElement('option');
const val = i.id || i.code || i.value || i.dataValue;
const txt = i.name || i.text || i.dataName || val;
if (val !== undefined && val !== null) {
opt.value = val;
opt.text = txt;
opt.dataset.fullData = JSON.stringify(i);
opt.dataset.name = txt;
opt.dataset.code = val;
inputEl.appendChild(opt);
}
});
const pendingVal = inputEl.dataset.pendingDefault;
if (pendingVal) {
inputEl.value = pendingVal;
if (!inputEl.value) {
for (let i = 0; i < inputEl.options.length; i++) {
if (inputEl.options[i].text === pendingVal || inputEl.options[i].text.includes(pendingVal)) {
inputEl.selectedIndex = i;
break;
}
}
}
}
} else {
inputEl.innerHTML = '';
console.error("Interface Load Error", err || data);
}
},
openTreeSelector: (title, treeData, onSelect) => {
if (!treeData || treeData.length === 0) { actions.fetchTreeData(); return alert("数据正在加载中或为空,请等待 1-2 秒后重试..."); }
const overlay = document.createElement('div'); overlay.className = 'yz-modal-overlay';
const modal = document.createElement('div'); modal.className = 'yz-modal'; modal.style.height = '600px';
const header = document.createElement('div'); header.className = 'yz-modal-header'; header.innerText = title;
const body = document.createElement('div'); body.className = 'yz-modal-body';
const searchInput = document.createElement('input'); searchInput.className = 'yz-input yz-tree-search'; searchInput.placeholder = '输入关键词搜索...'; body.appendChild(searchInput);
const treeContainer = document.createElement('div'); body.appendChild(treeContainer);
const renderTree = (nodes, parentUl) => {
nodes.forEach(node => {
const li = document.createElement('li'); li.className = 'yz-tree-node'; li.dataset.name = node.name;
const content = document.createElement('div'); content.className = 'yz-tree-content';
const hasChildren = node.children && node.children.length > 0;
const toggle = document.createElement('span'); toggle.className = 'yz-tree-toggle'; toggle.innerText = hasChildren ? '+' : '•'; if (!hasChildren) toggle.style.visibility = 'hidden'; content.appendChild(toggle);
const label = document.createElement('span'); label.className = 'yz-tree-label'; label.innerText = node.name; content.appendChild(label);
li.appendChild(content); parentUl.appendChild(li);
if (hasChildren) {
const childUl = document.createElement('ul'); childUl.className = 'yz-tree-ul yz-tree-children'; renderTree(node.children, childUl); li.appendChild(childUl);
toggle.onclick = (e) => { e.stopPropagation(); const isHidden = childUl.style.display !== 'block'; childUl.style.display = isHidden ? 'block' : 'none'; toggle.innerText = isHidden ? '-' : '+'; };
}
label.onclick = () => { onSelect({ code: node.code || node.id, name: node.name }); document.body.removeChild(overlay); };
});
};
const rootUl = document.createElement('ul'); rootUl.className = 'yz-tree-ul root'; renderTree(treeData, rootUl); treeContainer.appendChild(rootUl);
searchInput.addEventListener('input', (e) => {
const k = e.target.value.trim().toLowerCase();
treeContainer.querySelectorAll('.yz-tree-node').forEach(n => {
n.style.display = (!k || n.dataset.name.toLowerCase().includes(k)) ? 'block' : 'none';
if (k && n.style.display === 'block') { let p = n.parentElement; while (p && p.classList.contains('yz-tree-children')) { p.style.display = 'block'; p = p.parentElement.parentElement; } }
});
});
const footer = document.createElement('div'); footer.className = 'yz-modal-footer';
const closeBtn = document.createElement('button'); closeBtn.className = 'yz-btn'; closeBtn.innerText = '关闭'; closeBtn.onclick = () => document.body.removeChild(overlay);
footer.appendChild(closeBtn); modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay);
},
addScope: () => {
const areaInput = document.getElementById('yz-in-area');
const orgInput = document.getElementById('yz-in-org');
if (!areaInput.dataset.code || !orgInput.dataset.code) return alert("请先选择区域和组织");
state.userScopes.push({ area: { code: areaInput.dataset.code, name: areaInput.value }, org: { code: orgInput.dataset.code, name: orgInput.value } });
actions.renderScopeList();
areaInput.value = ''; areaInput.dataset.code = ''; orgInput.value = ''; orgInput.dataset.code = '';
},
removeScope: (index) => { state.userScopes.splice(index, 1); actions.renderScopeList(); },
renderScopeList: () => {
const container = document.getElementById('yz-scope-list'); container.innerHTML = '';
state.userScopes.forEach((s, idx) => {
const div = document.createElement('div'); div.className = 'yz-scope-item';
div.innerHTML = `${s.area.name} - ${s.org.name}
删除
`;
container.appendChild(div);
});
},
promptUserForApprovers: (contractName, candidatesData) => {
return new Promise((resolve) => {
if (!candidatesData || candidatesData.length === 0) { resolve([]); return; }
const overlay = document.createElement('div'); overlay.className = 'yz-modal-overlay';
const modal = document.createElement('div'); modal.className = 'yz-modal';
const header = document.createElement('div'); header.className = 'yz-modal-header'; header.innerText = `📋 确认审批人 - ${contractName}`;
const body = document.createElement('div'); body.className = 'yz-modal-body';
candidatesData.forEach(node => {
const group = document.createElement('div'); group.className = 'yz-node-group';
const title = document.createElement('div'); title.className = 'yz-node-title';
if (node.isError) title.classList.add('error-node');
title.innerHTML = `${node.nodeName} (${node.nodeId})`;
group.appendChild(title);
if (node.isError) {
const errorTip = document.createElement('div');
errorTip.className = 'yz-error-tip';
errorTip.innerText = '⚠️ 系统计算失败,请检查流程配置。';
group.appendChild(errorTip);
}
const uniqueUsers = [];
const seenIds = new Set();
if (node.users && node.users.length > 0) {
node.users.forEach(u => {
if (!seenIds.has(u.id)) {
seenIds.add(u.id);
uniqueUsers.push(u);
}
});
}
if (uniqueUsers.length > 0) {
uniqueUsers.forEach(user => {
const label = document.createElement('label'); label.className = 'yz-user-item';
const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = user.id; checkbox.dataset.name = user.name; checkbox.checked = true;
if (!user.id || user.id === 'Auto' || user.id.length < 5) {
checkbox.disabled = true;
checkbox.checked = false;
}
label.appendChild(checkbox);
const infoDiv = document.createElement('div');
let infoHtml = `${user.name}`;
if (user.positionName) {
infoHtml += ` ${user.positionName}`;
}
infoHtml += `
`;
if (user.organizationName) {
infoHtml += `${user.organizationName}
`;
}
infoDiv.innerHTML = infoHtml;
label.appendChild(infoDiv); group.appendChild(label);
});
} else {
const empty = document.createElement('div'); empty.style.color = 'red'; empty.innerText = '❌ 无候选人'; group.appendChild(empty);
}
group.dataset.nodeId = node.nodeId;
group.dataset.userVarName = node.userVarName || "";
group.dataset.flowTaskType = node.flowTaskType;
body.appendChild(group);
});
const footer = document.createElement('div'); footer.className = 'yz-modal-footer';
const cancelBtn = document.createElement('button'); cancelBtn.className = 'yz-btn'; cancelBtn.innerText = '跳过'; cancelBtn.onclick = () => { document.body.removeChild(overlay); resolve(null); };
const confirmBtn = document.createElement('button'); confirmBtn.className = 'yz-btn'; confirmBtn.innerText = '提交'; confirmBtn.onclick = () => {
const res = [];
body.querySelectorAll('.yz-node-group').forEach(g => {
const nodeId = g.dataset.nodeId;
const userVarName = g.dataset.userVarName;
const flowTaskType = g.dataset.flowTaskType;
const checked = Array.from(g.querySelectorAll('input[type="checkbox"]:checked'));
if (checked.length) {
res.push({
actTaskDefKey: nodeId,
executorIds: checked.map(c => c.value).join(','),
executorNames: checked.map(c => c.dataset.name).join(','),
instancyStatus: false,
nodeType: flowTaskType === 'countersign' ? 'CounterSign' : 'Approve',
nodeId: nodeId,
userVarName: userVarName,
flowTaskType: flowTaskType,
userIds: checked.map(c => c.value).join(','),
solidifyFlow: true
});
} else {
res.push({
actTaskDefKey: nodeId,
executorIds: "",
executorNames: "",
instancyStatus: false,
nodeType: flowTaskType === 'countersign' ? 'CounterSign' : 'Approve',
nodeId: nodeId,
userVarName: userVarName,
flowTaskType: flowTaskType,
userIds: "",
solidifyFlow: true
});
}
});
document.body.removeChild(overlay); resolve(res);
};
footer.appendChild(cancelBtn); footer.appendChild(confirmBtn); modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay);
});
},
exportToExcel: () => {
if (!state.exportData.length) return alert("无数据导出");
const wb = XLSX.utils.book_new(); const ws = XLSX.utils.json_to_sheet(state.exportData);
XLSX.utils.book_append_sheet(wb, ws, "结果"); XLSX.writeFile(wb, `合同导出_${new Date().toISOString().slice(0, 10)}.xlsx`);
},
updatePaymentRowIndices: () => document.querySelectorAll('.yz-payment-row').forEach((r, i) => r.querySelector('.yz-payment-title-text').textContent = `付款节点 #${i + 1}`),
updateSelect: (id, items, k, v) => {
const sel = document.getElementById(id); if (!sel) return; sel.innerHTML = '';
items.forEach(i => { const opt = document.createElement('option'); opt.value = i[k]; opt.text = i[v]; sel.appendChild(opt); });
},
handlePaymentLogic: (targetInput) => {
const allPercents = Array.from(document.querySelectorAll('.yz-pay-percent'));
const idx = allPercents.indexOf(targetInput);
if (idx === -1) return;
let val = parseFloat(targetInput.value);
if (val > 100) {
targetInput.value = 100;
val = 100;
}
for (let i = idx; i < allPercents.length - 1; i++) {
const curr = parseFloat(allPercents[i].value);
const nextInput = allPercents[i + 1];
const next = parseFloat(nextInput.value) || 0;
if (!isNaN(curr) && (isNaN(next) || next < curr)) {
nextInput.value = curr;
nextInput.style.backgroundColor = '#fff1f0';
setTimeout(() => nextInput.style.backgroundColor = '', 300);
}
}
},
getPaymentPresets: () => {
try {
return JSON.parse(localStorage.getItem('yz_payment_presets_v1') || '[]');
} catch (e) { return []; }
},
savePaymentPreset: () => {
const rows = document.querySelectorAll('.yz-payment-row');
if (!rows.length) return alert('没有付款节点可保存');
const presetName = prompt('请输入预设名称:');
if (!presetName || !presetName.trim()) return;
const payments = Array.from(rows).map(row => {
const getOpt = (sel) => {
if (sel && sel.selectedIndex >= 0) {
const opt = sel.options[sel.selectedIndex];
return { value: opt.value, name: opt.textContent };
}
return { value: '', name: '' };
};
return {
windUp: getOpt(row.querySelector('[data-type="windUp"]')),
stage: getOpt(row.querySelector('[data-type="stage"]')),
method: getOpt(row.querySelector('[data-type="method"]')),
premise: (row.querySelector('[id^="yz-in-pay-premise-"]') || {}).value || '',
period: (row.querySelector('[id^="yz-in-pay-period-"]') || {}).value || '',
percent: (row.querySelector('[id^="yz-in-pay-percent-"]') || {}).value || '',
remark: (row.querySelector('[id^="yz-in-pay-remark-"]') || {}).value || ''
};
});
const presets = actions.getPaymentPresets();
const existingIdx = presets.findIndex(p => p.name === presetName.trim());
if (existingIdx >= 0) {
if (!confirm(`预设"${presetName.trim()}"已存在,是否覆盖?`)) return;
presets[existingIdx] = { name: presetName.trim(), payments };
} else {
presets.push({ name: presetName.trim(), payments });
}
localStorage.setItem('yz_payment_presets_v1', JSON.stringify(presets));
actions.refreshPresetSelect();
utils.showStatus('预设已保存', 'success');
},
loadPaymentPreset: async (presetName) => {
if (!presetName) return;
const presets = actions.getPaymentPresets();
const preset = presets.find(p => p.name === presetName);
if (!preset) return alert('未找到预设');
const dicts = [
{ sk: 'windUpMethods', code: 'windUpMethod' },
{ sk: 'paymentStages', code: 'paymentStage' },
{ sk: 'paymentMethods', code: 'paymentMethod' }
];
for (const d of dicts) {
if (!state[d.sk] || !state[d.sk].length) {
try {
const res = await utils.requestPromise('GET', CONFIG.endpoints.getDict + '?dictCode=' + d.code, null, '加载字典');
if (res.success && res.data) state[d.sk] = res.data;
} catch (e) { console.warn('加载字典失败', d.code, e); }
}
}
const container = document.getElementById('yz-payment-container');
container.innerHTML = '';
if (preset.payments.length === 0) {
actions.updatePaymentRowIndices();
return;
}
const populateRow = (row, p) => {
const setSelect = (sel, data, sk) => {
if (!sel) return;
const items = state[sk] || [];
if (items.length) actions.updateSelect(sel.id, items, 'dataValue', 'dataName');
if (!data.value) return;
for (let i = 0; i < sel.options.length; i++) {
if (sel.options[i].value === data.value) {
sel.selectedIndex = i;
return;
}
}
const opt = document.createElement('option');
opt.value = data.value;
opt.text = data.name;
opt.selected = true;
sel.add(opt);
};
setSelect(row.querySelector('[data-type="windUp"]'), p.windUp, 'windUpMethods');
setSelect(row.querySelector('[data-type="stage"]'), p.stage, 'paymentStages');
setSelect(row.querySelector('[data-type="method"]'), p.method, 'paymentMethods');
const premiseInput = row.querySelector('[id^="yz-in-pay-premise-"]');
if (premiseInput) premiseInput.value = p.premise;
const periodInput = row.querySelector('[id^="yz-in-pay-period-"]');
if (periodInput) periodInput.value = p.period;
const percentInput = row.querySelector('[id^="yz-in-pay-percent-"]');
if (percentInput) percentInput.value = p.percent;
const remarkInput = row.querySelector('[id^="yz-in-pay-remark-"]');
if (remarkInput) remarkInput.value = p.remark;
};
for (let idx = 0; idx < preset.payments.length; idx++) {
actions.addPaymentRow();
const row = document.querySelectorAll('.yz-payment-row')[idx];
if (row) populateRow(row, preset.payments[idx]);
}
utils.showStatus(`已加载预设: ${presetName}`, 'success');
},
deletePaymentPreset: (presetName) => {
if (!presetName) return;
if (!confirm(`确认删除预设"${presetName}"?`)) return;
const presets = actions.getPaymentPresets().filter(p => p.name !== presetName);
localStorage.setItem('yz_payment_presets_v1', JSON.stringify(presets));
actions.refreshPresetSelect();
utils.showStatus(`已删除预设: ${presetName}`, 'success');
},
refreshPresetSelect: () => {
const sel = document.getElementById('yz-preset-select');
if (!sel) return;
const presets = actions.getPaymentPresets();
sel.innerHTML = '';
presets.forEach(p => {
const opt = document.createElement('option');
opt.value = p.name;
opt.textContent = p.name;
sel.appendChild(opt);
});
},
addPaymentRow: () => {
state.paymentRowCounter++; const uid = state.paymentRowCounter; const c = document.getElementById('yz-payment-container');
const d = document.createElement('div'); d.className = 'yz-payment-row'; d.id = `yz-pay-row-${uid}`;
d.innerHTML = `
`;
c.appendChild(d); actions.updatePaymentRowIndices();
d.querySelector('.yz-btn-danger').onclick = () => {
d.remove();
actions.updatePaymentRowIndices();
const remaining = document.querySelectorAll('.yz-pay-percent');
if (remaining.length > 0) actions.handlePaymentLogic(remaining[0]);
};
const bf = (id, k, s, sk) => document.getElementById(id).addEventListener('focus', () => {
if (state[sk] && state[sk].length) return actions.updateSelect(id, state[sk], 'dataValue', 'dataName');
if (state.loadingDicts[k]) return; state.loadingDicts[k] = true;
utils.request('GET', CONFIG.endpoints.getDict, { dictCode: s }, (err, data) => {
state.loadingDicts[k] = false; if (!err && data.success) { state[sk] = data.data; actions.updateSelect(id, state[sk], 'dataValue', 'dataName'); }
});
});
bf(`yz-in-pay-windUp-${uid}`, `windUp-${uid}`, 'windUpMethod', 'windUpMethods'); bf(`yz-in-pay-stage-${uid}`, `stage-${uid}`, 'paymentStage', 'paymentStages'); bf(`yz-in-pay-method-${uid}`, `method-${uid}`, 'paymentMethod', 'paymentMethods');
const percentInput = d.querySelector('.yz-pay-percent');
percentInput.addEventListener('input', function () {
actions.handlePaymentLogic(this);
});
percentInput.addEventListener('change', function () {
const allPercents = Array.from(document.querySelectorAll('.yz-pay-percent'));
const idx = allPercents.indexOf(this);
if (idx > 0) {
const prev = parseFloat(allPercents[idx - 1].value) || 0;
const curr = parseFloat(this.value) || 0;
if (curr < prev) {
this.value = prev;
actions.handlePaymentLogic(this);
}
}
});
},
manualSubmitApproval: async () => {
const rawText = document.getElementById('yz-manual-contract-code').value.trim();
if (!rawText) return alert("请输入合同编号或名称");
const keys = utils.parseCodeList(rawText);
if (keys.length === 0) return alert("请输入有效的合同编号");
if (!confirm(`识别到 ${keys.length} 个合同,将依次发起审批。\n\n提示:系统会自动复用第一份合同选择的审批流程和人员,请确保这些合同的审批逻辑一致。`)) return;
const btn = document.getElementById('yz-btn-manual-submit');
const originalText = btn.innerText;
btn.disabled = true;
let successCount = 0;
let failCount = 0;
let cachedFlowKey = null;
let cachedExecutors = null;
try {
for (let i = 0; i < keys.length; i++) {
const codeOrName = keys[i];
btn.innerText = `处理中 (${i + 1}/${keys.length})...`;
console.log(`[Batch Manual] Processing: ${codeOrName}`);
try {
const searchRes = await utils.requestPromise('POST', CONFIG.endpoints.searchContract, {
"quickSearchValue": codeOrName,
"quickSearchProperties": ["code"],
"pageInfo": { "page": 1, "rows": 10 }
}, `查询合同[${codeOrName}]`);
const contract = searchRes.data?.rows?.[0];
if (!contract) throw new Error(`未找到合同: ${codeOrName}`);
if (!cachedFlowKey) {
const flowList = await actions.getAvailableFlows(contract.id, contract.name);
const selected = await actions.promptFlowSelection(contract.name, flowList);
if (!selected) throw new Error("取消审批");
cachedFlowKey = selected;
}
const req = await utils.requestPromise('POST', CONFIG.endpoints.createRequest, { contractId: contract.id, name: contract.name });
if (!req.success || !req.data?.requestId) throw new Error("创建流程请求失败");
const requestId = req.data.requestId;
const dynModelCode = req.data.businessModelCode;
if (cachedExecutors) {
await utils.requestPromise('POST', CONFIG.endpoints.saveSolidify, {
businessModelCode: dynModelCode,
businessId: requestId,
executorsVos: JSON.stringify(cachedExecutors)
});
} else {
const pre = await utils.requestPromise('POST', CONFIG.endpoints.startFlowNew, {
businessKey: requestId,
businessModelCode: dynModelCode,
typeId: null,
flowDefKey: cachedFlowKey
});
if (!pre.success || !pre.data?.flowDefinationId) throw new Error("预发起失败");
const ent = await utils.requestPromise('POST', `${CONFIG.endpoints.getEntity}?id=${pre.data.flowDefinationId}&versionCode=-1&businessId=${requestId}&businessModelCode=${dynModelCode}`, {});
let nodesMap = {};
try { nodesMap = JSON.parse(ent.data.defJson).process.nodes; } catch (e) { }
const solidEx = ent.data?.solidifyExecutorOfOnly || {};
const flowNodeInfos = pre.data.nodeInfoList || [];
const cands = [];
for (let nid in nodesMap) {
const node = nodesMap[nid];
if (node.type === 'UserTask' && !(node.nodeType === 'Normal' && node.name.includes("修改"))) {
let u = [];
let calcError = false;
if (node.nodeConfig && node.nodeConfig.executor && Array.isArray(node.nodeConfig.executor)) {
try {
const rawExecutors = node.nodeConfig.executor.map(ex => {
const newEx = { ...ex };
delete newEx.rowdata;
if (ex.userType === "SelfDefinition") {
newEx.ids = ex.selfDefId || ex.selfDefOfOrgAndSelId || null;
newEx.customParam = ex.customParam || null;
newEx.allowDefaultStarter = node.nodeConfig.normal?.allowDefaultStarter || null;
newEx.repeatExecutorPass = node.nodeConfig.normal?.repeatExecutorPass || null;
} else {
if (newEx.ids === "") newEx.ids = null;
if (newEx.customParam === "") newEx.customParam = null;
}
return newEx;
});
if (rawExecutors.length > 0) {
const calcPayload = {
requestExecutorsVos: JSON.stringify(rawExecutors),
businessModelCode: dynModelCode,
businessId: requestId
};
const calcRes = await utils.requestPromise('POST', CONFIG.endpoints.getCalculatedUsers, calcPayload, `计算${node.name}`);
if (calcRes.success && calcRes.data && Array.isArray(calcRes.data)) {
calcRes.data.forEach(user => {
u.push({
id: user.userId || user.id,
name: user.userName || user.name,
positionName: user.positionName || '',
organizationName: user.organizationName || ''
});
});
} else { calcError = true; }
}
} catch (err) { calcError = true; }
}
if (u.length === 0 && solidEx[nid] && solidEx[nid].executorIds) {
const ids = solidEx[nid].executorIds.split(',');
const nms = solidEx[nid].executorNames.split(',');
u = ids.map((id, x) => ({ id: id, name: nms[x] || '', positionName: '', organizationName: '' }));
}
let matchedVarName = null;
if (flowNodeInfos) {
const infoMatch = flowNodeInfos.find(n => n.id === nid);
if (infoMatch) matchedVarName = infoMatch.userVarName;
}
if (u.length > 0 || calcError || (node.nodeConfig && node.nodeConfig.executor && node.nodeConfig.executor.length > 0)) {
cands.push({
nodeId: nid,
nodeName: node.name,
users: u,
flowTaskType: node.nodeType === 'CounterSign' ? 'countersign' : 'approve',
userVarName: matchedVarName,
isError: calcError
});
}
}
}
if (cands.length === 0) {
const fallbackNodes = Object.keys(solidEx);
fallbackNodes.forEach(nid => {
let u = [];
if (solidEx[nid] && solidEx[nid].executorIds) {
const ids = solidEx[nid].executorIds.split(','); const nms = solidEx[nid].executorNames.split(',');
u = ids.map((id, x) => ({ id: id, name: nms[x] || '', positionName: '', organizationName: '' }));
}
if (u.length) cands.push({ nodeId: nid, nodeName: solidEx[nid]?.actTaskDefKey || nid, users: u, flowTaskType: solidEx[nid]?.nodeType === 'CounterSign' ? 'countersign' : 'approve' });
});
}
const sel = await actions.promptUserForApprovers(contract.code, cands);
if (!sel || !sel.length) throw new Error("未选择审批人或取消");
cachedExecutors = sel;
await utils.requestPromise('POST', CONFIG.endpoints.saveSolidify, {
businessModelCode: dynModelCode,
businessId: requestId,
executorsVos: JSON.stringify(sel)
});
}
const startNodes = cachedExecutors.map(s => ({
nodeId: s.nodeId,
userVarName: s.userVarName,
flowTaskType: s.flowTaskType,
userIds: s.userIds,
solidifyFlow: true
}));
await utils.requestPromise('POST', CONFIG.endpoints.startFlowNew, {
businessKey: requestId,
businessModelCode: dynModelCode,
typeId: null,
flowDefKey: cachedFlowKey,
taskList: JSON.stringify(startNodes)
});
console.log(`[Batch Manual] Success: ${codeOrName}`);
successCount++;
} catch (e) {
console.error(`[Batch Manual] Failed: ${codeOrName}`, e);
if (i === 0 && e.message === "取消审批") {
throw e;
}
failCount++;
utils.showStatus(`合同 ${codeOrName} 失败: ${e.message}`, 'error');
await new Promise(r => setTimeout(r, 1000));
}
}
alert(`批量发起完成!\n成功: ${successCount}\n失败: ${failCount}`);
} catch (e) {
alert("操作终止: " + e.message);
} finally {
btn.innerText = originalText;
btn.disabled = false;
}
},
manualUploadAttachments: async () => {
const fileInput = document.getElementById('yz-in-file');
const files = fileInput && fileInput.files.length > 0 ? fileInput.files : [];
if (files.length === 0) return alert("请先在上方选择要上传的附件文件!");
const rawText = document.getElementById('yz-manual-upload-code').value.trim();
if (!rawText) return alert("请输入需要上传附件的合同编号或名称");
const keys = utils.parseCodeList(rawText);
if (keys.length === 0) return alert("请输入有效的合同编号");
if (!confirm(`准备为 ${keys.length} 个合同独立匹配并上传附件。`)) return;
const btn = document.getElementById('yz-btn-manual-upload');
const originalText = btn.innerText;
btn.disabled = true;
let successCount = 0, failCount = 0, skipCount = 0;
try {
for (let i = 0; i < keys.length; i++) {
btn.innerText = `上传中 (${i + 1}/${keys.length})...`;
try {
const sRes = await utils.requestPromise('POST', CONFIG.endpoints.searchContract, { "quickSearchValue": keys[i], "pageInfo": { "page": 1, "rows": 10 } });
const c = sRes.data?.rows?.[0];
if (!c) throw new Error(`未在系统中查到合同`);
const matchedFiles = Array.from(files).filter(f => f.name.includes(c.code) || f.name.includes(c.contractCode) || (c.name && f.name.includes(c.name)));
if (matchedFiles.length === 0) { skipCount++; continue; }
for (let j = 0; j < matchedFiles.length; j++) {
const file = matchedFiles[j];
const u1 = await utils.requestPromise('POST', `${CONFIG.endpoints.uploadFile}?companyCode=100&waterMark=true&toPdf=false`, utils.buildFileFormData(file));
if (u1.data?.docId) {
await utils.requestPromise('POST', CONFIG.endpoints.saveArchive, { contractId: c.id, contractOtherDocIds: [u1.data.docId], approveReferenceDocIds: [] });
await utils.sleep(1500);
let u2 = null, uploadSuccess = false;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
if (attempt > 1) { btn.innerText = `PDF重试中 (${attempt}/3)...`; await utils.sleep(3000); }
u2 = await utils.requestPromise('POST', `${CONFIG.endpoints.uploadFile}?companyCode=100&waterMark=true&toPdf=true`, utils.buildFileFormData(file));
uploadSuccess = true; break;
} catch (e) { if (attempt === 3) throw e; }
}
if (uploadSuccess && u2.data?.docId) await utils.requestPromise('POST', `${CONFIG.endpoints.saveArchiveBatch}?fileIds=${u2.data.docId}&relationType=1&entityId=${c.id}`, {});
}
}
successCount++;
} catch (e) { failCount++; }
await utils.sleep(500);
}
alert(`独立附件批量上传完成!\n\n成功: ${successCount}\n跳过(未匹配): ${skipCount}\n失败: ${failCount}`);
} catch (e) { alert("操作终止: " + e.message); }
finally { btn.innerText = originalText; btn.disabled = false; }
},
generateAttachMappingTable: async () => {
try {
const fileInput = document.getElementById('yz-in-file');
if (!fileInput) return alert("找不到附件上传框!");
const files = fileInput.files && fileInput.files.length > 0 ? Array.from(fileInput.files) : [];
if (files.length === 0) return alert("请先在上方选择附件文件!");
let rowData = [];
const excelInput = document.getElementById('yz-in-attach-excel');
const textInput = document.getElementById('yz-manual-upload-code');
const rawText = textInput ? textInput.value.trim() : "";
if (excelInput && excelInput.files && excelInput.files.length > 0) {
const btn = document.getElementById('yz-btn-generate-attach-map');
const originalText = btn.innerText;
btn.innerText = "解析 Excel 中...";
try {
const xlsData = await utils.readExcelFile(excelInput.files[0]);
if (!xlsData || xlsData.length === 0) throw new Error("Excel 数据为空");
xlsData.forEach(row => {
const code = (row['合同编号'] || '');
const attachName = (row['合同附件名称'] || '');
if (code && String(code).trim()) rowData.push({ code: String(code).trim(), attachName: String(attachName).trim() });
});
} catch (e) { btn.innerText = originalText; return alert("解析 Excel 失败:" + e.message); }
btn.innerText = originalText;
} else if (rawText) {
rowData = utils.parseCodeList(rawText).map(c => ({ code: c, attachName: '' }));
}
if (rowData.length === 0) return alert("未获取到有效的合同编号!");
const tbody = document.getElementById('yz-attach-mapping-tbody');
if (!tbody) return alert("找不到表格容器!");
tbody.innerHTML = '';
let optionsHtml = '';
files.forEach((f, idx) => { optionsHtml += ``; });
let autoMatchCount = 0;
rowData.forEach((row, rowIndex) => {
const tr = document.createElement('tr');
tr.dataset.code = row.code;
tr.id = `yz-attach-map-row-${rowIndex}`;
let matchedIndex = -1;
const matchKey = row.attachName || row.code;
if (matchKey) { for (let i = 0; i < files.length; i++) { if (files[i].name.includes(matchKey)) { matchedIndex = i; break; } } }
if (matchedIndex === -1) { const nm = row.code.match(/\d{10,}/); if (nm) { for (let i = 0; i < files.length; i++) { if (files[i].name.includes(nm[0])) { matchedIndex = i; break; } } } }
const selectEl = document.createElement('select');
selectEl.className = 'yz-input'; selectEl.style.width = '100%'; selectEl.innerHTML = optionsHtml;
if (matchedIndex !== -1) { selectEl.value = matchedIndex; selectEl.style.backgroundColor = '#f6ffed'; autoMatchCount++; }
const tdCode = document.createElement('td'); tdCode.innerText = row.code;
const tdSelect = document.createElement('td'); tdSelect.appendChild(selectEl);
const tdStatus = document.createElement('td'); tdStatus.className = 'res-cell'; tdStatus.style.textAlign = 'center'; tdStatus.innerText = '待确认';
tr.appendChild(tdCode); tr.appendChild(tdSelect); tr.appendChild(tdStatus);
tbody.appendChild(tr);
});
const container = document.getElementById('yz-attach-mapping-container');
if (container) container.style.display = 'block';
console.log(`[AttachMap] 清单生成完成 (${autoMatchCount}/${rowData.length})`);
} catch (e) { alert("发生错误:" + e.message); }
},
executeAttachMappedUpload: async () => {
const tbody = document.getElementById('yz-attach-mapping-tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
if (rows.length === 0) return;
const fileInput = document.getElementById('yz-in-file');
const files = Array.from(fileInput.files);
const tasks = [];
for (let tr of rows) {
const code = tr.dataset.code;
const select = tr.querySelector('select');
const fileIndex = select.value;
if (!fileIndex) return alert(`合同 [${code}] 未选择对应的文件!`);
tasks.push({ rowId: tr.id, code, file: files[fileIndex] });
}
if (!confirm(`即将上传 ${tasks.length} 个合同附件,确认?`)) return;
const btn = document.getElementById('yz-btn-execute-attach-map');
const originalText = btn.innerText;
btn.disabled = true;
let successCount = 0, failCount = 0;
const updateRowStatus = (rowId, st, msg) => {
const td = document.getElementById(rowId).querySelector('.res-cell');
td.innerText = st === 'success' ? '成功' : `失败: ${msg}`;
td.style.color = st === 'success' ? '#52c41a' : '#ff4d4f';
};
try {
for (let i = 0; i < tasks.length; i++) {
const task = tasks[i];
btn.innerText = `上传中 (${i + 1}/${tasks.length})...`;
document.getElementById(task.rowId).querySelector('.res-cell').innerText = '上传中';
try {
const c = await utils.searchContractByCode(task.code);
if (!c) throw new Error('查无此合同');
const u1 = await utils.requestPromise('POST', `${CONFIG.endpoints.uploadFile}?companyCode=100&waterMark=true&toPdf=false`, utils.buildFileFormData(task.file));
if (!u1.data?.docId) throw new Error('上传失败');
await utils.requestPromise('POST', CONFIG.endpoints.saveArchive, { contractId: c.id, contractOtherDocIds: [u1.data.docId], approveReferenceDocIds: [] });
await utils.sleep(1500);
let u2 = null, uploadSuccess = false;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
if (attempt > 1) { btn.innerText = `PDF重试 (${attempt}/3)...`; await utils.sleep(3000); }
u2 = await utils.requestPromise('POST', `${CONFIG.endpoints.uploadFile}?companyCode=100&waterMark=true&toPdf=true`, utils.buildFileFormData(task.file));
uploadSuccess = true; break;
} catch (e) { if (attempt === 3) throw e; }
}
if (uploadSuccess && u2.data?.docId) {
await utils.requestPromise('POST', `${CONFIG.endpoints.saveArchiveBatch}?fileIds=${u2.data.docId}&relationType=1&entityId=${c.id}`, {});
}
updateRowStatus(task.rowId, 'success'); successCount++;
} catch (e) { updateRowStatus(task.rowId, 'fail', e.message); failCount++; }
}
alert(`附件上传完成!\n\n成功: ${successCount}\n失败: ${failCount}`);
} catch (e) { alert("操作终止: " + e.message); }
finally { btn.innerText = originalText; btn.disabled = false; }
},
manualOfflineSign: async () => {
const fileInput = document.getElementById('yz-in-sign-file');
const files = fileInput && fileInput.files.length > 0 ? fileInput.files : [];
if (files.length === 0) return alert("请先选择已盖章的 PDF 文件!");
const rawText = document.getElementById('yz-manual-sign-code').value.trim();
if (!rawText) return alert("请输入需要回传盖章文件的合同编号或名称");
const keys = utils.parseCodeList(rawText);
if (keys.length === 0) return alert("请输入有效的合同编号");
if (!confirm(`准备为 ${keys.length} 个合同回传线下盖章文件。\n注意:请确保上方已选中所有对应的 PDF 文件,系统将根据文件名自动匹配。`)) return;
const btn = document.getElementById('yz-btn-manual-sign');
const originalText = btn.innerText;
btn.disabled = true;
let successCount = 0, failCount = 0, skipCount = 0;
try {
for (let i = 0; i < keys.length; i++) {
const codeOrName = keys[i];
btn.innerText = `回传中 (${i + 1}/${keys.length})...`;
console.log(`[Offline Sign] Processing: ${codeOrName}`);
try {
const sRes = await utils.requestPromise('POST', CONFIG.endpoints.searchContract, { "quickSearchValue": codeOrName, "pageInfo": { "page": 1, "rows": 10 } });
const c = sRes.data?.rows?.[0];
if (!c) throw new Error(`未在系统中查到合同`);
const matchedFiles = Array.from(files).filter(f =>
f.name.includes(c.code) ||
f.name.includes(c.contractCode) ||
(c.name && f.name.includes(c.name))
);
if (matchedFiles.length === 0) {
utils.showStatus(`合同 ${codeOrName} 无匹配签章文件,跳过`, 'error');
skipCount++;
continue;
}
const file = matchedFiles[0];
const signInfoRes = await utils.requestPromise('GET', `${CONFIG.endpoints.getSignInfo}?contractId=${c.id}`);
const signInfo = signInfoRes.data;
if (!signInfo || !signInfo.id) throw new Error("获取签章节点信息失败(可能尚未流转至签章节点)");
const f1 = utils.buildFileFormData(file);
const uRes = await utils.requestPromise('POST', `${CONFIG.endpoints.uploadFile}?companyCode=100&waterMark=false&toPdf=true`, f1, "上传签章PDF");
const docId = uRes.data?.docId;
if (!docId) throw new Error("文件上传失败,未返回 docId");
const payload = {
...signInfo,
signType: "OFFLINE_SIGN",
signDate: utils.getNowStr(),
officialAnnex: true,
signFileIdList: [docId],
signFileList: [{
uid: "rc-upload-" + Date.now(),
name: file.name,
size: file.size,
type: file.type || "application/pdf",
status: "done",
docId: docId,
response: {
data: { docId: docId, fileName: file.name, documentType: "Pdf" },
success: true,
error: false,
message: "处理成功!"
}
}]
};
await utils.requestPromise('POST', CONFIG.endpoints.offlineSign, payload, "提交签章回传");
console.log(`[Offline Sign] Success: ${codeOrName}`);
utils.showStatus(`合同 ${codeOrName} 签章回传成功`);
successCount++;
} catch (e) {
console.error(`[Offline Sign] Failed: ${codeOrName}`, e);
failCount++;
utils.showStatus(`合同 ${codeOrName} 失败: ${e.message}`, 'error');
}
await utils.sleep(800);
}
alert(`线下签章批量回传完成!\n\n✅ 成功: ${successCount}\n⏭️ 跳过(未匹配到文件): ${skipCount}\n❌ 失败: ${failCount}`);
} catch (e) {
alert("操作终止: " + e.message);
} finally {
btn.innerText = originalText;
btn.disabled = false;
}
},
searchAndExport: () => {
const v = document.getElementById('yz-search-input').value.trim(); if (!v) return alert("请输入编号");
let tId = document.getElementById('yz-template-select').value;
utils.toggleInputs(true);
const btn = document.getElementById('yz-btn-search'); btn.textContent = '处理中...';
utils.request('POST', CONFIG.endpoints.searchWinBid, { "quickSearchValue": v, "quickSearchProperties": ["tenderProjectName", "tenderProjectCode", "signCorporationName", "signCorporationCode"], "pageInfo": { "page": 1, "rows": 100 }, "sourceSystem": "SECOND_VERSION" }, (err, data) => {
if (err || !data.success) {
btn.textContent = '查询并自动导出';
utils.toggleInputs(false);
return utils.showStatus(data?.message || '查询失败', 'error');
}
state.searchResults = (data.data.rows || []).map((r, i) => ({ ...r, _uiId: `row-${i}`, _status: 'pending' })); actions.renderTable();
if (state.searchResults.length > 0) {
const sel = document.getElementById('yz-template-select');
if (sel && !sel.value && state.searchResults[0].contractTemplateId) {
sel.value = state.searchResults[0].contractTemplateId;
tId = state.searchResults[0].contractTemplateId;
actions.renderDynamicForm();
}
if (!tId && sel.options.length > 1) { sel.selectedIndex = 1; tId = sel.value; actions.renderDynamicForm(); }
if (!tId) {
btn.textContent = '查询并自动导出';
utils.toggleInputs(false);
return alert("未选择合同模板");
}
actions.createDraftOnly(tId, btn);
} else {
btn.textContent = '查询并自动导出';
utils.toggleInputs(false);
utils.showStatus('无数据', 'error');
}
});
},
createDraftOnly: async (templateId, searchBtn) => {
const list = state.searchResults; state.exportData = []; let successCount = 0;
for (let i = 0; i < list.length; i++) {
const item = list[i]; actions.updateRowStatus(item._uiId, 'processing', '创建中...');
try {
const cRes = await utils.requestPromise('POST', CONFIG.endpoints.createDraft, { "fixResultSubId": item.fixResultSubId, "templateId": templateId, "confide": false, "sourceSystem": "SECOND_VERSION" }, "创建草稿");
const cId = (typeof cRes.data === 'string') ? cRes.data : cRes.data?.id;
const dRes = await utils.requestPromise('GET', CONFIG.endpoints.getContractDetail, { id: cId }, "详情");
const d = dRes.data;
const pB = (d.contractParties || []).find(p => p.partyType === 'B') || {};
state.exportData.push({
"合同编号": d.contractCode || d.code,
"合同名称": d.name || d.contractName,
"不含税金额": d.additional?.amount || "",
"合同签订地点": d.additional?.signAddress || "",
"合同附件名称": "",
"乙方名称": pB.partyName,
"乙方法人代表": pB.legalPerson,
"乙方代理人姓名": pB.agentName,
"乙方纳税人身份": pB.taxpayerType,
"乙方纳税人识别号": pB.taxpayerCode,
"乙方开户行": pB.openedBankName,
"乙方开户行账号": pB.openedBankAccount,
"乙方联系电话": pB.telPhone,
"乙方联系邮箱": pB.email,
"乙方地址": pB.partyAddress
});
actions.updateRowStatus(item._uiId, 'success', null, { code: d.contractCode || d.code, taxAmount: d.additional?.taxAmount || 0, stateRemark: "成功" }); successCount++;
} catch (e) { actions.updateRowStatus(item._uiId, 'error', e.message); }
await new Promise(r => setTimeout(r, 300));
}
if (searchBtn) { searchBtn.textContent = "查询并自动导出"; }
utils.toggleInputs(false);
const tplSelect = document.getElementById('yz-template-select');
if (tplSelect) tplSelect.disabled = true;
if (state.exportData.length) actions.exportToExcel();
utils.showStatus(`完成: ${successCount} / ${list.length}`);
},
batchUpdateFromExcel: async () => {
const fd = utils.getFormData();
if (!fd.excelFile) return alert("请上传Excel");
const taxInput = document.getElementById('yz-dynamic-tax');
if (taxInput && !taxInput.value) return alert("请选择税率");
if (!confirm(`确认更新?`)) return;
utils.toggleInputs(true);
const btn = document.getElementById('yz-btn-update-excel'); btn.textContent = "Processing...";
try {
const xlsData = await utils.readExcelFile(fd.excelFile); if (!xlsData.length) throw new Error("Excel为空");
if (fd.files.length > 100) throw new Error("附件超过100个");
let successCount = 0, failCount = 0;
let batchFlowKey = null;
let batchExecutors = null;
for (let i = 0; i < xlsData.length; i++) {
const row = xlsData[i]; const code = String(row['合同编号'] || '').trim();
if (!code || code === '-' || code === 'undefined') continue;
btn.textContent = `Processing (${i + 1}/${xlsData.length})...`;
try {
const sRes = await utils.requestPromise('POST', CONFIG.endpoints.searchContract, { "quickSearchValue": code, "quickSearchProperties": ["code"], "pageInfo": { "page": 1, "rows": 100 } });
const c = sRes.data?.rows?.find(r => r.contractCode === code || r.code === code); if (!c) throw new Error(`未找到 ${code}`);
const dRes = await utils.requestPromise('GET', CONFIG.endpoints.getContractDetail, { id: c.id });
let templateId = dRes.data.contractTemplateId;
if (!templateId) {
const uiTpl = document.getElementById('yz-template-select').value;
if (uiTpl) {
templateId = uiTpl;
console.warn("合同未关联模板,使用UI选择的模板ID:", templateId);
} else {
throw new Error("合同未关联模板ID,且UI未选择模板");
}
}
const fieldMap = await actions.fetchTemplateFields(templateId);
const upPayload = utils.transformExcelDataToStructure(dRes, fd, row, fieldMap);
await utils.requestPromise('POST', CONFIG.endpoints.updateContract, upPayload, "更新信息");
const payPl = fd.payments.map(p => ({ "key": utils.generateUUID(), "contractId": c.id, "windUpMethodId": p.windUp.value, "windUpMethodCode": p.windUp.value, "windUpMethodName": p.windUp.name, "stageId": p.stage.value, "stageCode": p.stage.value, "stageName": p.stage.name, "paymentMethodId": p.method.value, "paymentMethodCode": p.method.value, "paymentMethodName": p.method.name, "paymentPremise": p.premise, "payPeriod": p.period ? parseInt(p.period) : null, "paymentPercentage": parseFloat(p.percent), "remark": p.remark }));
await utils.requestPromise('POST', CONFIG.endpoints.savePaymentPlan, payPl, "付款计划");
const tName = row['合同附件名称'];
let hasUploadedFiles = false;
if (tName && fd.files.length) {
const mFiles = [];
for (let j = 0; j < fd.files.length; j++) if (fd.files[j].name.includes(tName)) mFiles.push(fd.files[j]);
if (mFiles.length > 0) {
for (let j = 0; j < mFiles.length; j++) {
const f1 = utils.buildFileFormData(mFiles[j]);
const u1 = await utils.requestPromise('POST', `${CONFIG.endpoints.uploadFile}?companyCode=100&waterMark=true&toPdf=false`, f1);
if (u1.data?.docId) {
await utils.requestPromise('POST', CONFIG.endpoints.saveArchive, { contractId: c.id, contractOtherDocIds: [u1.data.docId], approveReferenceDocIds: [] });
await utils.sleep(1500);
const f2 = utils.buildFileFormData(mFiles[j]);
let u2 = null;
let uploadSuccess = false;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
if (attempt > 1) {
btn.textContent = `Retry PDF (${attempt}/3)...`;
console.warn(`[V9.60] Retrying PDF upload for ${tName} (Attempt ${attempt})`);
await utils.sleep(3000);
}
u2 = await utils.requestPromise('POST', `${CONFIG.endpoints.uploadFile}?companyCode=100&waterMark=true&toPdf=true`, f2);
uploadSuccess = true;
break;
} catch (e) {
console.error(`PDF Upload Failed Attempt ${attempt}`, e);
if (attempt === 3) throw e;
}
}
if (uploadSuccess && u2.data?.docId) {
await utils.requestPromise('POST', `${CONFIG.endpoints.saveArchiveBatch}?fileIds=${u2.data.docId}&relationType=1&entityId=${c.id}`, {});
hasUploadedFiles = true;
}
}
}
} else {
console.warn(`[V9.60] 未找到匹配附件: ${tName}`);
}
}
if (fd.flow.autoSubmit) {
if (hasUploadedFiles) {
let selectedFlowKey = batchFlowKey;
if (!selectedFlowKey) {
const flowList = await actions.getAvailableFlows(c.id, row['合同名称'] || dRes.data.name);
selectedFlowKey = await actions.promptFlowSelection(row['合同名称'] || dRes.data.name, flowList);
if (selectedFlowKey) {
batchFlowKey = selectedFlowKey;
}
}
if (selectedFlowKey) {
const req = await utils.requestPromise('POST', CONFIG.endpoints.createRequest, { contractId: c.id, name: row['合同名称'] || dRes.data.name });
if (req.success && req.data?.requestId) {
const dynModelCode = req.data.businessModelCode;
if (batchExecutors) {
console.log("[V9.41] 复用已有审批人配置,跳过计算...", batchExecutors);
await utils.requestPromise('POST', CONFIG.endpoints.saveSolidify, { businessModelCode: dynModelCode, businessId: req.data.requestId, executorsVos: JSON.stringify(batchExecutors) });
const startNodes = batchExecutors.map(s => ({
nodeId: s.nodeId,
userVarName: s.userVarName,
flowTaskType: s.flowTaskType,
userIds: s.userIds,
solidifyFlow: true
}));
await utils.requestPromise('POST', CONFIG.endpoints.startFlowNew, { businessKey: req.data.requestId, businessModelCode: dynModelCode, typeId: null, flowDefKey: selectedFlowKey, taskList: JSON.stringify(startNodes) });
}
else {
const pre = await utils.requestPromise('POST', CONFIG.endpoints.startFlowNew, { businessKey: req.data.requestId, businessModelCode: dynModelCode, typeId: null, flowDefKey: selectedFlowKey });
if (pre.success && pre.data?.flowDefinationId) {
const ent = await utils.requestPromise('POST', `${CONFIG.endpoints.getEntity}?id=${pre.data.flowDefinationId}&versionCode=-1&businessId=${req.data.requestId}&businessModelCode=${dynModelCode}`, {});
let nodesMap = {};
try {
const def = JSON.parse(ent.data.defJson);
if (def && def.process && def.process.nodes) {
nodesMap = def.process.nodes;
}
} catch (e) { console.warn("Parse defJson failed", e); }
const solidEx = ent.data?.solidifyExecutorOfOnly || {};
const cands = [];
const flowNodeInfos = pre.data.nodeInfoList || [];
for (let nid in nodesMap) {
const node = nodesMap[nid];
if (node.type === 'UserTask' && !(node.nodeType === 'Normal' && node.name.includes("修改"))) {
let u = [];
let isCalculated = false;
let calcError = false;
if (node.nodeConfig && node.nodeConfig.executor && Array.isArray(node.nodeConfig.executor)) {
try {
const rawExecutors = node.nodeConfig.executor.map(ex => {
const newEx = { ...ex };
delete newEx.rowdata;
if (ex.userType === "SelfDefinition") {
newEx.ids = ex.selfDefId || ex.selfDefOfOrgAndSelId || null;
newEx.customParam = ex.customParam || null;
newEx.allowDefaultStarter = node.nodeConfig.normal?.allowDefaultStarter || null;
newEx.repeatExecutorPass = node.nodeConfig.normal?.repeatExecutorPass || null;
} else {
if (newEx.ids === "") newEx.ids = null;
if (newEx.customParam === "") newEx.customParam = null;
}
return newEx;
});
if (rawExecutors.length > 0) {
const calcPayload = {
requestExecutorsVos: JSON.stringify(rawExecutors),
businessModelCode: dynModelCode,
businessId: req.data.requestId
};
console.log(`%c[YzHelper Debug] 计算节点: ${node.name} (${nid})`, "color: #722ed1; font-weight: bold;");
utils.showStatus(`正在计算节点 [${node.name}]...`, 'processing');
const calcRes = await utils.requestPromise('POST', CONFIG.endpoints.getCalculatedUsers, calcPayload, `计算${node.name}`);
if (calcRes.success && calcRes.data && Array.isArray(calcRes.data)) {
calcRes.data.forEach(user => {
u.push({
id: user.userId || user.id,
name: user.userName || user.name,
positionName: user.positionName || '',
organizationName: user.organizationName || ''
});
});
if (u.length > 0) isCalculated = true;
} else {
calcError = true;
}
}
} catch (err) {
console.warn(`节点 ${node.name} 计算失败`, err);
calcError = true;
}
}
if (u.length === 0 && solidEx[nid] && solidEx[nid].executorIds) {
const ids = solidEx[nid].executorIds.split(',');
const nms = solidEx[nid].executorNames.split(',');
u = ids.map((id, x) => ({
id: id,
name: nms[x] || '',
positionName: '',
organizationName: ''
}));
}
let matchedVarName = null;
if (flowNodeInfos) {
const infoMatch = flowNodeInfos.find(n => n.id === nid);
if (infoMatch) matchedVarName = infoMatch.userVarName;
}
if (u.length > 0 || calcError || (node.nodeConfig && node.nodeConfig.executor && node.nodeConfig.executor.length > 0)) {
cands.push({
nodeId: nid,
nodeName: node.name,
users: u,
flowTaskType: node.nodeType === 'CounterSign' ? 'countersign' : 'approve',
userVarName: matchedVarName,
isError: calcError
});
}
}
}
if (cands.length === 0) {
const fallbackNodes = Object.keys(solidEx);
fallbackNodes.forEach(nid => {
let u = [];
if (solidEx[nid] && solidEx[nid].executorIds) {
const ids = solidEx[nid].executorIds.split(','); const nms = solidEx[nid].executorNames.split(',');
u = ids.map((id, x) => ({ id: id, name: nms[x] || '', positionName: '', organizationName: '' }));
}
if (u.length) cands.push({ nodeId: nid, nodeName: solidEx[nid]?.actTaskDefKey || nid, users: u, flowTaskType: solidEx[nid]?.nodeType === 'CounterSign' ? 'countersign' : 'approve' });
});
}
if (pre.data.nodeInfoList) {
cands.forEach(c => {
const match = pre.data.nodeInfoList.find(n => n.id === c.nodeId);
if (match) c.userVarName = match.userVarName;
});
}
const sel = await actions.promptUserForApprovers(code, cands);
if (sel && sel.length) {
batchExecutors = sel;
await utils.requestPromise('POST', CONFIG.endpoints.saveSolidify, { businessModelCode: dynModelCode, businessId: req.data.requestId, executorsVos: JSON.stringify(sel) });
const startNodes = sel.map(s => ({
nodeId: s.nodeId,
userVarName: s.userVarName,
flowTaskType: s.flowTaskType,
userIds: s.userIds,
solidifyFlow: true
}));
await utils.requestPromise('POST', CONFIG.endpoints.startFlowNew, { businessKey: req.data.requestId, businessModelCode: dynModelCode, typeId: null, flowDefKey: selectedFlowKey, taskList: JSON.stringify(startNodes) });
}
}
}
}
} else {
utils.showStatus("用户取消或跳过审批流程", "error");
}
} else {
utils.showStatus("无附件或上传失败,跳过审批", "error");
throw new Error("附件上传失败,无法发起审批");
}
}
successCount++;
} catch (e) { console.error(e); failCount++; }
}
alert(`完成! 成功: ${successCount}, 失败: ${failCount}`);
} catch (e) { alert("Error: " + e.message); } finally {
utils.toggleInputs(false);
const tplSelect = document.getElementById('yz-template-select');
if (tplSelect) tplSelect.disabled = true;
btn.textContent = "根据 Excel 批量更新合同信息 (完成)";
}
},
toggleMinimize: () => {
const panel = document.getElementById('yz-helper-panel');
if (panel) panel.style.display = 'none';
},
loadTemplates: () => { utils.request('POST', CONFIG.endpoints.searchTemplate, { "enable": true, "page": true, "contractType": "FRAME_AGREEMENT", "companyCode": "100", "pageInfo": { "page": 1, "rows": 1000 } }, (err, data) => { if (!err && data.success) { state.templates = data.data.rows; actions.renderTemplateOptions(); } }); },
renderTemplateOptions: () => { const s = document.getElementById('yz-template-select'); if (!s) return; s.innerHTML = ''; state.templates.forEach(t => { const o = document.createElement('option'); o.value = t.id; o.text = t.templateName; s.appendChild(o); }); },
renderTable: () => { const b = document.getElementById('yz-table-body'); b.innerHTML = ''; if (!state.searchResults.length) return b.innerHTML = '| 无数据 |
'; state.searchResults.forEach(r => { const tr = document.createElement('tr'); tr.id = r._uiId; tr.innerHTML = `${r.tenderProjectName} | ${r.supplierName} | ${r.contractTemplateName || '-'} | ${r._status === 'success' ? '✅' : '⏳'} | `; b.appendChild(tr); }); },
updateRowStatus: (uid, st, msg, det) => { const el = document.getElementById(uid)?.querySelector('.res-cell'); if (el) el.innerHTML = st === 'success' ? `✅ ${det.code}` : `${st === 'error' ? '❌' : '⏳'} ${msg || ''}`; }
};
function initUI() {
GM_addStyle(STYLES);
const div = document.createElement('div'); div.id = 'yz-helper-panel';
div.innerHTML = `
`;
document.body.appendChild(div);
document.getElementById('yz-btn-search').onclick = actions.searchAndExport;
document.getElementById('yz-btn-update-excel').onclick = actions.batchUpdateFromExcel;
document.getElementById('yz-btn-manual-submit').onclick = actions.manualSubmitApproval;
document.getElementById('yz-close-icon').onclick = actions.toggleMinimize;
document.getElementById('yz-header').ondblclick = actions.toggleMinimize;
document.getElementById('yz-template-select').onchange = actions.renderDynamicForm;
document.getElementById('yz-btn-add-payment').onclick = actions.addPaymentRow;
document.getElementById('yz-btn-save-preset').onclick = actions.savePaymentPreset;
document.getElementById('yz-btn-load-preset').onclick = () => {
const sel = document.getElementById('yz-preset-select');
if (!sel.value) return alert('请选择一个预设');
actions.loadPaymentPreset(sel.value);
};
document.getElementById('yz-btn-del-preset').onclick = () => {
const sel = document.getElementById('yz-preset-select');
if (!sel.value) return alert('请选择一个预设');
actions.deletePaymentPreset(sel.value);
};
actions.refreshPresetSelect();
document.getElementById('yz-in-area').onclick = () => actions.openTreeSelector('区域', state.areaTreeData, n => { document.getElementById('yz-in-area').value = n.name; document.getElementById('yz-in-area').dataset.code = n.code || n.id; });
document.getElementById('yz-in-org').onclick = () => actions.openTreeSelector('组织', state.orgTreeData, n => { document.getElementById('yz-in-org').value = n.name; document.getElementById('yz-in-org').dataset.code = n.code || n.id; });
document.getElementById('yz-btn-add-scope').onclick = actions.addScope;
document.getElementById('yz-helper-panel').addEventListener('delScope', e => actions.removeScope(e.detail));
const btnManualUpload = document.getElementById('yz-btn-manual-upload');
if (btnManualUpload) btnManualUpload.onclick = actions.manualUploadAttachments;
const btnGenAttachMap = document.getElementById('yz-btn-generate-attach-map');
if (btnGenAttachMap) btnGenAttachMap.onclick = actions.generateAttachMappingTable;
const btnExecAttachMap = document.getElementById('yz-btn-execute-attach-map');
if (btnExecAttachMap) btnExecAttachMap.onclick = actions.executeAttachMappedUpload;
const btnGenMap = document.getElementById('yz-btn-generate-map');
if (btnGenMap) btnGenMap.onclick = actions.generateMappingTable;
const btnExecMap = document.getElementById('yz-btn-execute-map');
if (btnExecMap) btnExecMap.onclick = actions.executeMappedUpload;
actions.addPaymentRow();
actions.loadTemplates();
}
window.addEventListener('load', () => { actions.init(); setTimeout(initUI, 1000); });
})();