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: {