Commit 33c4ab5c authored by 黄同智's avatar 黄同智

复刻项目

parent eacb3d4c
......@@ -155,6 +155,53 @@
padding-left: 4px;
}
/* 气泡框 */
.bubble {
position: absolute;
bottom: calc(100% + 14px);
left: -25px;
width: 150px;
padding: 10px 5px;
color: #000000;
border-radius: 4px;
border: 1px solid #62A7E3;
background-color: #EAF5FE;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s ease, visibility 0.2s ease;
z-index: 100;
pointer-events: none;
}
/* 显示气泡 */
.form-question:hover .bubble {
visibility: visible;
opacity: 1;
}
.bubble::before {
content: '';
position: absolute;
bottom: -12px;
left: 23px;
border-width: 12px 4px 0 10px;
border-style: solid;
border-color: #62A7E3 transparent transparent transparent;
z-index: 1;
}
/* 背景三角(内层,盖住部分边框) */
.bubble::after {
content: '';
position: absolute;
bottom: -9px;
left: 24px;
border-width: 9px 4px 0 8px;
border-style: solid;
border-color: #EAF5FE transparent transparent transparent;
z-index: 2;
}
.question1 {
margin-left: 30px;
}
......@@ -194,10 +241,12 @@
bottom: 64px;
}
.yy {
left: 264px;
bottom: 64px;
}
.zz {
left: 390px;
bottom: 64px;
......@@ -285,12 +334,14 @@
<div class="form-row">
<label class="form-label"><span class="required">*</span>起止日期:</label>
<input type="text" id="dateFrom" class="form-input small x-small" name="dateFrom">
<input type="text" id="dateFrom" class="form-input small x-small" name="dateFrom"
oninput="startDate = this.value" onblur="handleBlur('dateFrom')">
<div class="date-picker" id="sDate">
<img class="dateimg" src="./images/date.png" alt="" />
</div>
<span>-</span>
<input type="text" id="dateTo" class="form-input small x-small" name="dateTo">
<input type="text" id="dateTo" class="form-input small x-small" name="dateTo"
oninput="endDate = this.value" onblur="handleBlur('dateTo')">
<div class="date-picker" id="eDate">
<img class="dateimg" src="./images/date.png" alt="" />
<!-- <input class="date-input dateTo" type="date" /> -->
......@@ -300,7 +351,11 @@
<label><input type="radio" name="quickDate" value="180" /> 最近180天</label>
<label><input type="radio" name="quickDate" value="365" /> 最近365天</label>
</div>
<span class="form-question question1"></span>
<span class="form-question question1">
<span class="bubble">
请输入或选择最近10年内的日期;每次查询时间段不超过1年;日期输入标准格式20180101,表示2018年1月1日
</span>
</span>
</div>
<div class="form-row">
<label class="form-label">交易金额:</label>
......@@ -334,7 +389,11 @@
<input type="text" class="form-input small" name="detailFrom">
<span class="px-2">-</span>
<input type="text" class="form-input small" name="detailTo">
<span class="form-question"></span>
<span class="form-question">
<span class="bubble">
仅输入起始编号时,查询该编号(含)之后的明细;仅输入截止编号时,查询该编号(含)之前的明细;起止编号同时输入时,截止编号需大于或等于起始编号。
</span>
</span>
</div>
<!-- 按钮组 -->
......@@ -343,7 +402,8 @@
<button class="buttonx xx" onclick="onAccount(1)" type="submit">确定</button>
</form>
<button class="buttonx yy" onclick="onAccount(2)">返回</button>
<button onclick="onAccount(3)" type="submit" class="buttonx zz" style="width: auto;padding: 0 20px;">异步下载查询</button>
<button onclick="onAccount(3)" type="submit" class="buttonx zz"
style="width: auto;padding: 0 20px;">异步下载查询</button>
</div>
<!-- 温馨提示 -->
......@@ -357,26 +417,67 @@
</html>
<script>
/**
* 解析 YYYYMMDD 格式为 Date 对象
* @param {string} dateStr - 8位数字字符串,如 20250101
* @returns {Date}
*/
function parseYYYYMMDD(dateStr) {
const year = parseInt(dateStr.substring(0, 4));
const month = parseInt(dateStr.substring(4, 6)) - 1;
const day = parseInt(dateStr.substring(6, 8));
return new Date(year, month, day);
}
/**
* 检测 YYYYMMDD 格式的日期间隔是否大于365天
* @param {string} startStr - 开始日期,如 20250101
* @param {string} endStr - 结束日期,如 20260101
* @returns {boolean}
*/
function isOver365DaysYYYYMMDD(startStr, endStr) {
const start = parseYYYYMMDD(startStr);
const end = parseYYYYMMDD(endStr);
const diffDays = (end - start) / (1000 * 60 * 60 * 24);
return Math.abs(diffDays) > 365;
}
let startDate = '';
let endDate = '';
const handleBlur = (dateId) => {
console.log('handleBlur', startDate, endDate, isOver365DaysYYYYMMDD(startDate, endDate));
if (startDate && endDate && isOver365DaysYYYYMMDD(startDate, endDate)) {
alert('起止日期间隔不能超过365天');
if (dateId === 'dateFrom') {
startDate = '';
document.getElementById(dateId).value = '未选择';
} else {
endDate = '';
document.getElementById(dateId).value = '未选择';
}
const checkedQuickDate = document.querySelector('input[name="quickDate"]:checked');
checkedQuickDate && (checkedQuickDate.checked = false);
}
};
const pickerStart = new CalendarPlugin({
trigger: '#sDate',
onSelect: function (date) {
const result = date.replaceAll('-', '');
document.getElementById('dateFrom').value = result || '未选择';
startDate = date.replaceAll('-', '');
handleBlur('dateFrom');
}
});
const pickerEnd = new CalendarPlugin({
trigger: '#eDate',
onSelect: function (date) {
const result = date.replaceAll('-', '');
document.getElementById('dateTo').value = result || '未选择';
endDate = date.replaceAll('-', '');
handleBlur('dateTo');
}
});
// 快速日期
document.querySelectorAll('input[name=quickDate]').forEach(el => {
el.addEventListener('change', () => {
......@@ -391,8 +492,10 @@
const today = new Date(formattedDate);
const from = new Date(today.getTime() - days * 86400000);
const fmt = d => d.getFullYear() + String(d.getMonth() + 1).padStart(2, '0') + String(d.getDate()).padStart(2, '0');
document.getElementById('dateFrom').value = fmt(from);
document.getElementById('dateTo').value = fmt(today);
startDate = fmt(from);
endDate = fmt(today);
document.getElementById('dateFrom').value = startDate;
document.getElementById('dateTo').value = endDate;
});
});
......@@ -401,7 +504,7 @@
function onAccount(number) {
if( number == 1) {
if (number == 1) {
form.addEventListener('submit', async (e) => {
e.preventDefault(); // 阻止页面刷新
......
......@@ -157,15 +157,18 @@
transition: all 0.2s;
}
/* 气泡框 - 完全参照截图风格:白底、圆角、箭头、阴影,左侧竖条装饰+列表排版 */
/* 气泡框 */
.bubble {
position: absolute;
bottom: calc(100% + 14px);
left: 0;
width: 320px;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 20px 35px -10px rgba(0, 0, 0, 0.2), 0 1px 3px rgba(0, 0, 0, 0.05);
left: -25px;
width: 150px;
padding: 10px 5px;
text-align: left;
color: #000000;
border-radius: 4px;
border: 1px solid #62A7E3;
background-color: #EAF5FE;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s ease, visibility 0.2s ease;
......@@ -174,21 +177,32 @@
}
/* 显示气泡 */
.question:hover .bubble {
.form-question:hover .bubble {
visibility: visible;
opacity: 1;
}
/* 箭头:朝下(因为气泡在上方,箭头指向触发元素) */
.bubble::before {
content: '';
position: absolute;
bottom: -12px;
left: 23px;
border-width: 12px 4px 0 10px;
border-style: solid;
border-color: #62A7E3 transparent transparent transparent;
z-index: 1;
}
/* 背景三角(内层,盖住部分边框) */
.bubble::after {
content: '';
position: absolute;
bottom: -8px;
bottom: -9px;
left: 24px;
border-width: 8px 8px 0 8px;
border-width: 9px 4px 0 8px;
border-style: solid;
border-color: #ffffff transparent transparent transparent;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.05));
border-color: #EAF5FE transparent transparent transparent;
z-index: 2;
}
.question1 {
......@@ -328,12 +342,14 @@
<div class="form-row">
<label class="form-label"><span class="required">*</span>起止日期:</label>
<input type="text" id="dateFrom" class="form-input small x-small" name="dateFrom">
<input type="text" id="dateFrom" class="form-input small x-small" name="dateFrom"
oninput="startDate = this.value" onblur="handleBlur('dateFrom')">
<div class="date-picker" id="sDate">
<img class="dateimg" src="./images/date.png" alt="" />
</div>
<span>-</span>
<input type="text" id="dateTo" class="form-input small x-small" name="dateTo">
<input type="text" id="dateTo" class="form-input small x-small" name="dateTo"
oninput="endDate = this.value" onblur="handleBlur('dateTo')">
<div class="date-picker" id="eDate">
<img class="dateimg" src="./images/date.png" alt="" />
<!-- <input class="date-input dateTo" type="date" /> -->
......@@ -345,7 +361,7 @@
</div>
<span class="form-question question1">
<span class="bubble">
好景不长v计划表v看见啊办法深V合计阿萨办法v啊好几把送达方v科技啊表达法v
请输入或选择最近一年半内日期;每次查询时间段不超过3个月;日期标准输入格式20180101,表示2018年1月1日
</span>
</span>
</div>
......@@ -375,7 +391,12 @@
<label><input type="radio" name="reverse" checked> 显示</label>
<label><input type="radio" name="reverse"> 不显示</label>
</div>
<span class="form-question question2"></span>
<span class="form-question question2">
<span class="bubble">
冲正交易流水查询仅供查询使用,不提供电子回执
</span>
</span>
</div>
<div class="form-row mt-20">
......@@ -409,7 +430,12 @@
<input type="text" class="form-input small" name="detailFrom">
<span class="px-2">-</span>
<input type="text" class="form-input small" name="detailTo">
<span class="form-question"></span>
<span class="form-question">
<span class="bubble">
仅输入起始编号时,查询该编号(含)之后的明细;仅输入截止编号时,查询该编号(含)之前的明细;起止编号同时输入时,截止编号需大于或等于起始编号。
</span>
</span>
</div>
<div style="height: 90px;"></div>
......@@ -438,26 +464,67 @@
</html>
<script>
/**
* 解析 YYYYMMDD 格式为 Date 对象
* @param {string} dateStr - 8位数字字符串,如 20250101
* @returns {Date}
*/
function parseYYYYMMDD(dateStr) {
const year = parseInt(dateStr.substring(0, 4));
const month = parseInt(dateStr.substring(4, 6)) - 1;
const day = parseInt(dateStr.substring(6, 8));
return new Date(year, month, day);
}
/**
* 检测 YYYYMMDD 格式的日期间隔是否大于365天
* @param {string} startStr - 开始日期,如 20250101
* @param {string} endStr - 结束日期,如 20260101
* @returns {boolean}
*/
function isOver365DaysYYYYMMDD(startStr, endStr) {
const start = parseYYYYMMDD(startStr);
const end = parseYYYYMMDD(endStr);
const diffDays = (end - start) / (1000 * 60 * 60 * 24);
return Math.abs(diffDays) > 365;
}
let startDate = '';
let endDate = '';
const handleBlur = (dateId) => {
console.log('handleBlur', startDate, endDate, isOver365DaysYYYYMMDD(startDate, endDate));
if (startDate && endDate && isOver365DaysYYYYMMDD(startDate, endDate)) {
alert('起止日期间隔不能超过365天');
if (dateId === 'dateFrom') {
startDate = '';
document.getElementById(dateId).value = '未选择';
} else {
endDate = '';
document.getElementById(dateId).value = '未选择';
}
const checkedQuickDate = document.querySelector('input[name="quickDate"]:checked');
checkedQuickDate && (checkedQuickDate.checked = false);
}
};
const pickerStart = new CalendarPlugin({
trigger: '#sDate',
onSelect: function (date) {
const result = date.replaceAll('-', '');
document.getElementById('dateFrom').value = result || '未选择';
startDate = date.replaceAll('-', '');
handleBlur('dateFrom');
}
});
const pickerEnd = new CalendarPlugin({
trigger: '#eDate',
onSelect: function (date) {
const result = date.replaceAll('-', '');
document.getElementById('dateTo').value = result || '未选择';
endDate = date.replaceAll('-', '');
handleBlur('dateTo');
}
});
// 快速日期
document.querySelectorAll('input[name=quickDate]').forEach(el => {
el.addEventListener('change', () => {
......@@ -470,15 +537,15 @@
const formattedDate = `${year}-${month}-${day}`;
const today = new Date(formattedDate);
const from = new Date(today.getTime() - days * 86400000);
const fmt = d => d.getFullYear() + String(d.getMonth() + 1).padStart(2, '0') + String(d.getDate()).padStart(2, '0');
document.getElementById('dateFrom').value = fmt(from);
document.getElementById('dateTo').value = fmt(today);
startDate = fmt(from);
endDate = fmt(today);
document.getElementById('dateFrom').value = startDate;
document.getElementById('dateTo').value = endDate;
});
});
const form = document.getElementById('myForm');
function onAccount(number) {
......@@ -488,9 +555,9 @@
// 方式1:使用 FormData 自动搜集
const formData = new FormData(form);
localStorage.setItem('accountQueryParams', JSON.stringify(Object.fromEntries(formData.entries())));
setTimeout(() => {
window.location.href = './account_list.html'
window.localStorage.setItem('dateFrom', startDate);
window.location.href = './account_list.html';
}, 500);
});
}
......
......@@ -160,14 +160,13 @@
.detail-table th {
background-color: #c8e0f5;
padding: 0px 5px;
text-align: left;
font-weight: normal;
}
.detail-table tr:nth-child(2n+1) {
background-color: #ebf4fb;
background-color: #f7fafd;
}
.detail-table tr:nth-child(2n) {
......@@ -175,7 +174,7 @@
}
.detail-table tr:hover {
background-color: #ebf4fb;
background-color: #f7fafd;
}
.detail-table td {
......@@ -301,6 +300,8 @@
/* 箭头 */
.detail-table .xflex {
width: fit-content;
margin: 0 auto;
display: flex;
text-align: center;
align-items: flex-end;
......@@ -337,7 +338,7 @@
font-size: 14px;
}
table.grid td.center {
table.grid td {
text-align: center;
}
......@@ -348,6 +349,7 @@
th {
height: 70px;
cursor: pointer;
}
/* table.grid td.col-select,
......@@ -509,6 +511,10 @@
background: url(https://xc.b2bstatic3.ccb.com/V6/STY6/CN/images7/tablePrint.png) no-repeat left center;
}
.more {
background: url(https://xc.b2bstatic3.ccb.com/V6/STY6/CN/images7/yun_genDuo.png) no-repeat left center;
}
.moreUl li {
width: 150px !important;
display: flex;
......@@ -644,7 +650,7 @@
</div>
<div class="tableLBtn_box">
<div class="tableLBtn_active">
<a href="#" class="tableRBtn download">更多功能</a>
<a href="#" class="tableRBtn more">更多功能</a>
</div>
<div class="downloadBox downloadBox2" style="left: 0;">
<ul class="moreUl">
......@@ -665,8 +671,8 @@
<table class="detail-table grid" id="resultTable">
<thead>
<tr>
<th class="col-select" style="width: 42px;">选择</th>
<th style="width: 77px;">
<th class="col-select" style="width: 44px;">选择</th>
<th style="width: 88px;">
<div class="xflex">
<span>交易时间 </span>
<div class="jiantou">
......@@ -675,7 +681,7 @@
</div>
</div>
</th>
<th colspan="2" style="width: 220px;">
<th colspan="2" style="width: 211px;">
<div class="thbox">
<div>发生额/元</div>
<div class="flex">
......@@ -696,7 +702,7 @@
</div>
</div>
</th>
<th style="width: 110px;">
<th style="width: 88px;">
<div class="xflex">
<span>余额 </span>
<div class="jiantou">
......@@ -705,7 +711,7 @@
</div>
</div>
</th>
<th style="width: 151px;">
<th style="width: 166px;">
<div class="xflex">
<span>对方户名 </span>
<div class="jiantou">
......@@ -714,7 +720,7 @@
</div>
</div>
</th>
<th style="width: 150px;">
<th style="width: 143px;">
<div class="xflex">
<span>对方账号 </span>
<div class="jiantou">
......@@ -723,7 +729,7 @@
</div>
</div>
</th>
<th style="width: 101px;">
<th style="width: 104px;">
<div class="xflex">
<span>对方<br>开户机构 </span>
<div class="jiantou">
......@@ -732,7 +738,7 @@
</div>
</div>
</th>
<th style="width: 86px;">
<th style="width: 88px;">
<div class="xflex">
<span>记账日期 </span>
<div class="jiantou">
......@@ -741,7 +747,7 @@
</div>
</div>
</th>
<th style="width: 76px;">
<th style="width: 85px;">
<div class="xflex">
<span>摘要 </span>
<div class="jiantou">
......@@ -750,7 +756,7 @@
</div>
</div>
</th>
<th style="width: 101px;">
<th style="width: 95px;">
<div class="xflex">
<span>备注 </span>
<div class="jiantou">
......@@ -768,7 +774,7 @@
</div>
</div>
</th>
<th class="thmore" style="width: 70px;display: none;">
<th class="thmore" style="width: 64px;display: none;">
<div class="xflex">
<span>企业-<br>流水号 </span>
<div class="jiantou">
......@@ -795,7 +801,7 @@
</div>
</div>
</th>
<th class="thmore" style="width: 108px;display: none;">
<th class="thmore" style="width: 109px;display: none;">
<div class="xflex">
<span>交易介质编号</span>
<div class="jiantou">
......@@ -807,7 +813,7 @@
<th style="width: 40px;">
<div id="moreright" class="lashen" onclick="lookmore()">>></div>
<div id="moreleft" style="display: none;" class="lashen" onclick="noLookmore()">
<<< /div>
<< </div>
</th>
</tr>
</thead>
......@@ -817,7 +823,7 @@
<div class="empty" id="emptyMsg" style="display:none;">未查询到符合条件的记录</div>
<!-- 工具栏/统计栏 -->
<div class="toolbar-box">
<div class="toolbar">
<!-- <div class="toolbar">
<div class="toolbar-left">
<label><input type="checkbox"> 全选</label>
<span>本次查询时间:20250901-20260428</span>
......@@ -826,13 +832,13 @@
<a href="#"><img src="/images/tableDownload.png" alt=""> 下载当前页</a>
<a href="#"><img src="/images/tableDownload.png" alt=""> 下载全部</a>
</div>
</div>
</div> -->
<!-- 工具栏 -->
<!-- <div class="toolbar">
<div class="left">
<input type="checkbox" id="filterCheckbox" onclick="openFilter(this)">
<span>筛选</span>
<div class="toolbar2">
<div class="toolbar-left">
<label><input type="checkbox"> 全选</label>
<span>本次查询时间:20250901-20260428</span>
</div>
<div id="rightType">
<div class="right">
......@@ -845,7 +851,7 @@
<li onclick="submitSelect('0','1')" class="firstLi">Txt下载</li>
<li onclick="submitSelect('0','2')">Excel下载</li>
<li onclick="submitSelect('0','3')">Csv下载</li>
<li onclick="submitSelect('0','4')">PDF下载</li>
<li onclick="downloadPDF2()">PDF下载</li>
</ul>
</div>
</div>
......@@ -858,7 +864,21 @@
<li onclick="submitSelect('0','1')" class="firstLi">Txt下载</li>
<li onclick="submitSelect('0','2')">Excel下载</li>
<li onclick="submitSelect('0','3')">Csv下载</li>
<li onclick="submitSelect('0','4')">PDF下载</li>
<li onclick="downloadPDF2()">PDF下载</li>
</ul>
</div>
</div>
<div class="tableLBtn_box">
<div class="tableLBtn_active">
<a href="#" class="tableRBtn download">下载其他格式</a>
</div>
<div class="downloadBox" style="left: 0;">
<ul>
<li class="firstLi">网银Txt</li>
<li>网银Excel</li>
<li>网银Csv</li>
<li>现金Txt</li>
<li>现金Excel</li>
</ul>
</div>
</div>
......@@ -875,7 +895,7 @@
</div>
</div>
</div>
</div> -->
</div>
<!-- 交易统计+分页栏 -->
<div class="toolbar" style="border-top:none;">
......@@ -1127,81 +1147,119 @@
return bytes;
}
// 数字字形 CID 表 (从 PDF 字体反推)
const DIGIT_CID = {
'0': '0013', '1': '0014', '2': '0015', '3': '0016', '4': '0017',
'5': '0018', '6': '0019', '7': '001A', '8': '001B', '9': '001C'
};
const TE = new TextEncoder();
function selectPdfByYear(year) {
if (year === '2024') return PDF_2024_B64;
if (year === '2025') return PDF_2025_B64;
if (year === '2026') return PDF_2026_B64;
return PDF_2026_B64;
}
const TD = new TextDecoder('latin1');
// 数字 0-9 对应的 CID (来自 PDF 字体)
const DIGIT_CIDS = ['0013', '0014', '0015', '0016', '0017', '0018', '0019', '001a', '001b', '001c'];
const DIGIT_SET = new Set(DIGIT_CIDS);
const CID_YEAR = '14a4'; // 年
const CID_MONTH = '1d38'; // 月
const CID_DAY = '1c15'; // 日
const CID_COLON = '001d'; // :
// 扫描 inflated 流, 把所有 <...> 中的 4 字符 hex CID 抽出, 记录每个 CID 的字节偏移
function extractCids(inflated) {
const text = TD.decode(inflated);
const cids = [];
const re = /<([0-9a-fA-F]+)>/g;
let m;
while ((m = re.exec(text)) !== null) {
const hex = m[1];
if (hex.length % 4 !== 0) continue;
const groupStart = m.index + 1;
for (let k = 0; k < hex.length; k += 4) {
const orig = hex.substr(k, 4);
cids.push({ cid: orig.toLowerCase(), offset: groupStart + k, origCase: orig });
}
}
return cids;
}
// 在 inflated 中找日期序列(4 数字+年+2 数字+月+2 数字+日), 替换成新日期;
// 紧随其后 5 个 CID 内若出现(2 数字+冒号+2 数字), 替换成新时间
// 返回 {bytes, dates, times} 或 null
function patchInflated(inflated, year, month, day, hour, minute) {
const cids = extractCids(inflated);
const Y = String(year).padStart(4, '0');
const M = String(month).padStart(2, '0');
const D = String(day).padStart(2, '0');
const H = String(hour).padStart(2, '0');
const Mn = String(minute).padStart(2, '0');
const newDate = (Y + M + D).split('');
const newTime = (H + Mn).split('');
const repls = [];
let nDates = 0, nTimes = 0;
const pushRepl = (cid, digit) => {
let nc = DIGIT_CIDS[parseInt(digit, 10)];
if (/[A-F]/.test(cid.origCase)) nc = nc.toUpperCase();
repls.push({ offset: cid.offset, newCid: nc });
};
let i = 0;
while (i <= cids.length - 11) {
const c = cids;
const ok =
DIGIT_SET.has(c[i].cid) && DIGIT_SET.has(c[i + 1].cid) &&
DIGIT_SET.has(c[i + 2].cid) && DIGIT_SET.has(c[i + 3].cid) &&
c[i + 4].cid === CID_YEAR &&
DIGIT_SET.has(c[i + 5].cid) && DIGIT_SET.has(c[i + 6].cid) &&
c[i + 7].cid === CID_MONTH &&
DIGIT_SET.has(c[i + 8].cid) && DIGIT_SET.has(c[i + 9].cid) &&
c[i + 10].cid === CID_DAY;
if (!ok) { i++; continue; }
[0, 1, 2, 3, 5, 6, 8, 9].forEach((p, idx) => pushRepl(c[i + p], newDate[idx]));
nDates++;
// 仅在日期附近找时间(避开数据表里的交易时间), 上限往后 5 个 CID
const lookEnd = Math.min(cids.length - 5, i + 16);
for (let j = i + 11; j <= lookEnd; j++) {
if (
DIGIT_SET.has(c[j].cid) && DIGIT_SET.has(c[j + 1].cid) &&
c[j + 2].cid === CID_COLON &&
DIGIT_SET.has(c[j + 3].cid) && DIGIT_SET.has(c[j + 4].cid)
) {
[0, 1, 3, 4].forEach((p, idx) => pushRepl(c[j + p], newTime[idx]));
nTimes++;
break;
}
}
i += 11;
}
if (!repls.length) return null;
const out = new Uint8Array(inflated);
for (const r of repls) {
for (let k = 0; k < 4; k++) out[r.offset + k] = r.newCid.charCodeAt(k);
}
return { bytes: out, dates: nDates, times: nTimes };
}
// "2026年02月28日" 的字节模式 — 11 个 CID + kerning 组成的固定 ASCII 串
// <2><0><2><6><年><0><2><月><2><8><日>
function buildDateBytes(year, month, day) {
const Y = String(year).padStart(4, '0').split('');
const M = String(month).padStart(2, '0').split('');
const D = String(day).padStart(2, '0').split('');
return TE.encode(
`<${DIGIT_CID[Y[0]]}>-10.000000<${DIGIT_CID[Y[1]]}>` +
`<${DIGIT_CID[Y[2]]}>-10.000000<${DIGIT_CID[Y[3]]}>` +
`-10.000000<14A4>` +
`<${DIGIT_CID[M[0]]}>-10.000000<${DIGIT_CID[M[1]]}>` +
`<1D38>` +
`<${DIGIT_CID[D[0]]}>-10.000000<${DIGIT_CID[D[1]]}>` +
`-10.000000<1C15>`
);
}
// "09:42" 时间的字节模式 — 5 个 CID
function buildTimeBytes(hour, minute) {
const H = String(hour).padStart(2, '0').split('');
const Mn = String(minute).padStart(2, '0').split('');
return TE.encode(
`<${DIGIT_CID[H[0]]}><${DIGIT_CID[H[1]]}>` +
`-10.000000<001D>-10.000000` +
`<${DIGIT_CID[Mn[0]]}>-10.000000<${DIGIT_CID[Mn[1]]}>`
);
}
function indexOfBytes(haystack, needle, fromIdx = 0) {
outer: for (let i = fromIdx; i <= haystack.length - needle.length; i++) {
for (let j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) continue outer;
}
return i;
}
return -1;
}
function replaceAllBytes(buf, oldBytes, newBytes) {
if (oldBytes.length !== newBytes.length) throw new Error('length mismatch');
let n = 0, idx = 0;
while ((idx = indexOfBytes(buf, oldBytes, idx)) >= 0) {
buf.set(newBytes, idx);
idx += newBytes.length;
n++;
}
return n;
}
// 解析 PDF 顶层结构,遍历所有 FlateDecode 流,inflate→替换→deflate+pad→写回
function patchPdf(pdfBytes, oldDate, newDate, oldTime, newTime) {
// 用 latin1 视角扫描 stream/endstream 标记 (字节 1:1)
// 解析 PDF 顶层结构, 遍历所有 FlateDecode 流, inflate→替换→deflate+pad→写回
function patchPdf(pdfBytes, year, month, day, hour, minute) {
const text = TD.decode(pdfBytes);
const re = /(\d+)\s+(\d+)\s+obj\s*([\s\S]*?)\bstream\r?\n([\s\S]*?)\r?\nendstream/g;
let totalReplaced = 0;
let m;
// 收集所有命中的修改 (offset, oldLen, newBytes)
const patches = [];
let m;
while ((m = re.exec(text)) !== null) {
const dictText = m[3];
if (!/\/Filter\s*(\[\s*)?\/FlateDecode/.test(dictText)) continue;
const streamStart = m.index + m[0].indexOf('stream') + 'stream'.length;
const nl = pdfBytes[streamStart] === 0x0D ? 2 : 1; // \r\n or \n
const nl = pdfBytes[streamStart] === 0x0D ? 2 : 1;
const payloadStart = streamStart + nl;
// 找 endstream 的开始
const endIdx = m.index + m[0].lastIndexOf('endstream');
let payloadEnd = endIdx;
// PDF 规范: endstream 前最多有一个 EOL 标志(\r\n / \n / \r). 只剥一个, 否则可能误删数据字节
// endstream 前最多有一个 EOL, 只剥一个
if (payloadEnd - payloadStart >= 2 && pdfBytes[payloadEnd - 2] === 0x0D && pdfBytes[payloadEnd - 1] === 0x0A) {
payloadEnd -= 2;
} else if (payloadEnd - payloadStart >= 1 && (pdfBytes[payloadEnd - 1] === 0x0A || pdfBytes[payloadEnd - 1] === 0x0D)) {
......@@ -1209,39 +1267,27 @@
}
const payload = pdfBytes.subarray(payloadStart, payloadEnd);
// 仅处理 /Filter /FlateDecode 流
if (!/\/Filter\s*(\[\s*)?\/FlateDecode/.test(dictText)) continue;
let inflated;
try { inflated = pako.inflate(payload); } catch (e) { continue; }
let mutated = new Uint8Array(inflated); // copy
let n1 = replaceAllBytes(mutated, oldDate, newDate);
let n2 = replaceAllBytes(mutated, oldTime, newTime);
if (n1 + n2 === 0) continue;
const r = patchInflated(inflated, year, month, day, hour, minute);
if (!r) continue;
// 重新压缩并 pad 到原长度
let deflated = pako.deflate(mutated, { level: 9 });
let deflated = pako.deflate(r.bytes, { level: 9 });
if (deflated.length > payload.length) {
// 试更高压缩仍超 — 用 level 9 已是上限, 改成移除 filter 不现实
// 退化: 用 store mode
deflated = pako.deflate(mutated, { level: 0 });
deflated = pako.deflate(r.bytes, { level: 0 });
}
if (deflated.length > payload.length) {
throw new Error(`deflate length ${deflated.length} > original ${payload.length}, stream ${m[1]}`);
}
// pad: 直接补 0 字节会破坏 zlib 校验, 改用追加 sync flush 块到长度
const out = new Uint8Array(payload.length);
out.set(deflated);
// 多余空间填 0; 由于 zlib 在解码 deflated 完成后就结束读取, 后续字节是 PDF 外的"垃圾"
// 测试:大多 PDF reader 容忍 stream 末尾有冗余
totalReplaced += n1 + n2;
totalReplaced += r.dates + r.times;
patches.push({ start: payloadStart, end: payloadEnd, replacement: out });
}
if (!patches.length) return null;
// 应用 patch
const result = new Uint8Array(pdfBytes);
for (const p of patches) {
result.set(p.replacement, p.start);
// 末尾余量填 0x20 (空格), 不影响 PDF 文法且解码器会忽略
for (let i = p.start + p.replacement.length; i < p.end; i++) {
result[i] = 0x20;
}
......@@ -1251,18 +1297,16 @@
function downloadPDF2() {
if (typeof pako === 'undefined') { alert('pako 未加载'); return; }
// const linkEl = document.querySelector('.result-bar .actions a:last-child');
// const orig = linkEl.textContent;
// linkEl.textContent = '生成中...';
const dateFrom=window.localStorage.getItem('dateFrom'); // 先清空缓存, 避免调试时旧数据干扰
console.log('dateFrom', dateFrom)
setTimeout(() => {
try {
const pdf = base64ToBytes(PDF_B64);
// const dateFrom = document.getElementById('dateFrom').value.trim();
const year = dateFrom.slice(0, 4);
const pdfB64 = selectPdfByYear(year);
const pdf = base64ToBytes(pdfB64);
const now = new Date();
const oldDate = buildDateBytes(2026, 2, 28);
const newDate = buildDateBytes(now.getFullYear(), now.getMonth() + 1, now.getDate());
const oldTime = buildTimeBytes(9, 42);
const newTime = buildTimeBytes(now.getHours(), now.getMinutes());
const r = patchPdf(pdf, oldDate, newDate, oldTime, newTime);
const r = patchPdf(pdf, now.getFullYear(), now.getMonth() + 1, now.getDate(), now.getHours(), now.getMinutes());
const finalBytes = r ? r.bytes : pdf;
const blob = new Blob([finalBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
......@@ -1271,7 +1315,7 @@
a.download = '活期账户明细下载全部.pdf';
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
console.log('日期替换次数:', r ? r.count : 0);
console.log('替换次数:', r ? r.count : 0);
} catch (e) {
console.error(e);
alert('PDF 处理失败: ' + e.message);
......
......@@ -54,6 +54,7 @@
padding-left: 10px;
padding-right: 11px;
}
.filter .left {
display: flex;
align-items: center;
......@@ -364,6 +365,7 @@
table.grid th.col-select {
display: none;
} */
.form-question {
cursor: pointer;
margin-left: 8px;
......@@ -377,6 +379,53 @@
color: #fff;
padding-left: 4px;
}
/* 气泡框 */
.bubble {
position: absolute;
bottom: calc(100% + 14px);
left: -25px;
width: 150px;
padding: 10px 5px;
color: #000000;
border-radius: 4px;
border: 1px solid #62A7E3;
background-color: #EAF5FE;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s ease, visibility 0.2s ease;
z-index: 100;
pointer-events: none;
}
/* 显示气泡 */
.form-question:hover .bubble {
visibility: visible;
opacity: 1;
}
.bubble::before {
content: '';
position: absolute;
bottom: -12px;
left: 23px;
border-width: 12px 4px 0 10px;
border-style: solid;
border-color: #62A7E3 transparent transparent transparent;
z-index: 1;
}
/* 背景三角(内层,盖住部分边框) */
.bubble::after {
content: '';
position: absolute;
bottom: -9px;
left: 24px;
border-width: 9px 4px 0 8px;
border-style: solid;
border-color: #EAF5FE transparent transparent transparent;
z-index: 2;
}
</style>
</head>
......@@ -402,7 +451,11 @@
<div class="left">
<input type="checkbox" id="filterCheckbox" onclick="openFilter(this)">
<span>筛选</span>
<span class="form-question"></span>
<span class="form-question">
<span class="bubble">
支持通过任务设定日期等要素筛选异步查询任务,设定日期仅支持按单日筛选
</span>
</span>
</div>
</div>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment