feat(EQD-6838): 债券三字段交互约定2/3 + 簿记逻辑1 实现

问题:
- 簿记基础逻辑1未满足:选标的后 setUnderlyingCode 调 getSpotPrice 自动填充净价/全价
- 交互约定3未满足:vue-number-input 的 input 事件只在失焦/回车时触发(__change),
  按键输入时不触发,导致用户输入过程中 源/AUTO 标识不会消失
- 交互约定2补充未满足:calcBondForItem 的早期返回分支(源字段为空/开始日缺失)
  未调 applyBondCalcFailure,导致回车后另两字段和标识未清空

修复:
- 簿记逻辑1:setUnderlyingCode 切换标的时清空净价/全价/收益率三字段,
  不再调 getSpotPrice 自动回填;refreshInitialUnderlyingPrices 续发场景也不自动填充
- 交互约定3:cshtml 三个 vue-number-input 加 v-on:keydown.native 绑定,
  新增 onBondPriceKeydown 方法——按键即清 源/AUTO 标识、标本字段 REV;
  回车键(13/108)和修饰键(Ctrl/Alt/Tab/方向键等)除外
- 交互约定2补充:calcBondForItem 的源字段为空/开始日缺失两个早期返回分支
  也调 applyBondCalcFailure,确保回车后另两字段清空、三标识全清
This commit is contained in:
hjhan
2026-07-29 19:09:29 +08:00
parent fb00f13421
commit d662399ab4
2 changed files with 37 additions and 15 deletions
+3 -3
View File
@@ -474,13 +474,13 @@
</a>
</td>
<td v-if="trade.StructureType!='普通收益互换'">
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="inputFormatSwapBondDeliveryPrice" v-on:input="onDpPriceInput(item)" v-on:enter="onBondPriceEnter(item,'DP')"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='DP'">源</span><span style="color:#1565c0;font-size:11px;margin-left:4px;" v-else-if="item.isBond && item.bondAuto && item.bondAuto.DP">AUTO</span><span style="color:#ef6c00;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondRev && item.bondRev.DP" title="人工输入:未回车触发计算器">REV</span>
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="inputFormatSwapBondDeliveryPrice" v-on:input="onDpPriceInput(item)" v-on:enter="onBondPriceEnter(item,'DP')" v-on:keydown.native="onBondPriceKeydown(item,'DP',$event)"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='DP'">源</span><span style="color:#1565c0;font-size:11px;margin-left:4px;" v-else-if="item.isBond && item.bondAuto && item.bondAuto.DP">AUTO</span><span style="color:#ef6c00;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondRev && item.bondRev.DP" title="人工输入:未回车触发计算器">REV</span>
</td>
<td v-if="trade.StructureType!='普通收益互换'">
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiNetNoFeePrice')" v-model="item.PosiNetNoFeePrice" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceEdit(item,'CP')" v-on:enter="onBondPriceEnter(item,'CP')"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='CP'">源</span><span style="color:#1565c0;font-size:11px;margin-left:4px;" v-else-if="item.isBond && item.bondAuto && item.bondAuto.CP">AUTO</span><span style="color:#ef6c00;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondRev && item.bondRev.CP" title="人工输入:未回车触发计算器">REV</span>
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiNetNoFeePrice')" v-model="item.PosiNetNoFeePrice" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceEdit(item,'CP')" v-on:enter="onBondPriceEnter(item,'CP')" v-on:keydown.native="onBondPriceKeydown(item,'CP',$event)"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='CP'">源</span><span style="color:#1565c0;font-size:11px;margin-left:4px;" v-else-if="item.isBond && item.bondAuto && item.bondAuto.CP">AUTO</span><span style="color:#ef6c00;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondRev && item.bondRev.CP" title="人工输入:未回车触发计算器">REV</span>
</td>
<td v-if="trade.StructureType!='普通收益互换'">
<vue-number-input :key="getPosiPriceFormatKey(item,'InitYtm')" v-model="item.InitYtm" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceEdit(item,'YD')" v-on:enter="onBondPriceEnter(item,'YD')"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='YD'">源</span><span style="color:#1565c0;font-size:11px;margin-left:4px;" v-else-if="item.isBond && item.bondAuto && item.bondAuto.YD">AUTO</span><span style="color:#ef6c00;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondRev && item.bondRev.YD" title="人工输入:未回车触发计算器">REV</span><a href="javascript:void(0);" v-on:click="resetBondCalc(item)" v-if="item.isBond" style="margin-left:6px;font-size:11px;color:#1565c0;">重算</a>
<vue-number-input :key="getPosiPriceFormatKey(item,'InitYtm')" v-model="item.InitYtm" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceEdit(item,'YD')" v-on:enter="onBondPriceEnter(item,'YD')" v-on:keydown.native="onBondPriceKeydown(item,'YD',$event)"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='YD'">源</span><span style="color:#1565c0;font-size:11px;margin-left:4px;" v-else-if="item.isBond && item.bondAuto && item.bondAuto.YD">AUTO</span><span style="color:#ef6c00;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondRev && item.bondRev.YD" title="人工输入:未回车触发计算器">REV</span><a href="javascript:void(0);" v-on:click="resetBondCalc(item)" v-if="item.isBond" style="margin-left:6px;font-size:11px;color:#1565c0;">重算</a>
</td>
<td v-if="trade.StructureType=='普通收益互换'">
<vue-number-input :key="getPosiPriceFormatKey(item,'normalPosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="inputFormatSwapDeliveryPrice" v-on:input="changeSpotPrice(item)"></vue-number-input>
@@ -349,6 +349,23 @@ const vue = new Vue({
if (!item.isBond) return;
SwapCalc.applyBondManualEdit(item, type);
},
//交互约定3(按键实时触发):vue-number-input 的 input 事件只在失焦/回车时触发,
// 按键输入过程中不触发,导致用户输入时 源/AUTO 标识不会消失。
// 此方法绑在 v-on:keydown.native 上,按键即清 源/AUTO 标识、标本字段 REV。
// 回车键(13/108)除外——回车由 v-on:enter 驱动 calcBondForItem,会把标识修正为 源/AUTO。
// 修饰键(Shift/Ctrl/Alt/Tab/方向键/F键等)除外——这些不改变数值,不应清标识。
onBondPriceKeydown(item, type, event) {
if (!item.isBond) return;
var kc = event.which || event.keyCode;
// 回车由 v-on:enter 处理,不在此清标识
if (kc === 13 || kc === 108) return;
// 修饰键/导航键/功能键不改变数值,不清标识
if (event.ctrlKey || event.altKey || event.metaKey) return;
if (kc === 9 || kc === 35 || kc === 36 || kc === 37 || kc === 39 || kc === 116) return;
// Backspace/Delete 改变数值,需清标识
// 数字键/小键盘/小数点/减号 改变数值,需清标识
SwapCalc.applyBondManualEdit(item, type);
},
//全价列:普通标的重算名义本金;债券标的按交互约定3 标 REV(不联动)。
// 回车计算由独立的 v-on:enter="onBondPriceEnter(item,'DP')" 负责。
onDpPriceInput(item) {
@@ -381,7 +398,11 @@ const vue = new Vue({
var price = driver === 'CP' ? item.PosiNetNoFeePrice
: driver === 'DP' ? item.PosiGrossPrice
: item.InitYtm;
if (price === null || price === '' || isNaN(price)) return; // 编辑中(空/非数)静默,不提示
if (price === null || price === '' || isNaN(price)) {
// 交互约定2补充:源字段为空/非数也算"算不出来",清另两字段+所有标识
SwapCalc.applyBondCalcFailure(item, driver);
return;
}
var priceType = driver === 'CP' ? 'CP' : driver === 'DP' ? 'DP' : 'YD';
var self = this;
// 估值日:债券互换的"期初"净价/全价/收益率应以【互换起始日 StartDate】估值。
@@ -392,6 +413,8 @@ const vue = new Vue({
if (sdMsg) {
// D3 同款去重:开始日缺失时用户在价格格逐键输入会连刷相同提示,故同一文案只弹一次
if (SwapCalc.shouldShowBondErr(item, sdMsg)) main.message(sdMsg);
// 交互约定2补充:开始日缺失也算"算不出来",清另两字段+所有标识
SwapCalc.applyBondCalcFailure(item, driver);
return; // 估值日缺失:不调用计算器、不覆盖任何手工输入
}
main.post('/Bond/CalcBond', {
@@ -766,9 +789,11 @@ const vue = new Vue({
if (!data || !data.Code) return;
var underlyingCode = item.UnderlyingCode;
if (underlyingCode != data.Code) {
// 切换标的:清空上一债券残留的收益率,避免旧债券计算结果污染新债券
// 注:clearBondCalcFlags 只清手动/源标志,未清字段值;
// 净价/全价会被末尾 getSpotPrice 无条件回填,收益率(InitYtm)无刷新逻辑,必须显式清空
// 簿记基础逻辑1:新增互换交易选择标的后,不自动填充价格等数值
// 切换标的时清空净价/全价/收益率三字段,由用户手填或回车调计算器反算。
// 历史上此处调 getSpotPrice 自动回填,会导致用户未确认的价格被静默填入
item.PosiNetNoFeePrice = null;
item.PosiGrossPrice = null;
item.InitYtm = null;
}
item.CountRatio = data.CountRatio;
@@ -789,8 +814,8 @@ const vue = new Vue({
this.$set(item, 'isBond', tradeHelper.IsBond(data.InstrumentType));
// 切换标的时清空债券三字段互算的手动/源标志(D1 修复:避免旧债券手填状态污染新债券)
SwapCalc.clearBondCalcFlags(item);
// Refresh the initial price even when the user re-selects the same underlying.
this.getSpotPrice(item.UnderlyingCode, this.trade.TradeDate, item);
// 簿记基础逻辑1:不再自动调 getSpotPrice 回填价格,由用户手填或回车调计算器
// 续发(renewal)场景的初始价恢复由 refreshInitialUnderlyingPrices 独立处理
//this.initMarginRate();
},
setFloatRateUnderlyingCode(data, item) {
@@ -1556,14 +1581,11 @@ const vue = new Vue({
},
// A renewal opens as a new trade, so the underlying autocomplete only restores
// its code and does not emit change. Fetch the initial price explicitly.
// its code and does not emit change. Historically fetched the initial price explicitly.
// 簿记基础逻辑1:续发场景也不自动填充价格,由用户手填或回车调计算器反算。
refreshInitialUnderlyingPrices() {
if (!page.isAdd) return;
this.paySwapList.forEach(item => {
if (item.UnderlyingCode) {
this.getSpotPrice(item.UnderlyingCode, this.trade.TradeDate, item);
}
});
// 不再自动调 getSpotPrice 回填价格;用户可手动回车调计算器或手填
},
//利息端添加行
addGetSwapRate() {