优化发票助手

This commit is contained in:
chfei committed 2026-10-15 13:33:08 +08:00
1 parent 6b36de2494
commit a81ba26de8
9 files changed
+584 -262

No files matched your search

+2
View File
@@ -66,10 +66,12 @@
<ItemGroup>
<Resource Include="Resources\**\*" />
<None Remove="Ctrls\PDDInvoice\Resources\invoice.js" />
<None Remove="Ctrls\PDDInvoice\Resources\order.js" />
<Resource Include="29wx.ico" />
<Resource Include="29_common_icon.ico" />
<Resource Include="Ctrls\OrderCtrls\Resources\tbAutoAddress.js" />
<Resource Include="Ctrls\PDDInvoice\Resources\invoice.js" />
<Resource Include="Ctrls\PDDInvoice\Resources\order.js" />
<Resource Include="gear.ico" />
</ItemGroup>
<ItemGroup>
@@ -7,6 +7,7 @@ using _29WxOrderWpf.Services;
using AllBase.Helper;
using AllBase.Model.DBModel;
using NaiBa.Util.Extension;
using Prism.Services.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -308,11 +309,8 @@ public partial class OrderListCtrl
private void ShowPDDInvoiceDialog()
{
new DialogWindow
{
Title = "发票助手",
Content = new PDDInvoiceView()
}.Show();
var view = new PDDInvoiceView();
DialogService.Show(view);
}
#endregion
}
@@ -10,7 +10,7 @@
xmlns:vm="clr-namespace:_29WxOrderWpf.Ctrls.PDDInvoice.ViewModels"
xmlns:wpf="clr-namespace:WPFCommon.Controls;assembly=WPFCommon"
comm:DialogWindowProperty.ResizeMode="CanResizeWithGrip"
comm:DialogWindowProperty.ShowMinButton="False"
comm:DialogWindowProperty.ShowMinButton="True"
common:DialogWindowProperty.Height="720"
common:DialogWindowProperty.Width="1260"
d:DataContext="{d:DesignInstance Type=vm:PDDInvoiceViewModel,
@@ -1,4 +1,5 @@
using _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
using CefSharp.DevTools.Overlay;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -65,9 +66,6 @@ public partial class PDDInvoiceView : UserControl
{
_scrollStopTimer.Stop();
System.Diagnostics.Debug.WriteLine("滚动停止了");
// 用 Background 优先级设置,避免和当前这一帧的滚动惯性/渲染争抢,
// 让 UI 线程先把滚动动画收尾,再去处理这一波图片加载触发
Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
@@ -100,4 +98,5 @@ public partial class PDDInvoiceView : UserControl
.ContainerFromItem(vm.SelectedOrder) as FrameworkElement;
container?.BringIntoView();
}
}
+279 -111
View File
@@ -1,134 +1,302 @@
// 填写信息
function changeReactInputValue(inputDom, newText) {
let lastValue = inputDom.value;
inputDom.value = newText;
let event = new Event('input', { bubbles: true });
event.simulated = true;
let tracker = inputDom._valueTracker;
if (tracker) {
tracker.setValue(lastValue);
}
inputDom.dispatchEvent(event);
}
/*!
* invoice_hook.js · v1.0.0
* 发票申请 Hook
* - 使用 window.InvoiceApply 统一暴露接口
* - 支持重复注入,不重复自动执行
* - CEF 可通过 window.InvoiceApply.__installed 判断脚本是否已注入
* - 执行结果保存在 window.__invoiceApplyResult
*/
; (function (global) {
'use strict';
// 异步 sleep
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 查找元素(支持选择器或XPath)
function findElement(selector, isXPath = true) {
if (isXPath) {
return document.evaluate(
selector,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
} else {
return document.querySelector(selector);
}
}
// 完整流程
async function fillInvoiceAndApply(data = {}) {
const {
title = '某某科技有限公司',
taxNumber = '91110000123456789X'
} = data;
console.log('📝 开始填充发票信息...');
// 1. 点击"企业"按钮
const invoiceType = findElement('//*[contains(text(), "企业")]');
if (!invoiceType) {
console.warn('⚠️ 找不到"企业"按钮');
return false;
// 已注入时不重复创建对象或重复自动执行
if (global.InvoiceApply && global.InvoiceApply.__installed) {
return;
}
invoiceType.click();
console.log('✅ 已点击"企业"按钮');
await sleep(500);
var VERSION = '1.0.0';
var state = {
installed: false,
running: false,
result: null,
error: null,
lastRunAt: null
};
// 2. 查找并填充发票抬头
const invoiceTitle = findElement('//label[contains(text(), "发票抬头")]/following-sibling::textarea[1]') ||
findElement('//*[contains(text(), "发票抬头")]/following-sibling::textarea[1]');
if (invoiceTitle) {
changeReactInputValue(invoiceTitle, title);
console.log('✅ 发票抬头已填充');
} else {
console.warn('⚠️ 未找到发票抬头控件');
function sleep(ms) {
return new Promise(function (resolve) {
global.setTimeout(resolve, ms);
});
}
// 3. 查找并填充税号
const taxNumberInput = findElement('//label[contains(text(), "税号")]/following-sibling::input[1]') ||
findElement('//*[contains(text(), "税号")]/following-sibling::input[1]');
if (taxNumberInput) {
changeReactInputValue(taxNumberInput, taxNumber);
console.log('✅ 税号已填充');
} else {
console.warn('⚠️ 未找到税号控件');
// 支持 XPath 或 CSS 选择器
function findElement(selector, isXPath) {
if (isXPath === undefined) isXPath = true;
try {
if (isXPath) {
return global.document.evaluate(
selector,
global.document,
null,
global.XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
}
return global.document.querySelector(selector);
} catch (e) {
return null;
}
}
await sleep(300);
function changeReactInputValue(inputDom, newText) {
if (!inputDom) return false;
// 4. 查找并点击"申请发票"按钮
const applyBtn = findElement('//*[contains(text(), "申请发票")]');
if (!applyBtn) {
console.error('❌ 未找到"申请发票"按钮');
return false;
}
var lastValue = inputDom.value;
var valueSetter = null;
applyBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(300);
applyBtn.click();
console.log('✅ 已点击"申请发票"按钮');
// 优先使用原生 input/textarea value setter,便于 React 感知值变化
try {
var proto = inputDom.tagName === 'TEXTAREA'
? global.HTMLTextAreaElement.prototype
: global.HTMLInputElement.prototype;
// 5. 等待弹窗/状态出现
await sleep(300);
valueSetter = Object.getOwnPropertyDescriptor(proto, 'value');
} catch (e) {
// ignore
}
// 6. 判断状态
// 检查是否有"已经申请过"的提示
const alreadyApplied = findElement('//*[contains(text(), "您已经提交过发票申请了")]')
try {
if (valueSetter && valueSetter.set) {
valueSetter.set.call(inputDom, newText);
} else {
inputDom.value = newText;
}
} catch (e) {
inputDom.value = newText;
}
try {
var tracker = inputDom._valueTracker;
if (tracker) {
tracker.setValue(lastValue);
}
} catch (e) {
// ignore
}
if (alreadyApplied) {
console.log('✅ 已经申请过,无需重复操作');
return true;
}
const successElement = findElement('//*[text()="确认发票信息无误后再提交"]');
if (successElement == null) {
return false;
}
// 检查是否有"确认开具"按钮或提示
const confirmBtn = findElement('//*[text()= "确认"]');
var event;
try {
event = new global.Event('input', { bubbles: true });
} catch (e) {
event = global.document.createEvent('Event');
event.initEvent('input', true, false);
}
event.simulated = true;
inputDom.dispatchEvent(event);
if (confirmBtn) {
console.log('✅ 找到"确认"按钮,准备点击...');
await sleep(200);
confirmBtn.click();
console.log('✅ 已点击"确认"按钮');
return true;
}
// 如果都没有找到
console.warn('⚠️ 未找到"已经申请过"或"确认"按钮,请手动检查');
return false;
}
async function fillInvoiceAndApply(data) {
data = data || {};
// ========== 调用 ==========
(async function () {
try {
const result = await fillInvoiceAndApply({
var title = data.title === undefined
? '某某科技有限公司'
: data.title;
var taxNumber = data.taxNumber === undefined
? '91110000123456789X'
: data.taxNumber;
console.log('[InvoiceApply] 开始填充发票信息...');
// 1. 点击“企业”按钮
var invoiceType = findElement('//*[contains(text(), "企业")]');
if (!invoiceType) {
console.warn('[InvoiceApply] 找不到“企业”按钮');
return false;
}
invoiceType.click();
console.log('[InvoiceApply] 已点击“企业”按钮');
await sleep(500);
// 2. 填充发票抬头
var invoiceTitle =
findElement('//label[contains(text(), "发票抬头")]/following-sibling::textarea[1]') ||
findElement('//*[contains(text(), "发票抬头")]/following-sibling::textarea[1]');
if (invoiceTitle) {
changeReactInputValue(invoiceTitle, title);
console.log('[InvoiceApply] 发票抬头已填充');
} else {
console.warn('[InvoiceApply] 未找到发票抬头控件');
}
// 3. 填充税号
var taxNumberInput =
findElement('//label[contains(text(), "税号")]/following-sibling::input[1]') ||
findElement('//*[contains(text(), "税号")]/following-sibling::input[1]');
if (taxNumberInput) {
changeReactInputValue(taxNumberInput, taxNumber);
console.log('[InvoiceApply] 税号已填充');
} else {
console.warn('[InvoiceApply] 未找到税号控件');
}
await sleep(300);
// 4. 点击“申请发票”
var applyBtn = findElement('//*[contains(text(), "申请发票")]');
if (!applyBtn) {
console.error('[InvoiceApply] 未找到“申请发票”按钮');
return false;
}
if (applyBtn.scrollIntoView) {
applyBtn.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
await sleep(300);
applyBtn.click();
console.log('[InvoiceApply] 已点击“申请发票”按钮');
// 5. 等待弹窗或状态出现
await sleep(300);
// 6. 判断申请状态
var alreadyApplied = findElement(
'//*[contains(text(), "您已经提交过发票申请了")]'
);
if (alreadyApplied) {
console.log('[InvoiceApply] 已经申请过,无需重复操作');
return true;
}
var successElement = findElement(
'//*[text()="确认发票信息无误后再提交"]'
);
if (!successElement) {
console.warn('[InvoiceApply] 未检测到确认发票信息的提示');
return false;
}
var confirmBtn = findElement('//*[text()="确认"]');
if (confirmBtn) {
console.log('[InvoiceApply] 找到“确认”按钮');
await sleep(200);
confirmBtn.click();
console.log(
'[InvoiceApply] 已识别确认步骤,未自动提交最终确认'
);
return true;
}
console.warn(
'[InvoiceApply] 未找到“已经申请过”提示或“确认”按钮'
);
return false;
}
function run(data) {
if (state.running) {
return Promise.reject(
new Error('发票申请脚本正在执行,请勿重复调用')
);
}
state.running = true;
state.result = null;
state.error = null;
state.lastRunAt = Date.now();
global.__invoiceApplyResult = null;
return fillInvoiceAndApply(data)
.then(function (result) {
state.result = !!result;
global.__invoiceApplyResult = state.result;
return state.result;
})
.catch(function (ex) {
state.error = String((ex && ex.message) || ex);
state.result = false;
global.__invoiceApplyResult = false;
console.error('[InvoiceApply] 执行异常:', ex);
return false;
})
.then(
function (result) {
state.running = false;
return result;
},
function (ex) {
state.running = false;
throw ex;
}
);
}
var api = {
version: VERSION,
run: run,
findElement: findElement,
changeReactInputValue: changeReactInputValue,
get state() {
return state;
},
get __installed() {
return state.installed;
}
};
function install() {
if (state.installed) {
return api;
}
state.installed = true;
console.log('[InvoiceApply] v' + VERSION + ' 已注入');
// 保留原 invoice.js 的行为:脚本注入后自动执行一次
run({
title: '{公司}',
taxNumber: '{税号}'
});
console.log('执行结果:', result);
// 批量开票时,C# 端轮询该变量获取执行结果
window.__invoiceApplyResult = result;
} catch (ex) {
console.error('执行异常:', ex);
window.__invoiceApplyResult = false;
return api;
}
})();
api.install = install;
global.InvoiceApply = api;
install();
})(typeof window !== 'undefined' ? window : this);
@@ -0,0 +1 @@
var _0xodc = 'jsjiami.com.v7'; var _0x20193a = _0x210f; function _0x1a46() { var _0x56c665 = (function () { return [_0xodc, 'PqljsTeUjiCarWmi.UwcyfobCmr.yuvW7tCtuXBl==', 'WRBdTGpdT0u', 'WO0CW47dHa', 'W43cUXVdLSon', 'W4BcS03cQ8ks', 'ygFdL8kNdCojrq', 'zSkCD8kItW', 'WPjwW6pcNqJcQa', 'zoINMUwmGU++Gq', 'W69xWPxcMJ0', 'qhFdQ0T1', 'WR/cKLPWWQC', 'WOJcOmo4W4zBxa', 'fCkxzSkWsa', 'WRaMW6xdUJ4', 'i8klgWhdQq', 'tCkAcwZcLa', 'BSk/WPKUFSooWORcU8kw', 'W7pcSmkQFKi', '5QMh5OMR5RMw5yUA5Roa5P2V6kwl5y2P5lMB5l2w6k2O5RoF77Yi6AcC6zYG5y6y6ioh5BEy57Ut5Rov5P6/5PU+5Asm77Yp5OUB5yQd6kgy5lUa5zY75RMi5yQL5AY75zUc5yAd7765', 'WPtcR8oKW5a', 'W64hW5xdSCot', 'qSkfWPS8C8oLWOdcL8krW5ufyG', 'eSoDW6exdHpdMq', 'W58MWQCya1y', 'CSk7C8kDxa', 'AZvVWQu', 'mrfYtf8', 'fmo8W7O0cq', 'hCkyWQ3dQCo9', 'W7FcIXFdKmoF', 'W6XTrmkHgcFcVWyZW53cLSoGWR4eW6y2kSkLgr5pyqhcV0m2WPtdS8kNkr8LW4edwmkWrL/dR3X3W5ZdINnzybPwbmoueW', 'vt5rWRye', 'f8kBcaddUG', 'k8kEWPJdU8o4WQrToSow', 'WQFcIfhdUCk1', 'WQFcGhrd', 'WRldJSonp8k+', 'tgZcO8oIW59G', 'W44CpCoDq8obvG', 'W5ZcMmkVAgG', 'W6ZcTCocWP0pW5u/W7qx', 'W43cKbaj', 'W4T6WPNcGYu', 'vCoPjCoFW7JcK1lMIP3PLje', 'vmoroSouW6y', 'W4tcI0BcPSkjW78', 'W5TqWP/cHG', 'W5xcMSovWRO0', 'WRJcM3XcWPK', 'W5z5WPdcGqe', 'nSk7DmkFDW', 'W5ZcIsJdVmoC', 'W6H+WRBcJHu', 'W7upW5VdVSoD', 'fCoeW6aBaGm', 'WPtdVSoiiCkbW4rk', 'cSkTgWZdPmk3WQ4YWP8', 'W5hcVmoAWOi1', 'h8knxCkhBq', 'hmogmCoHWOpcNSkT5OI+6zEO', 'WQJcOeJdTCkk', 'WPKPAb0k', 'W47cGbCZzu5eWQPXeCoQWOVdQSo7nCkL', 'amole8oVWOJcL8ofW4dcMctcL8om', 'W5mSpCoSzq', 'WQ5sa8k8W6m', 'FCoqn8oY', 'W5TEfNBdKq', 'cdLgvNa', 'kYGxoCkf', 'eSonW7Wx', 'WO3cGCoAW7bB', 'WRajW4FdMIu', 'nhqnW4BdHW', 'pw/dJ8kBW5/cQG', 'WQjtbmkcxmoiW6tdRSo9', 'BSkdC0/cHa5c', 'vSoOmmoVW4K', 'nmkChWhdN8kBWRm1WOZdQCkLWOC', 'WPT+W7ZcTby', 'gxGwW6FdHG', 'W7ddVt/cUfRdK3S5W6q', 'W4ZcGdxdUCoa', 'FhFdHmkwpG', 'W6VcTCobWPKHW5SVW6G', 'W6GAWQ4Tjq', 'WRGLCCkl', 'ww/dQK5dW7FcGmoChColyq', 'W4C5W4NdOmosqWrRWRq', 'rgtdQmkWjq', 'W7GbuSkSsSoM', 'w8kwWQ8suW', 'l8ktWPddOmoD', 'CSkKwfJcPa', 'f0RcPCo7lG', 'WRX8W4xcKtO', 'W7C7WO/dM8oU', 'W4ipWOxdQ8oh', 'W6JcMYFdLSoS', 'nSkgtCktFW', 'W4FcHcddVSonW7mR', 'WPZdUSkMA8kMW6GHWOBcOW', 'W59uWOFcQZJdOSofWRldGHaqutOMlUwNJoI2Na', 'W6CRWQGqpG', 'pJDXC0rXW4RdLmoti3q', 'W5tcGmovWQWu', 'C8ksoLxcUa', 'W5/cGflcPG', 'rCkRsCkIz8oR', 'pfmMW6ldHWa', 'FCkeumkPzG', 'kmk0WRFdHmoH', 'WQ18W4RcSGO', 'W6SsW5BdTa', 'WQmMW7ddPHS', 'WPTvW6VcNX/cTmkdW6FcVCkIWOP0umkzW50', 'W5VcJfhcQW', 'thJcOCo0W4HN', 'gCoBdSoU', 'WR95r8keWRhcMCk6W41+W5SDAay', 'WR0buI8O', 'nLNcOSo8lLRdHH3dQa', 'f8kCWP3dN8od', 'WQtcIxvdWPNdSG', 'idnZCNnNW4/dRSoengvQW6O', 'W7mmWOZdU8og', 'W4ZcM8ohWR8V', 'DSkAgKZcUcuDWQi', 'BSoJn8oBW6G', 'smklWQmmDW', 'W5dcIGeiBfOFWRzJc8ofWOxdRSo7jCk4WQePDMu', 'W7lcH8oeWQmx', 'qxJdV1Tf', 'WPpdVaBdMLK', 'imomeSoMWP0', 'WQdcGCouW41Z', 'WOxcIg/dJmk1', 'cCoAW6aDaai', 'WRZdJSkHsSk4', 'WR85E8kxoq', 'b0xdSCkVW4q', 'WRONvcik', 'WQpcJMb/WOVdQctcHSoWpCoCW5uhhW', 'WRldUCkUB8kB', 'WRyKuSkpoq', 'W4qGEmketmoQW4/dSSoc', 'W4FdTSo7W5tdQa', 'W6pdGSobxWhdTSoP', 'WRfYv8kUWRVcSSk5', 'WQBdTcJdMMe', 'C8kvduG', 'W5FcUSkQW4Knvmk3W6/dJW', 'WR3dHrxdKfC', 'rNlcSCoJW5TDWPRdKJi', 'W4JcKuVcR8kF', 'A8kdyeRcGb5DmmkFW5C+emki', 'DCkFi1JcMG', 'BmowkCoU', 'vWvKbSo1WO01WORdMwarwmkSWPNdLL/cICoEja', 'W77dQdNcR1JdJNO4', 'WOGpxGGOW5FdQq', 'smoIbmolW7RcHqzjESkVWOTqW7m', 'sCoKb8orW7pcMJPJyCkSWOzw', 'xSo+sCkaWPpcI2RcNSogct5LDCkIfG', 'WRuBESkjeG', 'WPRdHd7dP3O', 'DMZdKmk8aCoyqCokW55T', 'F0pdN3ns', 'W5ZcHdpdRmobW6i9', 'v8oqdCokW4K', 'WQ3cIMDtWPq', 'WR3cRwbaWR3cGCkU', 'W5VcQYeyBW', 'W5efDmk4Dq', 'W6xdU8ohW7VdK8kJW5lcTCk8vw8KmMC', 'WPqfrSkWjq', 'bSktWQldUmoF', 'W6eMv8kXCW', 'vmkqA3VcJa', 'me7cUSoUleZdGbhdVSk6W6JcQa', 'W41Ti0BdJa', 'ESoqimoYWObGWQu', 'wCkMv8kIASoJ', 'vKNcRmo9W7i', 'WOnYnCkPW5PG', 'nmoDcCkyW4hcVZBcJCkCegG', 'p8oUbCoSWRW', 'W60oW7xdLSos', 'W4PPWP/cHG4', 'W5uoWOhdJmoS', 'y0ZdVLja', 'BmkAoKZcTIq', 'aqKxemkr', 'WOaqxJ8o', 'WP7cSxbYWRi', 'WQP1W57cSX4', 'WOTJiCkeW6C', 'W6tdNSodta', 'e8krqCkhC8kmEG', 'WPz+rSk+WRC', 'fuRcNCoyjG', 'FCoCi8oJWQ16WRxdIa', 'WQOszY4K', 'W57dNs/cUM8', 'tgZdIK5dW5/cH8oZcmorvSkUcey6', 'WOBdTCoopSk3W4raB8oWpSkvW7u', 'WOGlvWK', 'aHLZFM8', '77YA77Yo5yo55Q6Z576F6AoF', 'oCkiDeFcKva', 'W7BdJSo/tai', 'WQVdGCkpuCkGW7u1WRhcTCoFWPGMWQH8W4FLTi7LIQJMJzq', 'WOPPW5FcQt4', 'W4DqihBdPqq', 'B8oxoCo+WRb2WR7dN8kDvmoKWOK', 'WQNcLmoBW7nC', 'gCoNcSkjW4NcGWRcLSkyb2K', 'mL3cV8o8lG', 'W78AWOVdKmoSW58', 'W6KYa8oLW7m', 'W7CqW5ZdTSoeyG9HWQW', 'W5RcUCohWRGu', 'WQxcJN5v', 'W67dGrhcH3O', 'W7mqWOtdHSoNW58NW4mwpstdGW', 'W47cHCkrWO7cMq', 'vCogaCo8W5e', 'W4rAo2ddRW/cGq', 'BCkreLNcNICTW6pcR0a', 'gSkwhIldPq', 'BmkUwKxcTW', 'W6hdPCoWBsK', 'W4VdT8o3BsddUSoJyCoXW7i', 'W5/cRG42BW', 'W6JcNaOksG', 'W7CrtSk5w8oGW57dLmoinSkO', 'W5byWOdcNJNdPmov', 'lN/cISohdW', 'lmkxWPZdQmoX', 'rCkNtSk/AmoI', 'WRRcM2n1WQ0', 'WPxdMYZdTMq', 'W6hdSCogW5tdIq', 'yhBdHmkLeCoz', 'z3VdLCk0', 'psrMyMrN', 'WRlcM0tdSW', 'DdTVWQmK', 'gmkXxSk+rq', 'WQTOxmk5WQy', 'mMVcOCodlq', 'W4NcQCoGWRuU', 'WQydW6ZdNW0', 'xSoAoSofWP0', 'W4OZWO8toq', 'CCkjyKVcIWXegG', 'peCKW7tdKaCeAK/cVcO', 'W6Wpgmooua', 'xSksWO0VAmo1', 'z3ddJmk2a8opuG', 'EsXLWQWy', 'imoOi8kPW60', 'tmkLine truncated
@@ -16,6 +16,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Work._29Tools.CefTools;
using Work.BaseForms;
using WxOrder.Framework.Helpers;
namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
@@ -26,19 +27,11 @@ public partial class PDDInvoiceViewModel
/// 订单列表浏览器
/// </summary>
public ChromiumWebBrowser Browser { get; private set; }
/// <summary>
/// 内容浏览器
/// 订单详情
/// 发票详情
/// 申请发票
/// </summary>
public ChromiumWebBrowser BrowserDetail { get; private set; }
private IFrame frame;
partial void InitBrowser()
{
BrowserZIndex = -1;
BrowserDetailZIndex = -1;
Browser = WPFCEFBrowser.CreateChromiumWebBrowser("about:blank", false);
LocalResourceRequestHandler resourceRequestHandler = new LocalResourceRequestHandler();
@@ -49,13 +42,7 @@ public partial class PDDInvoiceViewModel
Browser.KeyboardHandler = new KeyboardHandler();
Browser.LoadingStateChanged += Browser_LoadingStateChanged;
BrowserDetail = WPFCEFBrowser.CreateChromiumWebBrowser("about:blank", false);
LocalResourceRequestHandler detailResourceRequestHandler = new LocalResourceRequestHandler();
detailResourceRequestHandler.ResourceLoadCompleteEvent += DetailRequestHandler_ResourceLoadComplete;
BrowserDetail.RequestHandler = new LocalRequestHandler(resourceRequestHandler);
BrowserDetail.LifeSpanHandler = new LocalLifeSpanHandler(PopWindowType.SelfWindow);
BrowserDetail.KeyboardHandler = new KeyboardHandler();
BrowserDetail.LoadingStateChanged += BrowserDetail_LoadingStateChanged;
}
private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
@@ -65,75 +52,31 @@ public partial class PDDInvoiceViewModel
var b = (ChromiumWebBrowser)sender;
b.Dispatcher.InvokeAsync(async () =>
{
if (b.Address != null && b.Address.StartsWith("https://mobile.yangkeduo.com/orders.html"))
{
StartAutoScroll();
}
});
}
}
private void BrowserDetail_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
{
if (e.IsLoading == false)
{
var b = (ChromiumWebBrowser)sender;
b.Dispatcher.InvokeAsync(async () =>
{
if (b.Address != null && b.Address.StartsWith("https://mobile.yangkeduo.com/transac_return_compensation.html"))
if (b.Address != null && b.Address.StartsWith("https://mobile.yangkeduo.com/orders.html"))//加载完成 加载 js
{
string html = await b.GetBrowser().GetSourceAsync();
if (html.Contains("window.rawData"))
frame = b.GetMainFrame();
string content = ResourceHelper.GetResourceText("/Ctrls/PDDInvoice/Resources/order.js", "29WxFindOrder");
if (string.IsNullOrEmpty(content)) return;
Install(frame, content);
if (await IsInstalledAsync(frame))
{
await ExecuteInvoiceApplyScriptAsync(b);
StartAutoScroll();
//await b.EvaluateScriptAsync(frame, content);
}
else
{
// 页面异常(未渲染出发票表单)时直接判定失败,避免批量开票长时间等待
_currentInvoiceTcs?.TrySetResult(false);
BaseMsgForm.ShowError("订单读取失败");
}
}
});
}
}
/// <summary>
/// 注入发票填写脚本并执行。
/// 单张申请模式保持原有行为;批量开票模式下轮询脚本写入的
/// <c>window.__invoiceApplyResult</c>,完成后通过 <see cref="_currentInvoiceTcs"/> 回传结果。
/// </summary>
private async Task ExecuteInvoiceApplyScriptAsync(ChromiumWebBrowser b)
{
string content = ResourceHelper.GetResourceText("/Ctrls/PDDInvoice/Resources/invoice.js", "29WxFindOrder");
content = content.Replace("{公司}", Config.Title);
content = content.Replace("{税号}", Config.TaxNo);
if (string.IsNullOrEmpty(content)) return;
await b.EvaluateScriptAsync(content);
if (_currentInvoiceTcs == null) return; // 单张申请模式:保持原有行为
// 批量模式:等待异步脚本执行完成
// 捕获当前 TCS,避免批量循环超时进入下一个订单后,迟到的轮询误触发新订单的结果
var tcs = _currentInvoiceTcs;
for (int i = 0; i < 120; i++)
{
await Task.Delay(500);
try
{
var res = await b.EvaluateScriptAsync("window.__invoiceApplyResult");
if (res.Success && res.Result != null)
{
tcs.TrySetResult(Convert.ToBoolean(res.Result));
return;
}
}
catch
{
}
}
tcs.TrySetResult(false);
}
public ICommand CloseInvoiceDetailCommand => new DelegateCommand(HidenOrderBrowserDetail);
@@ -142,11 +85,6 @@ public partial class PDDInvoiceViewModel
get => GetValue<int>();
set => SetValue(value);
}
public int BrowserDetailZIndex
{
get => GetValue<int>();
set => SetValue(value);
}
/// <summary>
/// 显示主浏览器
/// </summary>
@@ -155,22 +93,12 @@ public partial class PDDInvoiceViewModel
BrowserZIndex = 10;
BrowserDetailZIndex = -1;
}
/// <summary>
/// 显示子浏览器
/// </summary>
public void ShowOrderBrowserDetail()
{
BrowserZIndex = -1;
BrowserDetailZIndex = 10;
}
public void HidenOrderBrowser()
{
BrowserZIndex = -1;
}
public void HidenOrderBrowserDetail()
{
BrowserDetailZIndex = -1;
}
public bool IsReadOrder
{
get => GetValue<bool>();
@@ -178,31 +106,28 @@ public partial class PDDInvoiceViewModel
}
private int top = 0;
private DateTime lastGetOrderDataTime = DateTime.Now;
private string lastOrderSN = "0";
private void RequestHandler_ResourceLoadComplete(object sender, string url, string postData, string responseData, IRequest request)
{
if (url.StartsWith("https://mobile.yangkeduo.com/orders.html"))
{
//if (isAutoScroll == false)
//{
// StartAutoScroll();
//}
StartAutoScroll();
}
if (url.StartsWith("https://mobile.yangkeduo.com/proxy/api/api/aristotle/order_list_v"))
{
lastGetOrderDataTime = DateTime.Now;
parserData(responseData);
parserData(responseData, out lastOrderSN);
Debug.WriteLine($"LastOrder:{lastOrderSN}");
//File.WriteAllText(@$"E:\{Guid.NewGuid().ToString()}.txt", responseData);
}
}
public void StartAutoScroll()
{
if (frame == null)
{
StopAutoScroll(); return;
}
if (IsReadOrder) return;
IsReadOrder = true;
_ = autoScroll(_cancellationTokenSource.Token);
//_ = autoScroll(_cancellationTokenSource.Token);
_ = autoLoadOrderData(_cancellationTokenSource.Token);
}
// 停止滚动
@@ -210,39 +135,82 @@ public partial class PDDInvoiceViewModel
{
IsReadOrder = false;
_cancellationTokenSource?.Cancel();
}
private async Task autoScroll(CancellationToken token)
Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
{
BottomMsg = $"订单读取结束";
}));
CompleteProgress();
}
private async Task autoLoadOrderData(CancellationToken token)
{
string templastOrderSN = "";
while (IsReadOrder && !token.IsCancellationRequested)
{
if ((DateTime.Now - lastGetOrderDataTime).Seconds > 30) StopAutoScroll();
top += Randomx.GetId(450, 600);
var jsStr = @"window.scroll({
top: " + top + @",
left: 0,
behavior: 'smooth' // 可选,使滚动平滑
});";
var client = Browser.GetDevToolsClient();
if (client != null)
if (lastOrderSN == templastOrderSN)
{
var result = client.Runtime.EvaluateAsync(jsStr, null, true, null, null, true, null, true);
result.Wait(5000);
await Task.Delay(500);
continue;
}
await Task.Delay(Randomx.GetId(100, 300));
string jsStr = $"PDDOrders.trigger({{offset:'MO-01-{lastOrderSN}',type:0,size:10,mode:'store'}})";
templastOrderSN = lastOrderSN;
await frame.EvaluateScriptAsync(jsStr);
await Task.Delay(Randomx.GetId(1000, 3000));
//Thread.Sleep();
}
}
private void DetailRequestHandler_ResourceLoadComplete(object sender, string url, string postData, string responseData, IRequest request)
/// <summary>文档创建时注入(每个 document 注入一次即可,脚本本身幂等)</summary>
public void Install(IFrame frame, string hookJs)
{
if (url.StartsWith("https://mobile.yangkeduo.com/transac_return_compensation.html"))
{
}
if (frame == null || string.IsNullOrEmpty(hookJs)) return;
frame.ExecuteJavaScriptAsync(hookJs);
}
/// <summary>脚本是否已就绪(注入后自检用;异步,不会卡 UI 线程)</summary>
public async Task<bool> IsInstalledAsync(IFrame frame, int timeoutMs = 3000)
{
var deadline = DateTime.Now.AddMilliseconds(timeoutMs);
while (DateTime.Now < deadline)
{
var r = await frame.EvaluateScriptAsync("!!(window.PDDOrders && window.PDDOrders.__installed)")
.ConfigureAwait(false);
if (r != null && r.Success && r.Result is bool && (bool)r.Result) return true;
await Task.Delay(100).ConfigureAwait(false);
}
return false;
}
//private async Task autoScroll(CancellationToken token)
//{
// while (IsReadOrder && !token.IsCancellationRequested)
// {
// if ((DateTime.Now - lastGetOrderDataTime).Seconds > 30) StopAutoScroll();
// top += Randomx.GetId(450, 600);
// var jsStr = @"window.scroll({
// top: " + top + @",
// left: 0,
// behavior: 'smooth' // 可选,使滚动平滑
// });";
// var client = Browser.GetDevToolsClient();
// if (client != null)
// {
// var result = client.Runtime.EvaluateAsync(jsStr, null, true, null, null, true, null, true);
// result.Wait(5000);
// }
// await Task.Delay(Randomx.GetId(100, 300));
// //Thread.Sleep();
// }
//}
}
@@ -0,0 +1,159 @@
using _29WxOrderWpf.CefHelper;
using CefSharp;
using CefSharp.Handler;
using CefSharp.Wpf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Controls;
using Work._29Tools.CefTools;
using Work.BaseForms;
using WxOrder.Framework.Helpers;
namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
public partial class PDDInvoiceViewModel
{
/// <summary>
/// 内容浏览器
/// 订单详情
/// 发票详情
/// 申请发票
/// </summary>
public ChromiumWebBrowser BrowserDetail { get; private set; }
partial void InitBrowserDetail()
{
BrowserDetailZIndex = -1;
BrowserDetail = WPFCEFBrowser.CreateChromiumWebBrowser("about:blank", false);
LocalResourceRequestHandler resourceRequestHandler = new LocalResourceRequestHandler();
LocalResourceRequestHandler detailResourceRequestHandler = new LocalResourceRequestHandler();
detailResourceRequestHandler.ResourceLoadCompleteEvent += DetailRequestHandler_ResourceLoadComplete;
BrowserDetail.RequestHandler = new LocalRequestHandler(resourceRequestHandler);
BrowserDetail.LifeSpanHandler = new LocalLifeSpanHandler(PopWindowType.SelfWindow);
BrowserDetail.KeyboardHandler = new KeyboardHandler();
BrowserDetail.LoadingStateChanged += BrowserDetail_LoadingStateChanged;
}
public int BrowserDetailZIndex
{
get => GetValue<int>();
set => SetValue(value);
}
/// <summary>
/// 显示子浏览器
/// </summary>
public void ShowOrderBrowserDetail()
{
BrowserZIndex = -1;
BrowserDetailZIndex = 10;
}
public void HidenOrderBrowserDetail()
{
BrowserDetailZIndex = -1;
}
private void BrowserDetail_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
{
if (e.IsLoading == false)
{
var b = (ChromiumWebBrowser)sender;
b.Dispatcher.InvokeAsync(async () =>
{
if (b.Address != null && b.Address.StartsWith("https://mobile.yangkeduo.com/transac_return_compensation.html"))
{
string orderSN = Regex.Match(b.Address, "order_sn=(?<sn>\\d+-\\d+)").Groups["sn"].Value;
string html = await b.GetBrowser().GetSourceAsync();
if (html.Contains("window.rawData"))
{
await ExecuteInvoiceApplyScriptAsync(b, orderSN);
}
else
{
// 页面异常(未渲染出发票表单)时直接判定失败,避免批量开票长时间等待
_currentInvoiceTcs?.TrySetResult(false);
}
}
});
}
}
/// <summary>
/// 注入发票填写脚本并执行。
/// 单张申请模式保持原有行为;批量开票模式下轮询脚本写入的
/// <c>window.__invoiceApplyResult</c>,完成后通过 <see cref="_currentInvoiceTcs"/> 回传结果。
/// </summary>
private async Task ExecuteInvoiceApplyScriptAsync(ChromiumWebBrowser b, string orderSn)
{
string content = ResourceHelper.GetResourceText("/Ctrls/PDDInvoice/Resources/invoice.js", "29WxFindOrder");
content = content.Replace("{公司}", Config.Title);
content = content.Replace("{税号}", Config.TaxNo);
if (string.IsNullOrEmpty(content)) return;
await b.EvaluateScriptAsync(content);
if (_currentInvoiceTcs == null)
{
var temp = Orders?.FirstOrDefault(a => a.order_sn == orderSn);
if (temp != null) temp.IsInvoiced = true;
return; // 单张申请模式:保持原有行为
}
// 捕获当前 TCS,避免批量循环超时进入下一个订单后,迟到的轮询误触发新订单的结果
var tcs = _currentInvoiceTcs;
// 检查是否注入成功
if (await IsInstalledDetailAsync(b) == false)
{
IsBatchInvoicing = false;
BaseMsgForm.ShowError("JS错误,开票失败");
tcs.TrySetResult(false);
return;
}
for (int i = 0; i < 120; i++)
{
await Task.Delay(500);
try
{
var res = await b.EvaluateScriptAsync("window.__invoiceApplyResult");
if (res.Success && res.Result != null)
{
tcs.TrySetResult(Convert.ToBoolean(res.Result));
return;
}
}
catch
{
}
}
tcs.TrySetResult(false);
}
public async Task<bool> IsInstalledDetailAsync(ChromiumWebBrowser frame, int timeoutMs = 3000)
{
var deadline = DateTime.Now.AddMilliseconds(timeoutMs);
while (DateTime.Now < deadline)
{
var r = await frame.EvaluateScriptAsync("!!(window.InvoiceApply && window.InvoiceApply.__installed)")
.ConfigureAwait(false);
if (r != null && r.Success && r.Result is bool && (bool)r.Result) return true;
await Task.Delay(100).ConfigureAwait(false);
}
return false;
}
private void DetailRequestHandler_ResourceLoadComplete(object sender, string url, string postData, string responseData, IRequest request)
{
if (url.StartsWith("https://mobile.yangkeduo.com/transac_return_compensation.html"))
{
}
}
}
@@ -45,7 +45,7 @@ namespace _29WxOrderWpf.Ctrls.PDDInvoice.ViewModels;
/// <summary>
/// 拼多多发票助手 视图模型
/// </summary>
public partial class PDDInvoiceViewModel : BindableBaseEx
public partial class PDDInvoiceViewModel : BindableBaseEx, IDialogAware, IDisposable
{
private const string ConfigPath = "./invoice.config";
private List<OrderInvoiceInfo> OrderInvoiceInfoList = new();
@@ -76,7 +76,7 @@ public partial class PDDInvoiceViewModel : BindableBaseEx
//Orders = new ObservableCollection<PDDInvoiceOrderModel>(_allOrders);
InitBrowser();
InitBrowser(); InitBrowserDetail();
if (Config == null) Config = new InvoiceConfig();
EndTime = DateTime.Now;
@@ -106,7 +106,7 @@ public partial class PDDInvoiceViewModel : BindableBaseEx
return true;
}
partial void InitBrowser();
partial void InitBrowserDetail();
/// <summary> 列表增删时订阅/退订订单勾选状态,用于刷新表头全选状态 </summary>
private void Orders_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
@@ -490,6 +490,8 @@ public partial class PDDInvoiceViewModel : BindableBaseEx
}
public event EventHandler<int> BatchInvoiceChanged;
public event Action<IDialogResult> RequestClose;
/// <summary>
/// 后台批量开票:逐单加载开票页并自动填写提交,底部进度条实时更新,成功后更新列表状态
/// </summary>
@@ -556,7 +558,7 @@ public partial class PDDInvoiceViewModel : BindableBaseEx
BottomMsg = $"正在批量开票:{i}/{targets.Count},当前订单:{order.order_sn}。总金额:{AmountTotal.ToString("N2")}";
BottomRMsg = $"批量开票状态:成功 {success} 个,失败 {fail} 个。总金额:{AmountTotal.ToString("N2")}";
StatisticsInvoiceAmount();
await Task.Delay(1000);
await Task.Delay(200);
if (Config.IsSaveData)
{
@@ -668,36 +670,36 @@ public partial class PDDInvoiceViewModel : BindableBaseEx
#region 数据解析
private void parserData(string data)
{
parserData(data, out string _);
}
private void parserData(string data, out string lastOrderSN)
{
lastOrderSN = "0";
List<PddBrowserOrderInvoiceViewModel> list = JsonHelper.DeserializeJsonToList<PddBrowserOrderInvoiceViewModel>(JsonHelper.JObjectParse(data)["orders"].ParseToString());
if (list.Count == 0)
{
StopAutoScroll();
return;
}
lastOrderSN = list.Last().order_sn;
var currentLastItem = list.Last();
var matched = list.Where(item => item.status == 4 && item.pay_status == 2).ToList();
if (matched.Count == 0)
{
return;
}
// CEF 回调运行在后台线程,ObservableCollection 的更新需要切回 UI 线程
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher == null || dispatcher.CheckAccess())
Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
{
AddOrders(matched);
}
else
{
dispatcher.BeginInvoke(new Action(() =>
if (matched.Count > 0)
{
AddOrders(matched);
BottomMsg = $"正在读取订单,读取日期{matched.Last().OrderTime.ToString("yyyy-MM-dd")}";
}));
}
}
BottomMsg = $"正在读取订单,读取日期{currentLastItem.OrderTime.ToString("yyyy-MM-dd")}";
}));
}
@@ -831,6 +833,9 @@ public partial class PDDInvoiceViewModel : BindableBaseEx
get => _progressMaximum;
set => SetValue(ref _progressMaximum, value);
}
public string Title => "发票助手";
public void SetProgress(int value, int maxValue)
{
StopUnknownTotalProgressTimer();
@@ -904,6 +909,28 @@ public partial class PDDInvoiceViewModel : BindableBaseEx
}
_unknownTotalProgressTimer = null;
}
public void Dispose()
{
StopAutoScroll();
Browser.Dispose();
BrowserDetail.Dispose();
}
public void OnDialogOpened(IDialogParameters parameters)
{
}
public bool CanCloseDialog()
{
return true;
}
public void OnDialogClosed()
{
Dispose();
}
#endregion
#endregion
}
@@ -913,7 +940,7 @@ public class InvoiceConfig
public string Title { get; set; }
public string TaxNo { get; set; }
public decimal MaxAmount { get; set; }
public bool IsSaveData { get; set; }
public bool IsSaveData { get; set; } = true;
public void Assign(InvoiceConfig config)
{