diff --git a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml
index 0643749a..52efc49e 100644
--- a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml
@@ -474,13 +474,13 @@
- 源
+ 源AUTOREV
|
- 源
+ 源AUTOREV
|
- 源重算
+ 源AUTOREV重算
|
diff --git a/YLErpWeb/fe-tests/bondCalc.test.js b/YLErpWeb/fe-tests/bondCalc.test.js
index 08f6ad7b..44eca626 100644
--- a/YLErpWeb/fe-tests/bondCalc.test.js
+++ b/YLErpWeb/fe-tests/bondCalc.test.js
@@ -1,126 +1,94 @@
/**
- * bondCalc.test.js — 债券净价/全价/收益率三字段互算的"逐字段手动锁定"守卫
+ * bondCalc.test.js — 债券净价/全价/收益率三字段互算(driver 源 + 交互约定2/3 状态机)
* ============================================================================
- * 核心保证(用户最担心的,也是第一天讨论的诉求):
- * 用户手填过的字段 = 被锁定,回写时**绝不**覆盖它;
- * 只有"未手填"的字段才从源反算得出、被回写;计算器未返回的值也不覆盖。
- * 因此用户可逐个手填全部三个,互算绝不会冲掉其中任何一个。
+ * 模型(交互约定1/2/3):
+ * - driver(bondDriverType):回车的"源"字段(CP净价 / DP全价 / YD收益率),恒为最后回车者。
+ * - bondAuto:计算器算出的另两个字段("AUTO")。
+ * - bondRev:编辑未回车的人工输入字段("REV")。
+ * - applyBondCalcResult(state, calc, driver):回写时跳过 driver 源字段(保留用户手输值),
+ * 另两字段用计算器结果覆盖——即"回车某字段即以它为源反算另两个"。
*
* 运行:cd YLErpWeb/fe-tests && npm i && npm test
- * 纯逻辑在 swapCalc.js::applyBondCalcResult(UMD,node 可直接 require)。
+ * 纯逻辑在 swapCalc.js(UMD,node 可直接 require)。
*/
const SwapCalc = require('../wwwroot/Scripts/app/swaptrade/swapCalc.js');
const CALC = { cleanPrice: 99, dirtyPrice: 100, ytm: 2.5 };
-describe('逐字段手动锁定:手填过的字段永不回写', () => {
- test('仅手填净价(CP):净价保持,全价/收益率被反算覆盖', () => {
- const state = { cleanPrice: 99.5, dirtyPrice: null, ytm: null }; // 用户手填净价 99.5
- SwapCalc.applyBondCalcResult(state, CALC, { CP: true, DP: false, YD: false });
- expect(state.cleanPrice).toBe(99.5); // 手填:绝不回写
- expect(state.dirtyPrice).toBe(100); // 未手填:被反算覆盖
+describe('以 driver 为源:回写时源字段保持、另两字段被反算覆盖', () => {
+ test('回车净价(CP)为源:净价保持,全价/收益率被反算覆盖', () => {
+ const state = { cleanPrice: 99.5, dirtyPrice: null, ytm: null };
+ SwapCalc.applyBondCalcResult(state, CALC, 'CP');
+ expect(state.cleanPrice).toBe(99.5); // 源:保持
+ expect(state.dirtyPrice).toBe(100); // 非源:被反算覆盖
expect(state.ytm).toBe(2.5);
});
- test('手填净价+全价(CP&DP):两者保持,仅收益率被覆盖', () => {
- const state = { cleanPrice: 99.5, dirtyPrice: 100.5, ytm: null };
- SwapCalc.applyBondCalcResult(state, CALC, { CP: true, DP: true, YD: false });
- expect(state.cleanPrice).toBe(99.5); // 手填:保持
- expect(state.dirtyPrice).toBe(100.5); // 手填:保持
- expect(state.ytm).toBe(2.5); // 未手填:被反算覆盖
+ test('回车全价(DP)为源:全价保持,净价/收益率被反算覆盖', () => {
+ const state = { cleanPrice: null, dirtyPrice: 100.5, ytm: null };
+ SwapCalc.applyBondCalcResult(state, CALC, 'DP');
+ expect(state.dirtyPrice).toBe(100.5); // 源:保持
+ expect(state.cleanPrice).toBe(99);
+ expect(state.ytm).toBe(2.5);
});
- test('用户不认可计算结果、手填全部三个:全部保留,无一被回写(核心场景)', () => {
- const state = { cleanPrice: 98, dirtyPrice: 101, ytm: 3.0 }; // 用户全手填
- SwapCalc.applyBondCalcResult(state, CALC, { CP: true, DP: true, YD: true });
- expect(state.cleanPrice).toBe(98); // 全部手填 → 全部保留
- expect(state.dirtyPrice).toBe(101);
- expect(state.ytm).toBe(3.0);
- });
-
- test('手填收益率(YD):收益率保持,净价/全价被覆盖', () => {
+ test('回车收益率(YD)为源:收益率保持,净价/全价被反算覆盖', () => {
const state = { cleanPrice: null, dirtyPrice: null, ytm: 2.6 };
- SwapCalc.applyBondCalcResult(state, CALC, { CP: false, DP: false, YD: true });
- expect(state.ytm).toBe(2.6);
+ SwapCalc.applyBondCalcResult(state, CALC, 'YD');
+ expect(state.ytm).toBe(2.6); // 源:保持
expect(state.cleanPrice).toBe(99);
expect(state.dirtyPrice).toBe(100);
});
-
- test('仅手填全价(DP):全价保持,净价/收益率被反算覆盖(最常用场景)', () => {
- const state = { cleanPrice: null, dirtyPrice: 100.5, ytm: null }; // 用户手填全价 100.5
- SwapCalc.applyBondCalcResult(state, CALC, { CP: false, DP: true, YD: false });
- expect(state.dirtyPrice).toBe(100.5); // 手填:绝不回写
- expect(state.cleanPrice).toBe(99); // 未手填:被反算覆盖
- expect(state.ytm).toBe(2.5);
- });
-
- test('手填净价+收益率(CP&YD):两者保持,仅全价被覆盖', () => {
- const state = { cleanPrice: 98.5, dirtyPrice: null, ytm: 3.2 };
- SwapCalc.applyBondCalcResult(state, CALC, { CP: true, DP: false, YD: true });
- expect(state.cleanPrice).toBe(98.5); // 手填:保持
- expect(state.ytm).toBe(3.2); // 手填:保持
- expect(state.dirtyPrice).toBe(100); // 未手填:被反算覆盖
- });
-
- test('手填全价+收益率(DP&YD):两者保持,仅净价被覆盖', () => {
- const state = { cleanPrice: null, dirtyPrice: 101, ytm: 2.8 };
- SwapCalc.applyBondCalcResult(state, CALC, { CP: false, DP: true, YD: true });
- expect(state.dirtyPrice).toBe(101); // 手填:保持
- expect(state.ytm).toBe(2.8); // 手填:保持
- expect(state.cleanPrice).toBe(99); // 未手填:被反算覆盖
- });
});
describe('边界:计算器未返回的值不覆盖、无变化不写', () => {
- test('手填净价 且 计算器未返收益率:源与缺失值均不写', () => {
+ test('回车净价(源) 且 计算器未返收益率:源与缺失值均不写', () => {
const state = { cleanPrice: null, dirtyPrice: null, ytm: 2.6 };
const partial = { cleanPrice: 99, dirtyPrice: 100, ytm: undefined };
- SwapCalc.applyBondCalcResult(state, partial, { CP: true, DP: false, YD: false });
- expect(state.cleanPrice).toBe(null); // CP 手填:即便 calc 返 99 也不写
- expect(state.dirtyPrice).toBe(100); // 未手填:被覆盖
- expect(state.ytm).toBe(2.6); // 计算器没返收益率 → 保持用户值
+ SwapCalc.applyBondCalcResult(state, partial, 'CP');
+ expect(state.cleanPrice).toBe(null); // CP 源:即便 calc 返 99 也不写
+ expect(state.dirtyPrice).toBe(100); // 非源:被覆盖
+ expect(state.ytm).toBe(2.6); // 计算器没返收益率 → 保持用户值
});
test('派生值与当前值差异 {
const state = { cleanPrice: 99.5, dirtyPrice: 100.0, ytm: 2.5 };
const near = { cleanPrice: 99.5, dirtyPrice: 100.00001, ytm: 2.5 };
- SwapCalc.applyBondCalcResult(state, near, { CP: true, DP: false, YD: false });
- expect(state.cleanPrice).toBe(99.5); // 手填:不写
- expect(state.dirtyPrice).toBe(100.0); // 差异 1e-5 < EPS → 不写
+ SwapCalc.applyBondCalcResult(state, near, 'CP');
+ expect(state.cleanPrice).toBe(99.5); // 源:不写
+ expect(state.dirtyPrice).toBe(100.0); // 差异 1e-5 < EPS → 不写
expect(state.ytm).toBe(2.5);
});
// 模拟 calcBondForItem:proxy 统一用【展示态】,模型字段是【存储态】,回写时 bondCalcPriceToStorage。
- test('浮动腿 item 写回:仅未手填字段被覆盖,且存储态转换正确', () => {
+ test('浮动腿 item 写回:仅非源字段被覆盖,且存储态转换正确', () => {
// 用户输入净价 99.5 → vue-number-input(percent:true) 存为存储态 0.995
- const item = { PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026,
- bondDriverType: 'CP', bondManual: { CP: true, DP: false, YD: false } };
+ const item = { PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026, bondDriverType: 'CP' };
const calc = { cleanPrice: 99, dirtyPrice: 100, ytm: 2.5 }; // 计算器返回展示态
const proxy = {
- cleanPrice: SwapCalc.bondPriceToCalc(item.PosiNetNoFeePrice), // 0.995 → 99.5(手填)
+ cleanPrice: SwapCalc.bondPriceToCalc(item.PosiNetNoFeePrice), // 0.995 → 99.5(源)
dirtyPrice: SwapCalc.bondPriceToCalc(item.PosiGrossPrice), // 1.0 → 100
ytm: SwapCalc.bondPriceToCalc(item.InitYtm) // 0.026 → 2.6
};
- SwapCalc.applyBondCalcResult(proxy, calc, item.bondManual);
+ SwapCalc.applyBondCalcResult(proxy, calc, 'CP');
item.PosiNetNoFeePrice = SwapCalc.bondCalcPriceToStorage(proxy.cleanPrice);
item.PosiGrossPrice = SwapCalc.bondCalcPriceToStorage(proxy.dirtyPrice);
item.InitYtm = SwapCalc.bondCalcPriceToStorage(proxy.ytm);
- expect(item.PosiNetNoFeePrice).toBe(0.995); // 手填(净价):存储态保持 0.995(界面仍显示 99.5)
+ expect(item.PosiNetNoFeePrice).toBe(0.995); // 源(净价):存储态保持 0.995(界面仍显示 99.5)
expect(item.PosiGrossPrice).toBe(1.0); // 全价:被反算覆盖,存储态 100/100=1.0
expect(item.InitYtm).toBe(0.025); // 收益率:被反算覆盖,存储态 2.5/100=0.025
});
// 核心回归:用户输入净价 100(存储态 1.0)时,绝不能因 proxy 残留存储态而再 ÷100 变成 0.01
test('核心回归:输入净价 100 不会被二次缩小为 1(存储态 0.01)', () => {
- const item = { PosiNetNoFeePrice: 1.0, PosiGrossPrice: null, InitYtm: null,
- bondDriverType: 'CP', bondManual: { CP: true, DP: false, YD: false } };
+ const item = { PosiNetNoFeePrice: 1.0, PosiGrossPrice: null, InitYtm: null, bondDriverType: 'CP' };
const calc = { cleanPrice: 100, dirtyPrice: 102.17928767123287, ytm: 4.860445236 };
const proxy = {
cleanPrice: SwapCalc.bondPriceToCalc(item.PosiNetNoFeePrice), // 1.0 → 100
dirtyPrice: SwapCalc.bondPriceToCalc(item.PosiGrossPrice),
ytm: SwapCalc.bondPriceToCalc(item.InitYtm)
};
- SwapCalc.applyBondCalcResult(proxy, calc, item.bondManual);
+ SwapCalc.applyBondCalcResult(proxy, calc, 'CP');
item.PosiNetNoFeePrice = SwapCalc.bondCalcPriceToStorage(proxy.cleanPrice);
item.PosiGrossPrice = SwapCalc.bondCalcPriceToStorage(proxy.dirtyPrice);
item.InitYtm = SwapCalc.bondCalcPriceToStorage(proxy.ytm);
@@ -135,15 +103,14 @@ describe('边界:计算器未返回的值不覆盖、无变化不写', () => {
[105, 107.3, 4.567890],
[100, 102.17928767123287, 4.860445236]
])('多轮净价校验:输入净价 %s 时,存储态保持 %s/100 且不会被二次缩小', (inputClean, calcDirty, calcYtm) => {
- const item = { PosiNetNoFeePrice: inputClean / 100, PosiGrossPrice: null, InitYtm: null,
- bondDriverType: 'CP', bondManual: { CP: true, DP: false, YD: false } };
+ const item = { PosiNetNoFeePrice: inputClean / 100, PosiGrossPrice: null, InitYtm: null, bondDriverType: 'CP' };
const calc = { cleanPrice: inputClean, dirtyPrice: calcDirty, ytm: calcYtm };
const proxy = {
cleanPrice: SwapCalc.bondPriceToCalc(item.PosiNetNoFeePrice),
dirtyPrice: SwapCalc.bondPriceToCalc(item.PosiGrossPrice),
ytm: SwapCalc.bondPriceToCalc(item.InitYtm)
};
- SwapCalc.applyBondCalcResult(proxy, calc, item.bondManual);
+ SwapCalc.applyBondCalcResult(proxy, calc, 'CP');
item.PosiNetNoFeePrice = SwapCalc.bondCalcPriceToStorage(proxy.cleanPrice);
item.PosiGrossPrice = SwapCalc.bondCalcPriceToStorage(proxy.dirtyPrice);
item.InitYtm = SwapCalc.bondCalcPriceToStorage(proxy.ytm);
@@ -212,22 +179,69 @@ describe('错误反馈:getBondCalcErrorMessage(对齐 C# BondCalcHepler 的
expect(SwapCalc.getBondCalcErrorMessage(resp)).toBeNull();
});
- test('端到端:业务错误时不覆盖手工输入(仅提示)', () => {
- const item = { PosiNetNoFeePrice: 99.5, PosiGrossPrice: 100.0, InitYtm: 2.6,
- bondDriverType: 'CP', bondManual: { CP: true, DP: false, YD: false } };
+ test('端到端:业务错误时保留源字段、清空另两字段与标识(交互约定2 失败)', () => {
+ const item = { PosiNetNoFeePrice: 99.5, PosiGrossPrice: 100.0, InitYtm: 2.6, bondDriverType: 'CP' };
const respObj = { errCode: 1, errMsg: "债券不存在", dirtyPrice: 0, cleanPrice: 0, ytm: 0 };
const err = SwapCalc.getBondCalcErrorMessage(respObj);
- expect(err).toBe("债券不存在"); // 有错 → 调用方会 main.message(err) 并 return
- if (!err) {
- const proxy = { cleanPrice: item.PosiNetNoFeePrice, dirtyPrice: item.PosiGrossPrice, ytm: item.InitYtm };
- SwapCalc.applyBondCalcResult(proxy, respObj, item.bondManual);
- item.PosiNetNoFeePrice = proxy.cleanPrice;
- item.PosiGrossPrice = proxy.dirtyPrice;
- item.InitYtm = proxy.ytm;
- }
- expect(item.PosiNetNoFeePrice).toBe(99.5);
- expect(item.PosiGrossPrice).toBe(100.0);
- expect(item.InitYtm).toBe(2.6);
+ expect(err).toBe("债券不存在"); // 有错 → 调用方会 main.message(err) 并调 applyBondCalcFailure
+ if (err) SwapCalc.applyBondCalcFailure(item, 'CP');
+ expect(item.PosiNetNoFeePrice).toBe(99.5); // 源:保留用户输入
+ expect(item.PosiGrossPrice).toBeNull(); // 非源:清空
+ expect(item.InitYtm).toBeNull();
+ expect(item.bondDriverType).toBeNull(); // 三字段标识全清
+ expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
+ expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
+ });
+});
+
+// ============================================================================
+// 交互约定1/2/3 状态机:applyBondCalcSuccess / applyBondCalcFailure / applyBondManualEdit
+// ============================================================================
+describe('交互约定2/3 状态机:applyBondCalcSuccess / Failure / ManualEdit', () => {
+ test('applyBondCalcSuccess(CP):源=CP,AUTO={DP,YD},REV 清空', () => {
+ const s = { bondDriverType: null, bondAuto: { CP: false, DP: false, YD: false }, bondRev: { CP: true, DP: false, YD: false } };
+ SwapCalc.applyBondCalcSuccess(s, 'CP');
+ expect(s.bondDriverType).toBe('CP');
+ expect(s.bondAuto).toEqual({ CP: false, DP: true, YD: true });
+ expect(s.bondRev).toEqual({ CP: false, DP: false, YD: false });
+ });
+
+ test('applyBondCalcSuccess(DP):源=DP,AUTO={CP,YD}', () => {
+ const s = {};
+ SwapCalc.applyBondCalcSuccess(s, 'DP');
+ expect(s.bondDriverType).toBe('DP');
+ expect(s.bondAuto).toEqual({ CP: true, DP: false, YD: true });
+ });
+
+ test('applyBondCalcFailure(CP):源值保留,另两字段清空,标识全清', () => {
+ const s = { PosiNetNoFeePrice: 99.5, PosiGrossPrice: 100, InitYtm: 2.6,
+ bondDriverType: 'CP', bondAuto: { CP: false, DP: true, YD: true }, bondRev: { CP: false, DP: false, YD: false } };
+ SwapCalc.applyBondCalcFailure(s, 'CP');
+ expect(s.PosiNetNoFeePrice).toBe(99.5); // 源:保留
+ expect(s.PosiGrossPrice).toBeNull(); // 非源:清空
+ expect(s.InitYtm).toBeNull();
+ expect(s.bondDriverType).toBeNull();
+ expect(s.bondAuto).toEqual({ CP: false, DP: false, YD: false });
+ expect(s.bondRev).toEqual({ CP: false, DP: false, YD: false });
+ });
+
+ test('applyBondManualEdit(DP):清 源/AUTO,仅 DP 标 REV(交互约定3)', () => {
+ const s = { bondDriverType: 'CP', bondAuto: { CP: false, DP: true, YD: true }, bondRev: { CP: false, DP: false, YD: false } };
+ SwapCalc.applyBondManualEdit(s, 'DP');
+ expect(s.bondDriverType).toBeNull();
+ expect(s.bondAuto).toEqual({ CP: false, DP: false, YD: false });
+ expect(s.bondRev).toEqual({ CP: false, DP: true, YD: false }); // 仅本字段 REV
+ });
+
+ test('全价回车后净价再回车:净价成新源、全价降级为 AUTO(最后回车者恒为源)', () => {
+ const s = { PosiNetNoFeePrice: null, PosiGrossPrice: null, InitYtm: null,
+ bondDriverType: null, bondAuto: { CP: false, DP: false, YD: false }, bondRev: { CP: false, DP: false, YD: false } };
+ SwapCalc.applyBondCalcSuccess(s, 'DP'); // 全价回车
+ expect(s.bondDriverType).toBe('DP');
+ expect(s.bondAuto).toEqual({ CP: true, DP: false, YD: true });
+ SwapCalc.applyBondCalcSuccess(s, 'CP'); // 净价再回车
+ expect(s.bondDriverType).toBe('CP');
+ expect(s.bondAuto).toEqual({ CP: false, DP: true, YD: true }); // 全价从源降级为 AUTO
});
});
diff --git a/YLErpWeb/fe-tests/swapCalc.test.js b/YLErpWeb/fe-tests/swapCalc.test.js
index 9dac27b6..87c6e17d 100644
--- a/YLErpWeb/fe-tests/swapCalc.test.js
+++ b/YLErpWeb/fe-tests/swapCalc.test.js
@@ -293,79 +293,77 @@ describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准',
});
// ============================================================================
-// D1 回归守卫:切换债券标的必须清空手动/源标志,避免旧债券手填状态污染新债券
-// 旧 bug:setUnderlyingCode 切债券时未重置 bondManual/bondDriverType,
-// 旧债券标记过的字段在新债券上会被错误跳过/沿用旧态。
+// D1 回归守卫:切换债券标的必须清空 源/AUTO/REV 标志,避免旧债券状态污染新债券
+// 旧 bug:setUnderlyingCode 切债券时未重置状态,旧债券标记过的字段在新债券上会被错误沿用。
// 修复:setUnderlyingCode 调 SwapCalc.clearBondCalcFlags(item)。
// ============================================================================
-describe('D1 切换标的清空债券互算手动/源标志', () => {
- test('clearBondCalcFlags 把 bondManual 三字段归 false、bondDriverType 归 null', () => {
+describe('D1 切换标的清空债券互算源/AUTO/REV 标志', () => {
+ test('clearBondCalcFlags 把 bondDriverType/bondAuto/bondRev 全清', () => {
const item = {
- bondManual: { CP: true, DP: false, YD: true },
- bondDriverType: 'YD'
+ bondDriverType: 'YD',
+ bondAuto: { CP: true, DP: true, YD: false },
+ bondRev: { CP: false, DP: false, YD: true }
};
const out = SwapCalc.clearBondCalcFlags(item);
- expect(out.bondManual).toEqual({ CP: false, DP: false, YD: false });
expect(out.bondDriverType).toBeNull();
+ expect(out.bondAuto).toEqual({ CP: false, DP: false, YD: false });
+ expect(out.bondRev).toEqual({ CP: false, DP: false, YD: false });
});
test('clearBondCalcFlags 对全新未交互标的(无标志)也安全初始化', () => {
const item = { UnderlyingCode: '200000.IB', isBond: true };
const out = SwapCalc.clearBondCalcFlags(item);
- expect(out.bondManual).toEqual({ CP: false, DP: false, YD: false });
expect(out.bondDriverType).toBeNull();
+ expect(out.bondAuto).toEqual({ CP: false, DP: false, YD: false });
+ expect(out.bondRev).toEqual({ CP: false, DP: false, YD: false });
expect(out.UnderlyingCode).toBe('200000.IB'); // 其它字段不受影响
});
- test('模拟"债券A手填→切债券B":B 不应继承 A 的手动标志', () => {
- // 债券 A:用户手填了全价,标记手动 + 设源
+ test('模拟"债券A回车→切债券B":B 不应继承 A 的源/AUTO/REV', () => {
+ // 债券 A:用户回车了全价,标记源 + AUTO
const item = { UnderlyingCode: '190000.IB', isBond: true,
- bondManual: { CP: false, DP: true, YD: false }, bondDriverType: 'DP' };
+ bondDriverType: 'DP',
+ bondAuto: { CP: true, DP: false, YD: true },
+ bondRev: { CP: false, DP: false, YD: false } };
// 切到债券 B(setUnderlyingCode 会调 clearBondCalcFlags)
SwapCalc.clearBondCalcFlags(item);
item.UnderlyingCode = '200000.IB';
- // 若不清空,applyBondCalcResult 会以旧的 bondManual.DP=true 跳过 B 的全价→错误
- const manual = item.bondManual;
- expect(manual.CP || manual.DP || manual.YD).toBe(false); // B 上无任何手动标志
- expect(item.bondDriverType).toBeNull(); // B 无计算源,下一步反算不会误用 A 的源
+ expect(item.bondDriverType).toBeNull(); // B 无计算源
+ expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
+ expect(item.bondRev).toEqual({ CP: false, DP: false, YD: false });
});
});
// ============================================================================
-// D2 回归守卫:重开(审批重开/刷新)已保存的债券成交单时,三字段互算的手动标志
-// 随页面重置丢失 → 用户一旦编辑任一价格字段就会以它为源重新反算、覆盖当初保存的其他两格。
-// 修复:加载路径对"债券且三字段齐全(净价/全价/收益率均有值)"的标的,调 markExistingBondManual
-// 把三格一次性锁为手动(true),重开期间计算器不再自动推导;点"重算"才清除重新计算。
+// D2 回归守卫:重开(审批重开/刷新)已保存的债券成交单时,三字段互算标识随页面重置丢失 →
+// 加载路径调 clearBondCalcMarksOnLoad 把 源/AUTO/REV 全部清空,纯展示保存值、无锁定。
+// 交互约定3规定"编辑未回车不联动",故用户载入后即使改某格(未回车)也不会反算另两格;
+// 只有用户主动回车某格时,才以该格为源重新推导(符合交互约定1)。
// ============================================================================
-describe('D2 重开已保存债券单 → 三字段锁定、编辑不联动另两格', () => {
- test('markExistingBondManual 把三字段全锁 true、不置计算源', () => {
- const item = { isBond: true, PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026 };
- const out = SwapCalc.markExistingBondManual(item);
- expect(out.bondManual).toEqual({ CP: true, DP: true, YD: true });
- expect(out.bondDriverType).toBeUndefined(); // 不置源 → calcBondForItem 早返回,重开零自动推导
+describe('D2 重开已保存债券单 → 三字段无标识、编辑不联动另两格', () => {
+ test('clearBondCalcMarksOnLoad 把 源/AUTO/REV 全清、不置计算源、不动价格值', () => {
+ const item = { isBond: true, PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026,
+ bondDriverType: 'CP',
+ bondAuto: { CP: false, DP: true, YD: true },
+ bondRev: { CP: false, DP: false, YD: false } };
+ const out = SwapCalc.clearBondCalcMarksOnLoad(item);
+ expect(out.bondDriverType).toBeNull();
+ expect(out.bondAuto).toEqual({ CP: false, DP: false, YD: false });
+ expect(out.bondRev).toEqual({ CP: false, DP: false, YD: false });
+ expect(out.PosiNetNoFeePrice).toBe(0.995); // 价格值本身不动,仅清标识
+ expect(out.PosiGrossPrice).toBe(1.0);
});
- test('重开后编辑某一格(如全价):另两格因 manual=true 被 applyBondCalcResult 跳过、不被覆盖', () => {
+ test('重开后仅编辑某一格(未回车):交互约定3 使另两格不被反算覆盖', () => {
const item = { isBond: true, PosiNetNoFeePrice: 0.995, PosiGrossPrice: 1.0, InitYtm: 0.026 };
- SwapCalc.markExistingBondManual(item);
- // 模拟用户编辑全价(DP):onBondPriceInput 会置 bondDriverType='DP' 并重算
- item.bondDriverType = 'DP';
- const proxy = { cleanPrice: 99.5, dirtyPrice: 100.0, ytm: 6.37 };
- const calc = { cleanPrice: 97.82, dirtyPrice: 98.5, ytm: 2.60 };
- SwapCalc.applyBondCalcResult(proxy, calc, item.bondManual);
- // 三格皆 manual=true → 一个都不回写,保存值(99.5/100.0/6.37)原样保留
- expect(proxy.cleanPrice).toBe(99.5);
- expect(proxy.dirtyPrice).toBe(100.0);
- expect(proxy.ytm).toBe(6.37);
- });
-
- test('加载路径不会误锁"全新未填的债券"(三值不全)', () => {
- // 全新债券标的,价格字段空 → 不应被标记为手动锁定
- const item = { isBond: true, PosiNetNoFeePrice: null, PosiGrossPrice: null, InitYtm: null };
- SwapCalc.markExistingBondManual(item);
- // 全新标的本不应调用 markExistingBondManual;此处断言:即便误调也不应制造假锁定干扰后续交互
- // (实际加载逻辑用 hasV 三值齐全判定,只在已保存单上调用,此用例验证函数本身不副作用其它字段)
- expect(item.UnderlyingCode).toBeUndefined();
+ SwapCalc.clearBondCalcMarksOnLoad(item);
+ // 模拟用户编辑全价(DP)但未回车:onBondPriceEdit 调 applyBondManualEdit → 清源/AUTO、DP 标 REV,不调计算器
+ SwapCalc.applyBondManualEdit(item, 'DP');
+ expect(item.bondDriverType).toBeNull(); // 无计算源 → 不会反算
+ expect(item.bondAuto).toEqual({ CP: false, DP: false, YD: false });
+ expect(item.bondRev).toEqual({ CP: false, DP: true, YD: false });
+ expect(item.PosiNetNoFeePrice).toBe(0.995); // 另两格原值保留
+ expect(item.InitYtm).toBe(0.026);
});
});
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js
index 58e7cfbe..509fa7e2 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js
@@ -201,19 +201,20 @@
}
/**
- * 债券净价/全价/收益率三字段互算:回写时跳过"用户手填过的字段"(逐字段手动锁定,永不覆盖)。
- * - manualSet: { CP:bool, DP:bool, YD:bool },标记哪些字段是用户本次会话中手动输入/修改过的
+ * 债券净价/全价/收益率三字段互算:以 driver(CP/DP/YD) 为"源",回写时跳过源字段(保留用户手输值),
+ * 其余两字段用计算器结果覆盖。
+ * - driver: 'CP'净价 / 'DP'全价 / 'YD'收益率,即本次回车的输入依据
* - calc: { cleanPrice, dirtyPrice, ytm } 来自 /Bond/CalcBond 的 resp.obj
* - state: 持有三个字段的对象(直接原地写回)
- * 设计(回应"计算结果不认可时如何优雅手动覆盖"):
- * 用户每手填一个字段,该字段即被锁定;反算只填充"未手填"的字段,已手填的(含刚编辑的)一律不回写。
- * 因此用户可逐个手填全部三个,互算绝不会冲掉其中任何一个。
+ * 设计(对应交互约定1+2):
+ * 回车某字段即以其为源调计算器,另两个字段被反算覆盖;源字段保持用户手输值不被回写。
+ * 故用户回车净价→全价/收益率被重算;再次回车全价→净价/收益率被重算(最后回车者恒为源)。
* 纯函数,jest 可直接测(见 fe-tests/bondCalc.test.js)。
*/
- function applyBondCalcResult(state, calc, manualSet) {
+ function applyBondCalcResult(state, calc, driver) {
var EPS = 1e-4;
function write(field, type, value) {
- if (manualSet && manualSet[type]) return; // 用户手填过的字段:绝不回写
+ if (driver === type) return; // 源字段:保留用户手输值,不回写
if (value === undefined || value === null) return; // 计算器未返回该值则不覆盖
var cur = state[field];
if (typeof cur === 'number' && Math.abs(cur - value) < EPS) return; // 无变化不写,避免光标跳动
@@ -241,15 +242,58 @@
return Number(displayPrice) / 100; // 展示态百分比 → 存储态小数
}
/**
- * 清空债券三字段互算的【手动/源】标志(纯函数,jest 可测)。
- * 用途:切换标的(setUnderlyingCode)时调用,避免旧债券的手填状态(bondManual/bondDriverType)
- * 污染新债券——否则旧债券标记过的字段在新债券上会被错误跳过 / 沿用旧态(D1 修复)。
- * 直接对传入对象赋值;在 Vue 组件里该 item 已是响应式对象(首次交互已 $set 过 bondManual),
- * 故重赋值能正常触发响应式更新;全新未交互过的标的清不清都无副作用。
+ * 清空债券三字段互算的【源/AUTO/REV】标志(纯函数,jest 可测)。
+ * 用途:切换标的(setUnderlyingCode)时调用,避免旧债券的状态污染新债券(D1 修复)。
+ * 直接对传入对象赋值;在 Vue 组件里该 item 已是响应式对象,重赋值能正常触发响应式更新。
*/
function clearBondCalcFlags(state) {
- state.bondManual = { CP: false, DP: false, YD: false };
state.bondDriverType = null;
+ state.bondAuto = { CP: false, DP: false, YD: false };
+ state.bondRev = { CP: false, DP: false, YD: false };
+ return state;
+ }
+
+ /**
+ * 交互约定2(纯函数,jest 可测):计算器调用【成功】后的标识落地。
+ * - 以 driver(CP/DP/YD) 为"源",标记 bondDriverType=driver;
+ * - 其余两字段标记 bondAuto=true("AUTO"=计算器算出的);
+ * - 清除 bondRev(人工输入标记)。
+ * 不写任何价格数值——数值由 applyBondCalcResult 回写。
+ */
+ function applyBondCalcSuccess(state, driver) {
+ state.bondDriverType = driver;
+ state.bondAuto = { CP: driver !== 'CP', DP: driver !== 'DP', YD: driver !== 'YD' };
+ state.bondRev = { CP: false, DP: false, YD: false };
+ return state;
+ }
+
+ /**
+ * 交互约定2(纯函数,jest 可测):计算器调用【失败】后的标识与数值落地。
+ * - 保留用户刚回车的"源"字段值(driver),让用户可就地修改;
+ * - 另两个字段清空为空白("另外两个数值都更新为空白");
+ * - 三个数值旁边的标识全部清空("三个数值旁边都清空标识")。
+ */
+ function applyBondCalcFailure(state, driver) {
+ var fields = { CP: 'PosiNetNoFeePrice', DP: 'PosiGrossPrice', YD: 'InitYtm' };
+ Object.keys(fields).forEach(function (t) {
+ if (t !== driver) state[fields[t]] = null; // 非源字段清空
+ });
+ state.bondDriverType = null;
+ state.bondAuto = { CP: false, DP: false, YD: false };
+ state.bondRev = { CP: false, DP: false, YD: false };
+ return state;
+ }
+
+ /**
+ * 交互约定3(纯函数,jest 可测):编辑某字段但未回车(失焦/输入)时的标识落地。
+ * - 清除 源 与 AUTO 标识(不再代表任何已算结果);
+ * - 仅本字段标 bondRev[type]=true("REV"=人工输入),不联动另外两字段。
+ */
+ function applyBondManualEdit(state, type) {
+ state.bondDriverType = null;
+ state.bondAuto = { CP: false, DP: false, YD: false };
+ state.bondRev = { CP: false, DP: false, YD: false };
+ if (type) state.bondRev[type] = true;
return state;
}
@@ -296,16 +340,16 @@
}
/**
- * D2 修复(纯函数,jest 可测):重开(审批重开/刷新)一只【已保存且三字段齐全】的债券成交单时,
- * 把三字段互算的手动标志一次性全置 true,使计算器在重开期间不再自动反算、覆盖当初保存的其他两个值。
- * - 不设置 bondDriverType:重开时没有任何字段作为"计算源",calcBondForItem 会早返回(要求 driver 非空),
- * 故纯展示保存值、零自动推导;用户点"重算"才清除标志并重新推导。
- * - 用户若编辑其中某格:onBondPriceInput 会把它设为 driver 并重算,但因另两格仍是 manual=true,
- * 不会被覆盖 → 满足"重开后手填覆盖跨会话 sticky、编辑不联动另两格"。
- * 仅在加载路径对"债券且三值齐全"的标的调用,全新未填的债券不会被误锁。
+ * 加载路径(重开/刷新已保存债券单)调用(纯函数,jest 可测):
+ * 把三字段互算的状态标识全部清空,使已保存的净价/全价/收益率纯展示、无任何 源/AUTO/REV 标记。
+ * - 不锁定、不置源:交互约定3规定"编辑未回车不联动",故用户载入后即使改某格(未回车)也不会反算另两格;
+ * 只有用户主动回车某格时,才以该格为源重新推导(符合交互约定1)。
+ * 仅在加载路径对"债券且三值齐全"的标的调用,全新未填的债券无副作用。
*/
- function markExistingBondManual(state) {
- state.bondManual = { CP: true, DP: true, YD: true };
+ function clearBondCalcMarksOnLoad(state) {
+ state.bondDriverType = null;
+ state.bondAuto = { CP: false, DP: false, YD: false };
+ state.bondRev = { CP: false, DP: false, YD: false };
return state;
}
@@ -342,7 +386,10 @@
bondPriceToCalc: bondPriceToCalc,
bondCalcPriceToStorage: bondCalcPriceToStorage,
clearBondCalcFlags: clearBondCalcFlags,
- markExistingBondManual: markExistingBondManual,
+ applyBondCalcSuccess: applyBondCalcSuccess,
+ applyBondCalcFailure: applyBondCalcFailure,
+ applyBondManualEdit: applyBondManualEdit,
+ clearBondCalcMarksOnLoad: clearBondCalcMarksOnLoad,
shouldShowBondErr: shouldShowBondErr,
getBondStartDateMissingMsg: getBondStartDateMissingMsg,
roundHalfAwayFromZero: roundHalfAwayFromZero,
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js
index b73a68ee..de29be48 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js
@@ -328,51 +328,61 @@ const vue = new Vue({
changeClearingAgency() {
this.trade.MetaDic["清算机构"] = this.viewState.Extend.ClearingAgency;
},
- //全价(DP)列输入:先按标的单价重算名义本金(非债券标的也需要),再对债券触发三字段反算。
- // 组件 vue-number-input 只 $emit('input')(失焦/回车时由底层 numberInput.onchange 触发),
- // 故此列绑 v-on:input,一个入口同时覆盖普通标的与债券两种情形。
+ //交互约定1+2:在净价/全价/收益率任一输入框【敲回车】→ 以该字段为源调计算器反算另两个。
+ // 成功:源字段标"源",另两个标"AUTO"(SwapCalc.applyBondCalcSuccess);
+ // 失败:保留源字段值、另两个清空、三字段标识全清(SwapCalc.applyBondCalcFailure)。
+ // 注意:计算触发只在回车(由 vue-number-input 的 enter 事件驱动),失焦不触发(见 onBondPriceEdit 约定3)。
+ onBondPriceEnter(item, type) { // type: 'CP'净价 / 'DP'全价 / 'YD'收益率
+ if (!item.isBond) return;
+ if (!item.UnderlyingCode) { main.message("请先选择债券标的"); return; }
+ var hasVal = function (v) { return v !== null && v !== '' && !isNaN(v); };
+ var price = type === 'CP' ? item.PosiNetNoFeePrice
+ : type === 'DP' ? item.PosiGrossPrice
+ : item.InitYtm;
+ if (!hasVal(price)) return; // 空/非数:不触发计算器
+ this.calcBondForItem(item, type);
+ },
+ //交互约定3:编辑某字段(失焦/输入)但未回车 → 不联动另两字段,清掉 源/AUTO 标识,
+ // 仅本字段标"REV"(人工输入),由 SwapCalc.applyBondManualEdit 落地。
+ // 由 vue-number-input 的 input 事件(失焦/回车都会触发)驱动;回车时 enter 事件会随后把标识修正为 源/AUTO。
+ onBondPriceEdit(item, type) {
+ if (!item.isBond) return;
+ SwapCalc.applyBondManualEdit(item, type);
+ },
+ //全价列:普通标的重算名义本金;债券标的按交互约定3 标 REV(不联动)。
+ // 回车计算由独立的 v-on:enter="onBondPriceEnter(item,'DP')" 负责。
onDpPriceInput(item) {
this.changeSpotPrice(item); // 名义本金随全价变化(非债券标的的既有逻辑)
- this.onBondPriceInput(item, 'DP'); // 债券:以全价为源反算净价/收益率(非债券内部会早返回)
+ if (item.isBond) SwapCalc.applyBondManualEdit(item, 'DP'); // 交互约定3:失焦未回车→REV
},
- //债券收益互换:编辑某一浮动腿的净价/全价/收益率之一 → 该字段标记为"已手动设过"(锁定),
- // 并以它为源调计算器反算"尚未手动设过"的另两个字段(已手动设过的一律不回写)
- onBondPriceInput(item, type) { // type: 'CP'净价 / 'DP'全价 / 'YD'收益率
- if (!item.isBond) return;
- var m = item.bondManual || { CP: false, DP: false, YD: false };
- m[type] = true;
- this.$set(item, 'bondManual', m); // 锁定用户手填字段
- this.$set(item, 'bondDriverType', type); // 以该字段为本次计算源
- this.calcBondForItem(item);
- },
- //放弃手动覆盖、以当前某字段为源重新自动反算(清空所有手动锁定标记)
+ //放弃手动标记、以当前某字段为源重新自动反算("重算"按钮)。
resetBondCalc(item) {
if (!item.isBond) return;
- this.$set(item, 'bondManual', { CP: false, DP: false, YD: false });
- // 优先沿用用户最后一次编辑锁定的源(bondDriverType),避免"净价残留脏值"时误以净价为源反算出连锁错误;
+ // 优先沿用用户最后一次回车锁定的源(bondDriverType),避免"净价残留脏值"时误以净价为源反算出连锁错误;
// 无历史源时再按 净价→全价→收益率 回退挑一个有值的字段。
var hasVal = function (v) { return v !== null && v !== '' && !isNaN(v); };
var driver = item.bondDriverType
|| (hasVal(item.PosiNetNoFeePrice) ? 'CP'
: hasVal(item.PosiGrossPrice) ? 'DP'
: hasVal(item.InitYtm) ? 'YD' : null);
- this.$set(item, 'bondDriverType', driver);
- this.calcBondForItem(item);
+ if (!driver) return;
+ this.calcBondForItem(item, driver);
},
- //以该浮动腿的 bondDriverType 为源调 /Bond/CalcBond;回写时跳过"已手动设过的字段"(bondManual),
- //仅反向填充用户尚未手动设过的字段——故用户可逐个手填全部三个而互算不会冲掉任一个
+ //以 driver(回车字段) 为源调 /Bond/CalcBond;回写时跳过源字段(保留用户手输值)、覆盖另两个。
//错误处理(对齐 bond-oms-ui / zszq-bond-oms):
// - 业务/参数错误(success=false 或 errCode!=0,如债券不存在/信息不全/参数非法)
// 以非阻塞 toast(main.message) 反馈真实原因,绝不回写、不弹模态框、不阻止手工输入;
// - 网络/服务异常:框架 main.post 已弹"请求失败",此处不再二次提示。
- calcBondForItem(item) {
- if (!item.isBond || !item.bondDriverType) return;
+ calcBondForItem(item, explicitDriver) {
+ var driver = explicitDriver || item.bondDriverType;
+ if (!item.isBond || !driver) return;
+ item.bondDriverType = driver; // 先定源,回写时 applyBondCalcResult 会跳过该字段
if (!item.UnderlyingCode) { main.message("请先选择债券标的"); return; }
- var price = item.bondDriverType === 'CP' ? item.PosiNetNoFeePrice
- : item.bondDriverType === 'DP' ? item.PosiGrossPrice
+ var price = driver === 'CP' ? item.PosiNetNoFeePrice
+ : driver === 'DP' ? item.PosiGrossPrice
: item.InitYtm;
if (price === null || price === '' || isNaN(price)) return; // 编辑中(空/非数)静默,不提示
- var priceType = item.bondDriverType === 'CP' ? 'CP' : item.bondDriverType === 'DP' ? 'DP' : 'YD';
+ var priceType = driver === 'CP' ? 'CP' : driver === 'DP' ? 'DP' : 'YD';
var self = this;
// 估值日:债券互换的"期初"净价/全价/收益率应以【互换起始日 StartDate】估值。
// 该字段在页面上本就是可选择的日期控件(vue-datepicker),用户可改;现已默认即有。
@@ -395,32 +405,33 @@ const vue = new Vue({
targetDate: targetDate
}, { alertFn: main.message }).done(function (resp) {
if (!resp || !resp.obj) return;
- // 业务层错误(债券不存在/信息不全/参数非法):仅提示,绝不覆盖手工输入
+ // 业务层错误(债券不存在/信息不全/参数非法):仅提示,绝不清空手工输入
var err = SwapCalc.getBondCalcErrorMessage(resp.obj);
if (err) {
// D3 修复:不可算债券上用户逐键手填时,每次按键都触发一次失败计算并弹 toast,会连刷相同提示;
// 同一错误文案连续出现只弹一次(shouldShowBondErr 维护 item._lastBondErr),抑制噪声、不影响任何计算逻辑。
if (SwapCalc.shouldShowBondErr(item, err)) main.message(err);
+ SwapCalc.applyBondCalcFailure(item, driver); // 交互约定2:失败→清另两字段+标识
return;
}
item._lastBondErr = null; // 成功则清标记,便于下次真出不同错误时仍能提示
- // 以既有三字段为代理,调用纯函数(已手动设过的字段不被覆盖),再写回。
+ // 以既有三字段为代理,调用纯函数(跳过源字段),再写回。
// 关键:proxy 内必须统一为【展示态】(per-100-face),因为 applyBondCalcResult 写入的是计算器返回的展示态。
// 模型字段是【存储态小数】(percent:true 下 1.00 对应界面 100),所以初始化时要 bondPriceToCalc(×100);
- // 若直接用存储态初始化,则用户手填字段被 applyBondCalcResult 跳过后,proxy 中仍残留存储态,
+ // 若直接用存储态初始化,则源字段被 applyBondCalcResult 跳过后,proxy 中仍残留存储态,
// 后续再 ÷100 就会导致该字段被二次缩小(如输入 100 变成 1)。
var proxy = {
cleanPrice: SwapCalc.bondPriceToCalc(item.PosiNetNoFeePrice),
dirtyPrice: SwapCalc.bondPriceToCalc(item.PosiGrossPrice),
ytm: SwapCalc.bondPriceToCalc(item.InitYtm)
};
- var manual = item.bondManual || { CP: false, DP: false, YD: false };
- SwapCalc.applyBondCalcResult(proxy, resp.obj, manual);
+ SwapCalc.applyBondCalcResult(proxy, resp.obj, driver);
// 回写前 bondCalcPriceToStorage(÷100)(展示态→存储态小数):proxy 里均为展示态;
// 模型字段存存储态(0.995),须 ÷100 落回模型,否则配合 percent:true 显示会 ×100 成离谱值。
item.PosiNetNoFeePrice = SwapCalc.bondCalcPriceToStorage(proxy.cleanPrice);
item.PosiGrossPrice = SwapCalc.bondCalcPriceToStorage(proxy.dirtyPrice);
item.InitYtm = SwapCalc.bondCalcPriceToStorage(proxy.ytm);
+ SwapCalc.applyBondCalcSuccess(item, driver); // 交互约定2:成功→标 源/AUTO
// 名义本金依赖全价(PosiGrossPrice):以净价/收益率为源反算出的全价被回写后,
// 直接赋值不会触发组件 input 事件,需在此显式重算,保持名义本金与全价一致。
if (self.calcNotional) self.calcNotional();
@@ -1517,14 +1528,13 @@ const vue = new Vue({
arr[num].index = num;
arr[num].PosiFeeType = thisObj.normalizePosiFeeType(arr[num].PosiFeeType);
this.StockEqvNotional = val.ContractSize * val.PosiQuantity * val.PosiGrossPrice;
- // D2 修复:重开(审批重开/刷新)已保存的债券成交单时,三字段互算的手动标志随页面重置而丢失;
- // 若不锁,用户一旦编辑任一价格字段就会以它为源重新反算、覆盖当初保存的其他两格。
- // 故在加载路径对"债券且三字段齐全(净价/全价/收益率均有值)"的标的,直接把三格标为手动锁定,
- // 计算器在重开期间不自动推导;用户点"重算"才清除并重新计算。
+ // 加载(重开/刷新)已保存的债券成交单:三字段互算状态标识全部清空,纯展示保存值、无任何 源/AUTO/REV 标记。
+ // 交互约定3规定"编辑未回车不联动",故用户载入后即使改某格(未回车)也不会反算另两格;
+ // 只有用户主动回车某格时,才以该格为源重新推导(符合交互约定1)。
var hasV = function (v) { return v !== null && v !== undefined && v !== '' && !isNaN(Number(v)); };
var isBond = val.isBond || tradeHelper.IsBond(val.UnderlyingInstrumentType);
if (isBond && hasV(val.PosiNetNoFeePrice) && hasV(val.PosiGrossPrice) && hasV(val.InitYtm)) {
- thisObj.$set(arr[num], 'bondManual', { CP: true, DP: true, YD: true });
+ SwapCalc.clearBondCalcMarksOnLoad(arr[num]);
}
});
if (thisObj.paySwapList.length > 0) {
diff --git a/YLErpWeb/wwwroot/Scripts/fast/fastVue.base.js b/YLErpWeb/wwwroot/Scripts/fast/fastVue.base.js
index f57f9c4a..b92ff6eb 100644
--- a/YLErpWeb/wwwroot/Scripts/fast/fastVue.base.js
+++ b/YLErpWeb/wwwroot/Scripts/fast/fastVue.base.js
@@ -332,6 +332,7 @@
if (!event) return false;
_chnInput = -1;
+ this._enterFired = false; // 每次按键先清 Enter 标记;仅在回车分支重新置位,避免误触发
let charCode = event.which || event.keyCode;
@@ -365,6 +366,7 @@
return _options.precision > 0 && !event.shiftKey && checkDotInput.call(this);
case 13: //enter
case 108: //enter
+ this._enterFired = true; // 标记回车触发,供 __change 区分"回车"与"失焦"
$(this).blur();
return true;
case 67: //ctrl+c
@@ -469,7 +471,9 @@
else if (_options.append === '‱') f /= 10000;
}
- _options.onchange && _options.onchange(f, this.value);
+ let isEnter = !!this._enterFired;
+ this._enterFired = false;
+ _options.onchange && _options.onchange(f, this.value, isEnter);
}
//功能禁用
@@ -543,9 +547,10 @@
this.inputctrl.setValue(this.value)
},
methods: {
- onchange(value, text) {
+ onchange(value, text, isEnter) {
this.init = false;
this.$emit('input', this.ret_type === 1 ? text : value);
+ if (isEnter) this.$emit('enter', this.ret_type === 1 ? text : value);
}
},
watch: {
|