NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name 调拨记录查询
// @namespace https://openuserjs.org/users/yiwaima
// @version 1.1.2
// @description 提供调拨记录查询功能,支持通过关键词搜索调拨记录
// @author yiwaima
// @match https://*.erp321.com/app/wms/allocate/allocateTab.aspx*
// @icon https://files.erp321.com/img/platIcon/ziShen.png
// @grant none
// @license MIT
// @homepageURL https://openuserjs.org/scripts/yiwaima/调拨记录查询
// @supportURL https://github.com/yiwaima/erp-scripts/issues
// @updateURL https://openuserjs.org/meta/yiwaima/调拨记录查询.meta.js
// @downloadURL https://openuserjs.org/install/yiwaima/调拨记录查询.user.js
// ==/UserScript==
(function () {
'use strict';
/***********************
* 工具函数
***********************/
const qs = (s, root = document) => root.querySelector(s);
const qsa = (s, root = document) => Array.from(root.querySelectorAll(s));
const createEl = (tag, attrs = {}, css = '') => {
const el = document.createElement(tag);
Object.assign(el, attrs);
if (css) el.style.cssText = css;
return el;
};
const storage = {
get(key, fallback = null) {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : fallback;
} catch (e) {
return fallback;
}
},
set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) { /* 忽略 */ }
},
remove(key) {
try {
localStorage.removeItem(key);
} catch (e) { /* 忽略 */ }
}
};
const notify = (msg, type = 'success') => {
const el = createEl('div', {
textContent: msg
}, `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%) scale(0.9);
padding: 14px 20px;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: ${type === 'success' ? '#4CAF50' : type === 'warning' ? '#FF9800' : '#f44336'};
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
z-index: 10001;
opacity: 0;
transition: all .25s ease;
text-align: center;
max-width: 80%;
`);
document.body.appendChild(el);
requestAnimationFrame(() => {
el.style.opacity = '1';
el.style.transform = 'translateX(-50%) scale(1)';
});
setTimeout(() => {
el.style.opacity = '0';
el.style.transform = 'translateX(-50%) scale(0.9)';
setTimeout(() => el.remove(), 250);
}, 2200);
};
const clipboardCopy = async (text) => {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
const temp = createEl('textarea', { value: text }, 'position:fixed;left:-9999px;top:-9999px;');
document.body.appendChild(temp);
temp.select();
document.execCommand('copy');
temp.remove();
}
};
const fetchJsonSafe = async (url, options = {}) => {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
let text = await res.text();
while (text && !text.trim().startsWith('{')) text = text.slice(1);
return JSON.parse(text);
};
const parseViewState = (html) => {
const vs = html.match(/id="__VIEWSTATE"\s+value="([^"]+)"/i);
const vg = html.match(/id="__VIEWSTATEGENERATOR"\s+value="([^"]+)"/i);
return { viewState: vs ? vs[1] : '', viewStateGenerator: vg ? vg[1] : '' };
};
const getCookie = (name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
};
/***********************
* 业务请求
***********************/
const Api = {
// 获取VIEWSTATE和VIEWSTATEGENERATOR
async getViewStateData(baseUrl, path) {
try {
const url = `${baseUrl}/app/wms/allocate/${path}`;
const res = await fetch(url, { credentials: 'include' });
if (!res.ok) throw new Error(`页面获取失败 ${res.status}`);
const html = await res.text();
return parseViewState(html);
} catch (error) {
console.error(`获取${path}的VIEWSTATE失败:`, error);
throw error;
}
},
// 搜索寄样调拨记录
async searchTransferRecords(keyword) {
try {
const uCoId = getCookie('u_co_id');
if (!uCoId) throw new Error('cookie缺少u_co_id');
// 获取当前页面的主机名,用于构建请求URL
const host = window.location.host;
const baseUrl = `https://${host}`;
// 获取当前日期和31天前的日期
const now = new Date();
const endDate = now.toISOString().split('T')[0];
const startDate = new Date(now.getTime() - 31 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
const endDateTime = `${endDate} 23:59:59.998`;
// 获取第一个请求页面的VIEWSTATE和VIEWSTATEGENERATOR
const { viewState: firstRequestViewState, viewStateGenerator: firstRequestViewStateGenerator } = await this.getViewStateData(baseUrl, 'allocate.aspx');
// 获取第二个请求页面的VIEWSTATE和VIEWSTATEGENERATOR
const { viewState: secondRequestViewState, viewStateGenerator: secondRequestViewStateGenerator } = await this.getViewStateData(baseUrl, 'allocateIn.aspx');
// 构建第一个请求的URL
const timestamp = Date.now();
const authorizeCoId = '15049315'; // 固定的authorize_co_id
const firstRequestUrl = `${baseUrl}/app/wms/allocate/allocate.aspx?inoutType=5&owner_co_id=${uCoId}&authorize_co_id=${authorizeCoId}&ts___=${timestamp}&am___=LoadDataToJSON`;
// 输出第一个请求的构建信息
console.log('第一个请求URL:', firstRequestUrl);
console.log('第一个请求参数:', {
viewState: firstRequestViewState.substring(0, 50) + '...', // 只显示前50个字符
viewStateGenerator: firstRequestViewStateGenerator,
uCoId,
authorizeCoId,
keyword,
startDate,
endDateTime
});
// 发送第一个请求
const firstRequestData = await this.sendFirstTransferRequest(
baseUrl,
firstRequestViewState,
firstRequestViewStateGenerator,
uCoId,
keyword,
startDate,
endDateTime
);
// 输出第一个请求的响应数据
console.log('第一个请求的响应数据:', firstRequestData);
// 发送第二个请求
const secondRequestData = await this.sendSecondTransferRequest(
baseUrl,
secondRequestViewState,
secondRequestViewStateGenerator,
uCoId,
keyword,
startDate,
endDateTime
);
// 合并两个请求的数据
const allData = [...firstRequestData, ...secondRequestData];
// 去重,避免重复的io_id
const uniqueData = this.removeDuplicates(allData, 'io_id');
// 提取需要的字段并返回
return uniqueData;
} catch (error) {
console.error('搜索调拨记录失败:', error);
throw error;
}
},
// 搜索退样调拨记录
async searchReturnTransferRecords(keyword, startDate, endDateTime) {
try {
const uCoId = getCookie('u_co_id');
if (!uCoId) throw new Error('cookie缺少u_co_id');
// 获取当前页面的主机名,用于构建请求URL
const host = window.location.host;
const baseUrl = `https://${host}`;
// 获取退样第一个请求页面的VIEWSTATE和VIEWSTATEGENERATOR
const { viewState: returnSampleFirstViewState, viewStateGenerator: returnSampleFirstViewStateGenerator } = await this.getViewStateData(baseUrl, 'allocate.aspx');
// 获取退样第二个请求页面的VIEWSTATE和VIEWSTATEGENERATOR
const { viewState: returnSampleSecondViewState, viewStateGenerator: returnSampleSecondViewStateGenerator } = await this.getViewStateData(baseUrl, 'allocateIn.aspx');
// 发送退样第一个请求
const returnSampleFirstData = await this.sendFirstReturnSampleRequest(
baseUrl,
returnSampleFirstViewState,
returnSampleFirstViewStateGenerator,
uCoId,
keyword,
startDate,
endDateTime
);
// 发送退样第二个请求
const returnSampleSecondData = await this.sendSecondReturnSampleRequest(
baseUrl,
returnSampleSecondViewState,
returnSampleSecondViewStateGenerator,
uCoId,
keyword,
startDate,
endDateTime
);
// 合并两个退样请求的数据
const allData = [...returnSampleFirstData, ...returnSampleSecondData];
// 去重,避免重复的io_id
const uniqueData = this.removeDuplicates(allData, 'io_id');
// 提取需要的字段并返回
return uniqueData;
} catch (error) {
console.error('搜索退样调拨记录失败:', error);
throw error;
}
},
// 处理仓库文本的函数
processWarehouseText(text, isReturnSample = false) {
if (!text) return '';
// 对于退样记录,获取link_warehouse并处理文本
if (isReturnSample) {
// 获取"-"之后的文本
const parts = text.split('-');
let processedText = parts.length > 1 ? parts.slice(1).join('-') : text;
// 排除"杭州达人"和"项目"字样
processedText = processedText.replace(/杭州达人|项目/g, '').trim();
return processedText;
}
// 对于寄样记录,直接返回warehouse
return text;
},
// 通用调拨记录请求方法
async sendTransferRequest(baseUrl, viewState, viewStateGenerator, uCoId, keyword, startDate, endDateTime, config) {
try {
// 构建请求URL
const timestamp = Date.now();
const url = `${baseUrl}/app/wms/allocate/${config.urlPath}?${config.urlParams}&t=${timestamp}&owner_co_id=${uCoId}&authorize_co_id=15049315&ts___=${timestamp}&am___=LoadDataToJSON`;
// 构建表单数据
const form = new FormData();
form.append('__VIEWSTATE', viewState);
form.append('__VIEWSTATEGENERATOR', viewStateGenerator);
form.append('io_date', startDate);
form.append('io_date', endDateTime);
form.append('_jt_page_size', '25');
form.append('sku_id', keyword);
form.append('__CALLBACKID', 'JTable1');
// 添加仓库相关参数
if (config.warehouseParams) {
Object.entries(config.warehouseParams).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(item => form.append(key, item));
} else {
form.append(key, value);
}
});
}
// 添加状态相关参数
if (config.statusParams) {
Object.entries(config.statusParams).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(item => form.append(key, item));
} else {
form.append(key, value);
}
});
}
// 构建CALLBACKPARAM
const callbackParam = {
Method: 'LoadDataToJSON',
Args: [
'1',
JSON.stringify(config.callbackParams.map(param => ({
...param,
v: param.k === 'sku_id' ? keyword : param.v,
v: param.k === 'io_date' && param.c === '>=' ? startDate : param.v,
v: param.k === 'io_date' && param.c === '<=' ? endDateTime : param.v
}))),
'{}'
]
};
form.append('__CALLBACKPARAM', JSON.stringify(callbackParam));
// 发送请求
console.log(`发送${config.name}请求:`, url);
console.log(`${config.name}请求表单数据:`, {
hasViewState: form.has('__VIEWSTATE'),
hasViewStateGenerator: form.has('__VIEWSTATEGENERATOR'),
hasSkuId: form.has('sku_id'),
hasCallbackId: form.has('__CALLBACKID'),
hasCallbackParam: form.has('__CALLBACKPARAM'),
warehouseParams: config.warehouseParams
});
const res = await fetchJsonSafe(url, {
method: 'POST',
credentials: 'include',
body: form
});
// 输出响应信息
console.log(`${config.name}请求API响应:`, res);
// 解析响应数据
const rv = res.ReturnValue ? JSON.parse(res.ReturnValue) : null;
console.log(`${config.name}请求解析后的ReturnValue:`, rv);
const rows = rv?.datas || [];
console.log(`${config.name}请求响应数据行数:`, rows.length);
console.log(`${config.name}请求响应数据:`, rows);
// 提取需要的字段并返回,排除labels为"调拨中取消"和"调拨红冲"的记录
return rows.filter(item => item.labels !== '调拨中取消' && item.labels !== '调拨红冲').map(item => {
// 判断是否为退样记录(根据配置名称判断)
const isReturnSample = config.name.includes('退样');
// 判断是否为退样的第二个请求
const isSecondReturnSample = config.name.includes('退样') && config.name.includes('第二个');
// 对于退样的第二个请求,获取link_warehouse;其他情况获取warehouse
const warehouse = isSecondReturnSample ? item.link_warehouse || '' : item.warehouse || '';
// 处理仓库文本
const processedWarehouse = this.processWarehouseText(warehouse, isSecondReturnSample);
return {
io_id: item.io_id || '',
remark: item.remark || '',
created: item.created || '',
warehouse: processedWarehouse
};
});
} catch (error) {
console.error(`发送${config.name}调拨记录请求失败:`, error);
return [];
}
},
// 发送第一个调拨记录请求(寄样)
async sendFirstTransferRequest(baseUrl, viewState, viewStateGenerator, uCoId, keyword, startDate, endDateTime) {
const config = {
name: '第一个',
urlPath: 'allocate.aspx',
urlParams: 'inoutType=5',
warehouseParams: {
'link_wh_id_id': '15049315#6',
'link_wh_id': '本仓-达人一仓',
'_cbb_link_wh_id': '15049315#6'
},
statusParams: {
'status_v': 'Confirmed|Confirmed,Confirmed|WaitConfirm,Picking,Creating,OuterConfirming',
'status': '完成,调拨中,拣货中,待确认,外部确认中',
'_cbb_status': ['Confirmed|Confirmed', 'Confirmed|WaitConfirm', 'Picking', 'Creating', 'OuterConfirming']
},
callbackParams: [
{ "k": "link_wh_id", "v": "15049315#6", "c": "@=" },
{ "k": "io_date", "v": startDate, "c": ">=", "t": "date" },
{ "k": "io_date", "v": endDateTime, "c": "<=", "t": "date" },
{ "k": "status", "v": "Confirmed|Confirmed,Confirmed|WaitConfirm,Picking,Creating,OuterConfirming", "c": "=" },
{ "k": "sku_id", "v": keyword, "c": "=" }
]
};
return this.sendTransferRequest(baseUrl, viewState, viewStateGenerator, uCoId, keyword, startDate, endDateTime, config);
},
// 发送第二个调拨记录请求(寄样)
async sendSecondTransferRequest(baseUrl, viewState, viewStateGenerator, uCoId, keyword, startDate, endDateTime) {
const config = {
name: '第二个',
urlPath: 'allocateIn.aspx',
urlParams: '',
warehouseParams: {
'wh_id_id': '15049315#6,15049315#7',
'wh_id': '本仓-达人一仓,本仓-达人二仓',
'_cbb_wh_id': ['15049315#6', '15049315#7']
},
statusParams: {
'status_v': 'OuterConfirming,Confirmed,WaitConfirm',
'status': '外部确认中,完成,待收货',
'_cbb_status': ['OuterConfirming', 'Confirmed', 'WaitConfirm']
},
callbackParams: [
{ "k": "wh_id", "v": "15049315#6,15049315#7", "c": "@=" },
{ "k": "io_date", "v": startDate, "c": ">=", "t": "date" },
{ "k": "io_date", "v": endDateTime, "c": "<=", "t": "date" },
{ "k": "status", "v": "OuterConfirming,Confirmed,WaitConfirm", "c": "=" },
{ "k": "sku_id", "v": keyword, "c": "=" }
]
};
return this.sendTransferRequest(baseUrl, viewState, viewStateGenerator, uCoId, keyword, startDate, endDateTime, config);
},
// 发送退样详细的第一个请求
async sendFirstReturnSampleRequest(baseUrl, viewState, viewStateGenerator, uCoId, keyword, startDate, endDateTime) {
const config = {
name: '退样第一个',
urlPath: 'allocate.aspx',
urlParams: 'inoutType=5',
warehouseParams: {
'link_wh_id_id': '15049315#11',
'link_wh_id': '本仓-六六仓',
'_cbb_link_wh_id': '15049315#11'
},
statusParams: {
'status_v': 'Confirmed|Confirmed,Confirmed|WaitConfirm,Picking,Creating,OuterConfirming',
'status': '完成,调拨中,拣货中,待确认,外部确认中',
'_cbb_status': ['Confirmed|Confirmed', 'Confirmed|WaitConfirm', 'Picking', 'Creating', 'OuterConfirming']
},
callbackParams: [
{ "k": "link_wh_id", "v": "15049315#11", "c": "@=" },
{ "k": "io_date", "v": startDate, "c": ">=", "t": "date" },
{ "k": "io_date", "v": endDateTime, "c": "<=", "t": "date" },
{ "k": "status", "v": "Confirmed|Confirmed,Confirmed|WaitConfirm,Picking,Creating,OuterConfirming", "c": "=" },
{ "k": "sku_id", "v": keyword, "c": "=" }
]
};
return this.sendTransferRequest(baseUrl, viewState, viewStateGenerator, uCoId, keyword, startDate, endDateTime, config);
},
// 发送退样详细的第二个请求
async sendSecondReturnSampleRequest(baseUrl, viewState, viewStateGenerator, uCoId, keyword, startDate, endDateTime) {
const config = {
name: '退样第二个',
urlPath: 'allocateIn.aspx',
urlParams: '',
warehouseParams: {
'wh_id_id': '15049315#11',
'wh_id': '本仓-六六仓',
'_cbb_wh_id': '15049315#11'
},
statusParams: {
'status_v': 'OuterConfirming,Confirmed,WaitConfirm',
'status': '外部确认中,完成,待收货',
'_cbb_status': ['OuterConfirming', 'Confirmed', 'WaitConfirm']
},
callbackParams: [
{ "k": "wh_id", "v": "15049315#11", "c": "@=" },
{ "k": "io_date", "v": startDate, "c": ">=", "t": "date" },
{ "k": "io_date", "v": endDateTime, "c": "<=", "t": "date" },
{ "k": "status", "v": "OuterConfirming,Confirmed,WaitConfirm", "c": "=" },
{ "k": "sku_id", "v": keyword, "c": "=" }
]
};
return this.sendTransferRequest(baseUrl, viewState, viewStateGenerator, uCoId, keyword, startDate, endDateTime, config);
},
// 去重函数
removeDuplicates(arr, key) {
const seen = new Set();
return arr.filter(item => {
const value = item[key];
if (seen.has(value)) {
return false;
}
seen.add(value);
return true;
});
},
// 获取调拨数量
async getTransferQuantity(io_id, keyword) {
try {
const uCoId = getCookie('u_co_id');
if (!uCoId) throw new Error('cookie缺少u_co_id');
// 获取当前页面的主机名,用于构建请求URL
const host = window.location.host;
const baseUrl = `https://${host}`;
// 构建请求URL
const timestamp = Date.now();
const url = `${baseUrl}/app/wms/allocate/allocate_item.aspx?io_id=${io_id}&type=%E8%B0%83%E6%8B%A8%E5%85%A5&link_co_id=${uCoId}&IsArchive=false&owner_co_id=${uCoId}&authorize_co_id=15049315&ts___=${timestamp}&am___=LoadDataToJSON`;
// 获取页面的VIEWSTATE和VIEWSTATEGENERATOR
const page = await fetch(url, { credentials: 'include' });
if (!page.ok) throw new Error(`页面获取失败 ${page.status}`);
const html = await page.text();
const { viewState, viewStateGenerator } = parseViewState(html);
// 构建表单数据
const form = new FormData();
form.append('__VIEWSTATE', viewState);
form.append('__VIEWSTATEGENERATOR', viewStateGenerator);
form.append('printsource', '0');
form.append('sku_search', keyword);
form.append('_jt_page_size', '25');
form.append('__CALLBACKID', 'JTable1');
// 构建CALLBACKPARAM
const callbackParam = {
Method: 'LoadDataToJSON',
Args: [
'1',
JSON.stringify([{ "k": "sku_search", "v": keyword, "c": "like" }]),
'{}'
]
};
form.append('__CALLBACKPARAM', JSON.stringify(callbackParam));
// 发送请求
const res = await fetchJsonSafe(url, {
method: 'POST',
credentials: 'include',
body: form
});
// 解析响应数据
const rv = res.ReturnValue ? JSON.parse(res.ReturnValue) : null;
const rows = rv?.datas || [];
// 提取第一个数据的qty
if (rows.length > 0) {
return rows[0].qty || 0;
}
return 0;
} catch (error) {
console.error('获取调拨数量失败:', error);
return 0;
}
}
};
/***********************
* 状态管理
***********************/
const EDGE_THRESHOLD = 10;
const COLLAPSE_WIDTH = 32;
const EXPAND_WIDTH = 550; // 调整窗口宽度为550px
const AUTO_COLLAPSE_DELAY = 150;
const HOVER_EXPAND_DELAY = 80;
const state = {
ui: null,
currentUserName: '',
positions: {
open: { left: '10px', top: '10px' },
collapsed: { left: '0px', top: '10px' }
},
collapsed: false,
timers: {
collapseTimer: null,
expandTimer: null
}
};
/***********************
* UI 组件
***********************/
function buildUI() {
const container = createEl('div', {}, `
position: fixed;
top: 10px;
left: 10px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
background: #fff;
padding: 10px;
border: 2px solid #4A90E2;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
font-family: Arial, sans-serif;
width: ${EXPAND_WIDTH}px;
min-height: 350px;
max-height: 500px;
overflow-y: auto;
transition: width .3s ease, left .3s ease;
user-select: text;
`);
const header = createEl('div', { textContent: '调拨记录查询' }, `
background-color: #4A90E2;
padding: 8px 15px;
border-radius: 6px 6px 0 0;
cursor: move;
font-weight: bold;
color: white;
text-align: center;
margin: -10px -10px 10px -10px;
user-select: none;
`);
// 第一行:输入框和搜索按钮(平行排列)
const searchRow = createEl('div', {}, `
display: flex;
gap: 8px;
width: 100%;
box-sizing: border-box;
`);
const searchInput = createEl('input', {
type: 'text',
placeholder: '输入关键词搜索调拨记录...'
}, `
flex: 1;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
`);
const searchBtn = createEl('button', { textContent: '搜索' }, `
padding: 8px 15px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
box-sizing: border-box;
white-space: nowrap;
height: 36px;
width: 90px;
text-align: center;
`);
searchRow.append(searchInput, searchBtn);
// 第二行:结果显示区域,包含寄样和退样两个部分
const resultsContainer = createEl('div', {}, `
display: flex;
gap: 10px;
width: 100%;
box-sizing: border-box;
`);
// 寄样详细显示
const sendSampleDisplay = createEl('div', {
textContent: '寄样详细将显示在这里'
}, `
flex: 1;
padding: 10px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 13px;
background: #f5f5f5;
text-align: left;
min-height: 200px;
max-height: 300px;
overflow-y: auto;
box-sizing: border-box;
`);
// 退样详细显示
const returnSampleDisplay = createEl('div', {
textContent: '退样详细将显示在这里'
}, `
flex: 1;
padding: 10px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 13px;
background: #f5f5f5;
text-align: left;
min-height: 200px;
max-height: 300px;
overflow-y: auto;
box-sizing: border-box;
`);
resultsContainer.append(sendSampleDisplay, returnSampleDisplay);
container.append(header, searchRow, resultsContainer);
document.body.appendChild(container);
state.ui = {
container,
header,
searchRow,
searchInput,
searchBtn,
sendSampleDisplay,
returnSampleDisplay
};
return state.ui;
}
/***********************
* 拖动与折叠
***********************/
function enableDragAndSnap() {
const { container, header, searchRow, sendSampleDisplay, returnSampleDisplay } = state.ui;
let dragging = false;
let startX = 0, startY = 0;
let rafId = null;
const savedPos = storage.get('erpSearchPosition');
const savedCollapse = storage.get('erpSearchCollapseState');
if (savedPos) {
container.style.left = savedPos.left;
container.style.top = savedPos.top;
state.positions.open = savedPos;
}
if (savedCollapse?.isCollapsed) {
state.positions.collapsed = savedCollapse.position || state.positions.collapsed;
collapse(savedCollapse.side || 'left');
}
header.addEventListener('mousedown', (e) => {
dragging = true;
startX = e.clientX - container.offsetLeft;
startY = e.clientY - container.offsetTop;
container.style.transition = 'none';
document.body.style.pointerEvents = 'none';
header.style.pointerEvents = 'auto';
if (state.collapsed) expand();
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
const x = Math.max(0, Math.min(window.innerWidth - container.offsetWidth, e.clientX - startX));
const y = Math.max(0, Math.min(window.innerHeight - container.offsetHeight, e.clientY - startY));
container.style.left = `${x}px`;
container.style.top = `${y}px`;
});
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
if (rafId) cancelAnimationFrame(rafId);
container.style.transition = 'width .3s ease, left .3s ease';
document.body.style.pointerEvents = 'auto';
container.style.top = `${Math.max(0, parseInt(container.style.top || '0', 10))}px`;
snapToEdge();
storage.set('erpSearchPosition', { left: container.style.left, top: container.style.top });
});
container.addEventListener('mouseleave', () => {
if (dragging || state.collapsed) return;
state.timers.collapseTimer = setTimeout(() => snapToEdge(), AUTO_COLLAPSE_DELAY);
});
container.addEventListener('mouseenter', () => {
if (state.timers.collapseTimer) clearTimeout(state.timers.collapseTimer);
if (state.collapsed) {
if (state.timers.expandTimer) clearTimeout(state.timers.expandTimer);
state.timers.expandTimer = setTimeout(() => expand(), HOVER_EXPAND_DELAY);
}
});
function snapToEdge() {
const left = parseInt(container.style.left || '0', 10);
state.positions.open = { left: container.style.left, top: container.style.top };
if (left <= EDGE_THRESHOLD) {
collapse('left');
} else if (left >= window.innerWidth - container.offsetWidth - EDGE_THRESHOLD) {
collapse('right');
}
}
function collapse(edge) {
state.collapsed = true;
// 隐藏所有子元素,只保留header
searchRow.style.display = 'none';
sendSampleDisplay.style.display = 'none';
returnSampleDisplay.style.display = 'none';
// 改变容器样式,使其收缩为一个小按钮
container.style.display = 'block';
container.style.height = '40px';
container.style.width = `${COLLAPSE_WIDTH}px`;
container.style.padding = '0';
container.style.margin = '0';
container.style.gap = '0';
// 保持适当的圆角和边框
container.style.borderRadius = '4px';
container.style.border = '2px solid #4A90E2';
container.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
// 调整位置
state.positions.open = { left: container.style.left, top: container.style.top };
container.style.left = edge === 'left' ? '0px' : `${Math.max(0, window.innerWidth - COLLAPSE_WIDTH)}px`;
state.positions.collapsed = { left: container.style.left, top: container.style.top };
// 调整header样式
header.textContent = '🔍';
header.style.display = 'block';
header.style.width = '100%';
header.style.height = '100%';
header.style.padding = '0';
header.style.margin = '0';
header.style.borderRadius = '2px';
header.style.textAlign = 'center';
header.style.lineHeight = '36px'; // 减去边框的2px
header.style.fontSize = '16px';
header.style.backgroundColor = '#4A90E2';
header.style.color = 'white';
header.style.cursor = 'pointer';
container.dataset.collapsedSide = edge;
storage.set('erpSearchCollapseState', { isCollapsed: true, side: edge, position: state.positions.collapsed });
}
function expand() {
state.collapsed = false;
// 恢复所有子元素的显示
searchRow.style.display = 'flex';
sendSampleDisplay.style.display = 'block';
returnSampleDisplay.style.display = 'block';
// 恢复容器的原始样式
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.height = 'auto';
container.style.width = `${EXPAND_WIDTH}px`;
container.style.padding = '10px';
container.style.margin = '0';
container.style.gap = '10px';
container.style.border = '2px solid #4A90E2';
container.style.borderRadius = '8px';
container.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
// 若无记录,使用当前偏移作为展开位置的基准
const targetLeft = state.positions.open.left || state.positions.collapsed.left || '10px';
const targetTop = state.positions.open.top || state.positions.collapsed.top || '10px';
container.style.left = targetLeft;
container.style.top = targetTop;
// 恢复header的原始样式
header.textContent = '调拨记录查询';
header.style.display = 'block';
header.style.width = 'auto';
header.style.height = 'auto';
header.style.padding = '8px 15px';
header.style.margin = '-10px -10px 10px -10px';
header.style.borderRadius = '6px 6px 0 0';
header.style.textAlign = 'center';
header.style.lineHeight = 'normal';
header.style.fontSize = '14px';
header.style.backgroundColor = '#4A90E2';
header.style.color = 'white';
header.style.cursor = 'move';
storage.set('erpSearchPosition', { left: container.style.left, top: container.style.top });
storage.remove('erpSearchCollapseState');
}
state.ui.expand = expand;
state.ui.snapToEdge = snapToEdge;
// 窗口尺寸变化时调整吸附位置
window.addEventListener('resize', () => {
if (state.collapsed) {
const side = container.dataset.collapsedSide || 'left';
if (side === 'right') {
container.style.left = `${Math.max(0, window.innerWidth - COLLAPSE_WIDTH)}px`;
} else {
container.style.left = '0px';
}
state.positions.collapsed = { left: container.style.left, top: container.style.top };
} else {
const left = Math.min(Math.max(0, parseInt(container.style.left || '0', 10)), Math.max(0, window.innerWidth - EXPAND_WIDTH));
const top = Math.min(Math.max(0, parseInt(container.style.top || '0', 10)), Math.max(0, window.innerHeight - container.offsetHeight));
container.style.left = `${left}px`;
container.style.top = `${top}px`;
state.positions.open = { left: container.style.left, top: container.style.top };
}
});
}
/***********************
* 表单辅助
***********************/
const FormHelper = {
fetchUser() {
const cookieName = document.cookie.match(/u_name=([^;]+)/);
if (cookieName?.[1]) {
state.currentUserName = decodeURIComponent(cookieName[1]);
return;
}
fetch('https://api.erp321.com/erp/webapi/UserApi/Passport/GetUserInfo', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=UTF-8',
'Referer': 'https://ww.erp321.com/',
'Origin': 'https://ww.erp321.com'
},
credentials: 'include',
body: JSON.stringify({})
}).then(r => r.json()).then(d => {
state.currentUserName = d?.data?.userName || '';
}).catch(() => { /* ignore */ });
}
};
/***********************
* 事件绑定
***********************/
function bindEvents() {
const { searchInput, searchBtn, sendSampleDisplay, returnSampleDisplay } = state.ui;
// 获取寄样数组中最早的日期
function getEarliestDate(records) {
if (records.length === 0) {
// 如果没有记录,返回30天前的日期
const date = new Date();
date.setDate(date.getDate() - 30);
return date.toISOString().split('T')[0];
}
// 找出最早的日期
const dates = records
.map(record => record.created)
.filter(date => date)
.map(date => new Date(date));
if (dates.length === 0) {
const date = new Date();
date.setDate(date.getDate() - 30);
return date.toISOString().split('T')[0];
}
const earliestDate = new Date(Math.min(...dates));
return earliestDate.toISOString().split('T')[0];
}
// 显示搜索结果
function displayResults(records, container, type = '寄样', sendSampleRecordsWithReturnQty = []) {
if (records.length === 0) {
container.innerHTML = `<div style="text-align: center; color: #999;">未找到匹配的${type}记录</div>`;
} else {
let html = `<div style="font-weight: bold; margin-bottom: 8px;">${type}详细:</div>`;
html += '<div style="display: flex; flex-direction: column; gap: 8px; user-select: text;">'
// 对退样记录进行处理,保持原有的显示数量,通过高度限制实现只显示三个的效果
records.forEach((record, index) => {
// 处理创建日期,保留年月日
let createdDate = '';
if (record.created) {
const dateMatch = record.created.match(/^(\d{4}-\d{2}-\d{2})/);
if (dateMatch) {
createdDate = dateMatch[1];
}
}
// 构建标题部分,包含退样数量
let titleHtml = `${index + 1}. ${record.io_id || '无'} ${record.warehouse || ''}`;
// 处理退样数量显示
if (record.returnQty !== undefined && record.returnQty > 0) {
// 根据类型和数量是否相符设置不同的颜色
let returnQtyColor;
if (type === '寄样') {
// 寄样:如果退样数量与寄样数量相符则显示绿色,否则显示红色
returnQtyColor = record.returnQty === record.qty ? '#4CAF50' : '#f44336';
} else {
// 退样:始终显示红色
returnQtyColor = '#f44336';
}
titleHtml += `-数量:${record.qty || 0} <span style="color: ${returnQtyColor}; font-weight: bold;">-${record.returnQty}</span>`;
} else {
titleHtml += `-数量:${record.qty || 0}`;
}
// 处理退样记录的数量显示(右侧退样详细)
if (type === '退样') {
// 查找当前退样记录匹配的寄样调拨单号
const matchingSendRecord = sendSampleRecordsWithReturnQty.find(sendRecord => {
return record.remark && record.remark.includes(sendRecord.io_id);
});
if (matchingSendRecord) {
titleHtml += ` <span style="color: #f44336; font-weight: bold;">-${record.qty || 0}</span>`;
}
}
// 不需要单独显示数量,标题中已经显示了
html += `
<div style="padding: 8px; border: 1px solid #e0e0e0; border-radius: 4px; background: #fff; word-wrap: break-word; user-select: text;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 4px; user-select: text;">
<div style="font-weight: bold; flex: 1; user-select: text;">${titleHtml}</div>
${createdDate ? `<div style="font-size: 12px; color: #999; margin-left: 10px; flex-shrink: 0; user-select: text;">${createdDate}</div>` : ''}
</div>
<div style="display: flex; justify-content: space-between; align-items: flex-start; font-size: 12px; color: #666; user-select: text;">
<div style="flex: 1; user-select: text;">备注:${record.remark || '无'}</div>
<button
class="copy-remark-btn"
data-remark="${encodeURIComponent((record.remark || '') + (record.io_id || ''))}"
title="复制备注和调拨单号"
style="
padding: 4px 8px;
background: #4CAF50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
flex-shrink: 0;
user-select: none;
"
onmouseover="this.style.background='#45a049'"
onmouseout="this.style.background='#4CAF50'"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="currentColor" d="M11.4 0a.85.85 0 0 1 .622.273l2.743 3a.88.88 0 0 1 .235.602v9.25a.867.867 0 0 1-.857.875H3.857A.867.867 0 0 1 3 13.125V.875C3 .392 3.384 0 3.857 0zM14 4h-2.6a.4.4 0 0 1-.4-.4V1H4v12h10z"></path>
<path fill="currentColor" d="M3 1H2a1 1 0 0 0-1 1v13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-1h-1v1H2V2h1z"></path>
</svg>
</button>
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
// 绑定复制按钮事件
document.querySelectorAll('.copy-remark-btn').forEach(btn => {
btn.addEventListener('click', async function() {
const remark = decodeURIComponent(this.dataset.remark);
try {
await navigator.clipboard.writeText(remark);
// 显示复制成功提示
this.title = '复制成功';
this.style.background = '#2196F3';
setTimeout(() => {
this.title = '复制备注';
this.style.background = '#4CAF50';
}, 1000);
} catch (error) {
console.error('复制失败:', error);
this.title = '复制失败';
this.style.background = '#f44336';
setTimeout(() => {
this.title = '复制备注';
this.style.background = '#4CAF50';
}, 1000);
}
});
});
}
}
// 搜索按钮点击事件
async function handleSearch() {
const keyword = searchInput.value.trim();
if (!keyword) {
notify('请输入搜索关键词', 'warning');
return;
}
try {
// 显示加载状态
sendSampleDisplay.innerHTML = '<div style="text-align: center; color: #4CAF50;">搜索中</div>';
returnSampleDisplay.innerHTML = '<div style="text-align: center; color: #4CAF50;">搜索中</div>';
searchBtn.disabled = true;
searchBtn.textContent = '搜索中';
// 获取当前页面的主机名,用于构建请求URL
const host = window.location.host;
const baseUrl = `https://${host}`;
const uCoId = getCookie('u_co_id');
if (!uCoId) throw new Error('cookie缺少u_co_id');
// 调用API搜索寄样调拨记录
const sendSampleRecords = await Api.searchTransferRecords(keyword);
// 为每个寄样记录获取调拨数量
const sendSampleRecordsWithQuantity = await Promise.all(sendSampleRecords.map(async (record) => {
const qty = await Api.getTransferQuantity(record.io_id, keyword);
return {
...record,
qty
};
}));
// 按照日期排序(最新的在前)
sendSampleRecordsWithQuantity.sort((a, b) => {
const dateA = new Date(a.created || 0);
const dateB = new Date(b.created || 0);
return dateB - dateA;
});
// 获取寄样数组中最早的日期
const earliestDate = getEarliestDate(sendSampleRecords);
const endDate = new Date().toISOString().split('T')[0];
const endDateTime = `${endDate} 23:59:59.998`;
// 调用API搜索退样调拨记录
const returnSampleRecords = await Api.searchReturnTransferRecords(keyword, earliestDate, endDateTime);
// 按照日期排序(最新的在前)
returnSampleRecords.sort((a, b) => {
const dateA = new Date(a.created || 0);
const dateB = new Date(b.created || 0);
return dateB - dateA;
});
// 根据左侧寄样记录的数量+2来限制退样记录的显示数量
const limit = sendSampleRecordsWithQuantity.length + 2;
const limitedReturnSampleRecords = returnSampleRecords.slice(0, limit);
// 为每个退样记录获取调拨数量
const returnSampleRecordsWithQuantity = await Promise.all(limitedReturnSampleRecords.map(async (record) => {
const qty = await Api.getTransferQuantity(record.io_id, keyword);
return {
...record,
qty
};
}));
// 为左侧寄样记录匹配退样数量(统计汇总)
const sendSampleRecordsWithReturnQty = sendSampleRecordsWithQuantity.map(sendRecord => {
// 查找所有备注中包含当前寄样调拨单号的退样记录
const matchingReturnRecords = returnSampleRecordsWithQuantity.filter(returnRecord => {
return returnRecord.remark && returnRecord.remark.includes(sendRecord.io_id);
});
// 统计汇总退样数量
const returnQty = matchingReturnRecords.reduce((total, record) => total + (record.qty || 0), 0);
return {
...sendRecord,
returnQty,
matchingReturnRecords // 保存匹配的退样记录,用于右侧显示
};
});
// 显示寄样搜索结果
displayResults(sendSampleRecordsWithReturnQty, sendSampleDisplay, '寄样');
// 显示退样搜索结果,传递寄样记录参数
displayResults(returnSampleRecordsWithQuantity, returnSampleDisplay, '退样', sendSampleRecordsWithReturnQty);
} catch (error) {
console.error('搜索失败:', error);
sendSampleDisplay.innerHTML = `<div style="text-align: center; color: #f44336;">搜索失败:${error.message}</div>`;
returnSampleDisplay.innerHTML = `<div style="text-align: center; color: #f44336;">搜索失败:${error.message}</div>`;
} finally {
// 恢复按钮状态
searchBtn.disabled = false;
searchBtn.textContent = '搜索';
}
}
searchBtn.addEventListener('click', handleSearch);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') handleSearch();
});
}
/***********************
* 初始化
***********************/
function init() {
buildUI();
enableDragAndSnap();
FormHelper.fetchUser();
bindEvents();
}
window.addEventListener('load', init);
})();