Merge remote-tracking branch 'dest/glms/feature/1.4.2' into feature/p132_74-risk-engine
This commit is contained in:
@@ -97,6 +97,21 @@ function SearchClick(isSearchclick) {
|
||||
listGrid.trigger('reloadGrid');
|
||||
}
|
||||
|
||||
function openEodPriceAdd() {
|
||||
var html = '<div style="padding:20px;">'
|
||||
+ '<p style="margin-bottom:12px;">请选择要新增的日终价格类型:</p>'
|
||||
+ '<button class="btn btn-primary" style="margin:4px;" onclick="chooseEodAdd(\'bond\')">债券</button>'
|
||||
+ '<button class="btn btn-primary" style="margin:4px;" onclick="chooseEodAdd(\'CommodityFutures\')">商品期货</button>'
|
||||
+ '<button class="btn btn-primary" style="margin:4px;" onclick="chooseEodAdd(\'Stock\')">股票</button>'
|
||||
+ '</div>';
|
||||
layer.open({ type: 1, title: '新增日终价格', content: html, area: ['300px', '170px'] });
|
||||
}
|
||||
function chooseEodAdd(type) {
|
||||
var map = { bond: '/eodPrice/EodBondPriceEdit', CommodityFutures: '/eodPrice/EodFuturePriceEdit', Stock: '/eodPrice/EodStockPriceEdit' };
|
||||
layer.closeAll();
|
||||
main.open("新增日终价格", map[type], { area: ['820px', '620px'] });
|
||||
}
|
||||
|
||||
function uploadSettlementBill() {
|
||||
layer.open({
|
||||
type: 1,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// 日终价格编辑页公共逻辑:标的代码输入联想(服务端模糊匹配 + 原生下拉)
|
||||
// 由 eodBondPriceEdit / eodFuturePriceEdit / eodStockPriceEdit 三个页面共用,避免重复代码。
|
||||
// 下拉盒子由本文件动态创建、样式全部内联,不依赖任何 HTML 容器或外部 CSS。
|
||||
|
||||
$(function () {
|
||||
$(".datepicker").datepicker({ changeMonth: true, changeYear: true, showButtonPanel: true, showOtherMonths: true, selectOtherMonths: true });
|
||||
$(".form-group").addClass("col-md-6");
|
||||
ensureEodSuggestBox();
|
||||
});
|
||||
|
||||
function checkSubmitData() {
|
||||
var pass = $('#form1').valid();
|
||||
return pass;
|
||||
}
|
||||
|
||||
var _eodSuggestTimer = null;
|
||||
var _eodSuggestActiveInput = null;
|
||||
|
||||
function ensureEodSuggestBox() {
|
||||
if (document.getElementById('eodSuggestBox')) return;
|
||||
var box = document.createElement('div');
|
||||
box.id = 'eodSuggestBox';
|
||||
box.style.cssText = 'display:none;position:fixed;z-index:9999;background:#fff;border:1px solid #ccc;' +
|
||||
'max-height:240px;overflow:auto;box-shadow:0 2px 8px rgba(0,0,0,.15);font-size:13px;';
|
||||
document.body.appendChild(box);
|
||||
}
|
||||
|
||||
// 服务端模糊联想:输入时按需查前20条匹配(代码或名称),原生下拉,不用插件
|
||||
function eodSuggest(input, kind) {
|
||||
_eodSuggestActiveInput = input;
|
||||
clearTimeout(_eodSuggestTimer);
|
||||
var q = (input.value || '').trim();
|
||||
if (q.length < 1) { eodHideSuggest(); return; }
|
||||
_eodSuggestTimer = setTimeout(function () {
|
||||
main.post('/eodPrice/SuggestUnderlyingForEod', { q: q, kind: kind }).done(function (res) {
|
||||
if (res.success && res.obj && res.obj.length) {
|
||||
eodRenderSuggest(res.obj, input, kind);
|
||||
} else {
|
||||
eodHideSuggest();
|
||||
}
|
||||
});
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function eodRenderSuggest(list, input, kind) {
|
||||
var box = document.getElementById('eodSuggestBox');
|
||||
if (!box) return;
|
||||
box.innerHTML = '';
|
||||
list.forEach(function (item) {
|
||||
var div = document.createElement('div');
|
||||
div.style.cssText = 'padding:6px 10px;cursor:pointer;white-space:nowrap;border-bottom:1px solid #f0f0f0;';
|
||||
div.onmouseenter = function () { div.style.background = '#f2f6ff'; };
|
||||
div.onmouseleave = function () { div.style.background = '#fff'; };
|
||||
|
||||
var code = document.createElement('span');
|
||||
code.style.cssText = 'display:inline-block;min-width:90px;font-weight:600;color:#333;';
|
||||
code.textContent = item.Code;
|
||||
var name = document.createElement('span');
|
||||
name.style.cssText = 'display:inline-block;color:#888;margin-left:10px;';
|
||||
name.textContent = item.Name || '';
|
||||
|
||||
div.appendChild(code); div.appendChild(name);
|
||||
div.onmousedown = function (e) {
|
||||
e.preventDefault();
|
||||
input.value = item.Code;
|
||||
eodHideSuggest();
|
||||
// 按 kind 安全解析各页面自定义的 lookup 回调(函数名字符串,避免引用未定义函数导致 ReferenceError)
|
||||
var fnName = { bond: 'lookupBond', future: 'lookupFuture', stock: 'lookupStock' }[kind];
|
||||
if (fnName && typeof window[fnName] === 'function') window[fnName]();
|
||||
};
|
||||
box.appendChild(div);
|
||||
});
|
||||
var r = input.getBoundingClientRect();
|
||||
box.style.left = r.left + 'px';
|
||||
box.style.top = (r.bottom + 2) + 'px';
|
||||
box.style.width = Math.max(r.width, 240) + 'px';
|
||||
box.style.display = 'block';
|
||||
}
|
||||
|
||||
function eodHideSuggest() {
|
||||
var b = document.getElementById('eodSuggestBox');
|
||||
if (b) b.style.display = 'none';
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var b = document.getElementById('eodSuggestBox');
|
||||
if (b && b.style.display === 'block' && !b.contains(e.target) && e.target !== _eodSuggestActiveInput) {
|
||||
b.style.display = 'none';
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
var queryurl = '/swaptrade2/EodPositionRiskQuery';
|
||||
var cloumnTargetName = "eodSwapPositionList";
|
||||
var eodSwapExportColumnNames = [];
|
||||
$(function () {
|
||||
var PostData = {};
|
||||
$("#DateValueDate").datepicker({
|
||||
@@ -18,6 +19,11 @@ $(function () {
|
||||
$("#myTab li:eq(1)").addClass("active");
|
||||
cloumnTargetName = "eodSwapList";
|
||||
colModelGrid = colModelGridEodSwap();
|
||||
eodSwapExportColumnNames = colModelGrid.filter(function (col) {
|
||||
return !col.optionHide;
|
||||
}).map(function (col) {
|
||||
return col.name;
|
||||
});
|
||||
}
|
||||
PostData.ValueDate = $("#DateValueDate").val();
|
||||
var grid = jQuery('#listGrid').jqGrid({
|
||||
@@ -457,6 +463,7 @@ function colModelGridEodPosition() {
|
||||
}
|
||||
//框架合约table
|
||||
function colModelGridEodSwap() {
|
||||
//按需求《估值模块V1》2.2 分组定义排列列顺序,确保组内列连续(setGroupHeaders 要求)
|
||||
var colModelGrid = [{
|
||||
name: 'position.id',
|
||||
label: 'id',
|
||||
@@ -475,7 +482,9 @@ function colModelGridEodSwap() {
|
||||
align: 'center',
|
||||
hidden: true,
|
||||
optionHide: true
|
||||
}, {
|
||||
},
|
||||
//=== 基本信息 ===
|
||||
{
|
||||
name: 'position.ValueDate',
|
||||
label: '交易日',
|
||||
index: 'position.ValueDate',
|
||||
@@ -513,6 +522,22 @@ function colModelGridEodSwap() {
|
||||
width: 90,
|
||||
align: 'center',
|
||||
}, {
|
||||
name: 'SwapTradeTypeStr',
|
||||
label: '互换类型',
|
||||
index: 'SwapTradeTypeStr',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
sortable: false
|
||||
}, {
|
||||
name: 'UnderlyingType',
|
||||
label: '标的类型',
|
||||
index: 'UnderlyingType',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
sortable: false
|
||||
},
|
||||
//=== 名义本金 ===
|
||||
{
|
||||
name: 'position.NotionalValue',
|
||||
label: '合约名义本金',
|
||||
index: 'position.NotionalValue',
|
||||
@@ -533,7 +558,9 @@ function colModelGridEodSwap() {
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat
|
||||
}, {
|
||||
},
|
||||
//=== 标的市值 ===
|
||||
{
|
||||
name: 'position.MarketValueLong',
|
||||
label: '合约多头标的市值',
|
||||
index: 'position.MarketValueLong',
|
||||
@@ -547,70 +574,81 @@ function colModelGridEodSwap() {
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat
|
||||
}, {
|
||||
name: 'position.FloatingPnL',
|
||||
},
|
||||
//=== 浮动端 ===
|
||||
{
|
||||
name: 'FloatingUnrealizedPnl',
|
||||
label: '合约浮动端待实现收益',
|
||||
index: 'position.FloatingPnL',
|
||||
index: 'FloatingUnrealizedPnl',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
}, {
|
||||
name: 'PeriodAmount',
|
||||
label: '期间付息/分红',
|
||||
index: 'PeriodAmount',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
cellattr: function () {
|
||||
return ' title="合约期间内的期间付息金额(无关乎派息支付日)"';
|
||||
},
|
||||
},
|
||||
//=== 利息端 ===
|
||||
{
|
||||
name: 'position.InterestPnL',
|
||||
label: '合约利率端待实现收益',
|
||||
label: '合约利息端待实现收益',
|
||||
index: 'position.InterestPnL',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.notional,
|
||||
}, {
|
||||
name: 'position.PostionValue',
|
||||
label: '合约持仓价值',
|
||||
index: 'position.PostionValue',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat
|
||||
}, {
|
||||
name: 'position.TdRealizedPnL',
|
||||
label: '合约当日实现收益',
|
||||
index: 'position.TdRealizedPnL',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat
|
||||
}, {
|
||||
name: 'position.RealizedPnL',
|
||||
label: '合约已实现收益',
|
||||
index: 'position.RealizedPnL',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
}, {
|
||||
},
|
||||
//=== 保证金 ===
|
||||
{
|
||||
name: 'position.InitMarginGain',
|
||||
label: '收取对手方初始预付金',
|
||||
label: '收取对手方初始保证金',
|
||||
index: 'position.InitMarginGain',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
}, {
|
||||
name: 'position.PostionMarginGain',
|
||||
label: '收取对手方维持预付金',
|
||||
label: '收取对手方维持保证金',
|
||||
index: 'position.PostionMarginGain',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
}, {
|
||||
name: 'position.InitMarginLoss',
|
||||
label: '支付初始预付金',
|
||||
label: '支付初始保证金',
|
||||
index: 'position.InitMarginLoss',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
}, {
|
||||
name: 'position.PostionMarginLoss',
|
||||
label: '支付维持预付金',
|
||||
label: '支付维持保证金',
|
||||
index: 'position.PostionMarginLoss',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
}, {
|
||||
name: 'MarginInterestGain',
|
||||
label: '收取对手方保证金利息',
|
||||
index: 'MarginInterestGain',
|
||||
width: 170,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
}, {
|
||||
name: 'MarginInterestLoss',
|
||||
label: '支付对手方保证金利息',
|
||||
index: 'MarginInterestLoss',
|
||||
width: 170,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
},
|
||||
//=== 估值与实现收益 ===
|
||||
{
|
||||
name: 'position.dv01',
|
||||
label: 'DV',
|
||||
index: 'position.dv01',
|
||||
@@ -623,25 +661,67 @@ function colModelGridEodSwap() {
|
||||
return cellvalue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 });
|
||||
},
|
||||
}, {
|
||||
name: 'SwapTradeTypeStr',
|
||||
label: '互换类型',
|
||||
index: 'SwapTradeTypeStr',
|
||||
width: 100,
|
||||
name: 'InterestPaymentMethod',
|
||||
label: '付息方式',
|
||||
index: 'InterestPaymentMethod',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
sortable: false
|
||||
}, {
|
||||
name: 'MaturityNettingValuation',
|
||||
label: '合约估值(到期轧差口径)',
|
||||
index: 'MaturityNettingValuation',
|
||||
width: 190,
|
||||
align: 'center',
|
||||
formatter: NullableStockEqvNotionalFormat,
|
||||
}, {
|
||||
name: 'PeriodPaymentValuation',
|
||||
label: '合约估值(期间支付派息口径)',
|
||||
index: 'PeriodPaymentValuation',
|
||||
width: 210,
|
||||
align: 'center',
|
||||
formatter: NullableStockEqvNotionalFormat,
|
||||
}, {
|
||||
name: 'position.RealizedPnL',
|
||||
label: '合约已实现收益',
|
||||
index: 'position.RealizedPnL',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
}
|
||||
|
||||
];
|
||||
return colModelGrid;
|
||||
}
|
||||
|
||||
//框架合约分组配置(对应需求《估值模块V1》2.2 字段定义)
|
||||
//columns 使用 colModel.name;组内列在 colModel 中必须连续
|
||||
var eodSwapGroupConfig = [
|
||||
{ title: '基本信息', columns: ['position.ValueDate', 'AssetBookName', 'ClientName', 'SwapTradeNo', 'StructureType', 'SwapTradeTypeStr', 'UnderlyingType'] },
|
||||
{ title: '名义本金', columns: ['position.NotionalValue', 'position.NotionalValueLong', 'position.NotionalValueShort'] },
|
||||
{ title: '标的市值', columns: ['position.MarketValueLong', 'position.MarketValueShort'] },
|
||||
{ title: '浮动端', columns: ['FloatingUnrealizedPnl', 'PeriodAmount'] },
|
||||
{ title: '利息端', columns: ['position.InterestPnL'] },
|
||||
{ title: '保证金', columns: ['position.InitMarginGain', 'position.PostionMarginGain', 'position.InitMarginLoss', 'position.PostionMarginLoss', 'MarginInterestGain', 'MarginInterestLoss'] },
|
||||
{ title: '估值与实现收益', columns: ['position.dv01', 'InterestPaymentMethod', 'MaturityNettingValuation', 'PeriodPaymentValuation', 'position.RealizedPnL'] }
|
||||
];
|
||||
|
||||
|
||||
function gridComplete() {
|
||||
var jgrid = $(this);
|
||||
if (arguments[0].Sum) {
|
||||
jgrid.footerData("set", { 'position.dv01': arguments[0].Sum["DV"] });
|
||||
}
|
||||
main.setcolumnChooser(jgrid, page.configcolumn_data);
|
||||
//框架合约Tab:列设置应用完成后补充期间付息提示。
|
||||
if (page.tabIndex == 2) {
|
||||
var defer = main.setcolumnChooser(jgrid, cloumnTargetName);
|
||||
$.when(defer).done(function () {
|
||||
jgrid.jqGrid('setLabel', 'PeriodAmount', null, null, {
|
||||
title: '合约期间内的期间付息金额(无关乎派息支付日)'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
main.setcolumnChooser(jgrid, page.configcolumn_data);
|
||||
}
|
||||
$(window).resize();
|
||||
}
|
||||
|
||||
@@ -657,6 +737,67 @@ function starttradeView(id) {
|
||||
main.open("查看交易", srcurl);
|
||||
}
|
||||
|
||||
function exportVisibleColumns() {
|
||||
var jgrid = jQuery('#listGrid');
|
||||
var dateStr = $("#DateValueDate").val() || '';
|
||||
var tabName = page.tabIndex == 2 ? '框架合约' : '日终持仓';
|
||||
var fileName = '日终持仓风险_互换_' + tabName + (dateStr ? '_' + dateStr : '');
|
||||
if (page.tabIndex != 2) {
|
||||
main.exportVisibleColumnsToExcel(jgrid, fileName, null);
|
||||
return;
|
||||
}
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '选择导出方式',
|
||||
shadeClose: false,
|
||||
shade: 0.4,
|
||||
area: ['300px', '200px'],
|
||||
content: '<div style="padding:20px">' +
|
||||
'<p><input class="btn btn-primary js-export-eod-swap-standard" type="button" value="导出标准格式"></p>' +
|
||||
'<p><input class="btn btn-primary js-export-eod-swap-visible" type="button" value="导出界面上全部数据"></p>' +
|
||||
'</div>',
|
||||
success: function (layero, index) {
|
||||
layero.find('.js-export-eod-swap-standard').on('click', function () {
|
||||
exportEodSwapRows(jgrid, fileName, eodSwapGroupConfig, eodSwapExportColumnNames);
|
||||
layer.close(index);
|
||||
});
|
||||
layero.find('.js-export-eod-swap-visible').on('click', function () {
|
||||
exportEodSwapRows(jgrid, fileName, null, getVisibleEodSwapBusinessColumnNames(jgrid));
|
||||
layer.close(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getVisibleEodSwapBusinessColumnNames(jgrid) {
|
||||
var colModel = jgrid.jqGrid('getGridParam', 'colModel') || [];
|
||||
return colModel.filter(function (col) {
|
||||
return col.hidden !== true && !col.optionHide && col.name !== 'cb' && col.name !== 'rn';
|
||||
}).map(function (col) {
|
||||
return col.name;
|
||||
});
|
||||
}
|
||||
|
||||
function exportEodSwapRows(jgrid, fileName, groupConfig, exportColumnNames) {
|
||||
var exportPostData = $.extend({}, GetPostData(), {
|
||||
page: 1,
|
||||
rows: 0,
|
||||
sidx: jgrid.jqGrid('getGridParam', 'sortname'),
|
||||
sord: jgrid.jqGrid('getGridParam', 'sortorder')
|
||||
});
|
||||
$.ajax({
|
||||
url: queryurl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
traditional: true,
|
||||
data: exportPostData
|
||||
}).done(function (result) {
|
||||
main.exportVisibleColumnsToExcel(jgrid, fileName, groupConfig, result && result.rows ? result.rows : [], exportColumnNames);
|
||||
}).fail(function () {
|
||||
main.message && main.message('导出失败,无法获取筛选后的全部数据');
|
||||
});
|
||||
}
|
||||
//---------------------------Formatter---------------------------------
|
||||
function PriceFormat(cellValue, options, rowObject) {
|
||||
return otcformat.trading.umprice(cellValue);
|
||||
@@ -669,6 +810,12 @@ function RealizedPnlFormat(cellValue, options, rowObject) {
|
||||
function StockEqvNotionalFormat(cellValue, options, rowObject) {
|
||||
return otcformat.trading.StockEqvNotional(cellValue);
|
||||
}
|
||||
function NullableStockEqvNotionalFormat(cellValue, options, rowObject) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '';
|
||||
}
|
||||
return StockEqvNotionalFormat(cellValue, options, rowObject);
|
||||
}
|
||||
function PosiStatusFormat(cellValue, options, rowObject) {
|
||||
return cellValue == 1 ? "已平" : "正常";
|
||||
}
|
||||
@@ -767,4 +914,4 @@ function SearchClick(isSearchclick) {
|
||||
function showcolumnChooser() {
|
||||
var jgrid = jQuery('#listGrid');
|
||||
main.showcolumnChooser(jgrid, cloumnTargetName, page.configcolumn_data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,32 +2,16 @@
|
||||
$(function () {
|
||||
var PostData = {};
|
||||
|
||||
//控件选择时的触发事件
|
||||
main.setTradeDatePicker("", "#ValueDate", page.calcDate, function (selectedDate) {
|
||||
if (selectedDate) {
|
||||
$("#ValueDateFrom").datepicker("option", "maxDate", selectedDate);
|
||||
}
|
||||
});
|
||||
main.setTradeDatePicker("", "#ValueDateFrom", page.calcDate, function (selectedDate) {
|
||||
if (selectedDate) {
|
||||
$("#ValueDate").datepicker("option", "minDate", selectedDate);
|
||||
}
|
||||
});
|
||||
//手动修改时的触发事件
|
||||
$("#ValueDate").change(function () {
|
||||
$("#ValueDateFrom").datepicker("option", "maxDate", $("#ValueDate").val());
|
||||
});
|
||||
$("#ValueDateFrom").change(function () {
|
||||
$("#ValueDate").datepicker("option", "minDate", $("#ValueDateFrom").val());
|
||||
});
|
||||
main.setTradeDatePicker("", "#ValueDate", page.calcDate);
|
||||
|
||||
//默认初始值赋值逻辑
|
||||
// 互换估值页仅暴露一个估值日。当前后端按 ValueDate 单日查询,
|
||||
// 因此 ValueDateFrom 传同日仅用于保持请求对象和报告参数的日期语义一致。
|
||||
$("#ValueDate").val(page.EndTime || page.calcDate);
|
||||
$("#ValueDateFrom").val(page.StartTime && page.EndTime ? page.StartTime : '');
|
||||
PostData.StructureType = page.StructureType;
|
||||
PostData.ValueDateFrom = $("#ValueDateFrom").val();
|
||||
PostData.ValueDateFrom = $("#ValueDate").val();
|
||||
PostData.ValueDate = $("#ValueDate").val();
|
||||
PostData.ClientId = $("#ClientId").val();
|
||||
PostData.BookId = $("#BookId").val();
|
||||
|
||||
var grid = jQuery('#listGrid').jqGrid({
|
||||
url: '/swaptrade2/clientEodSwapPositionQuery',
|
||||
@@ -50,8 +34,7 @@ $(function () {
|
||||
pagerpos: 'left',
|
||||
rowNum: 100,
|
||||
rowList: [100, 1000],
|
||||
loadComplete: gridComplete,
|
||||
grouping: true
|
||||
loadComplete: gridComplete
|
||||
});
|
||||
g_grid = jQuery('#listGrid');
|
||||
|
||||
@@ -91,6 +74,8 @@ function SearchTable(isSearchclick) {
|
||||
function searchPositionDetials(isSearchclick) {
|
||||
var listGrid = $('#listGrid');
|
||||
listGrid.appendPostData({ ClientId: $("#ClientId").val() });
|
||||
//簿记账户筛选(需求3.1):未选择时传空,后端返回该对手方下全部账户的合约
|
||||
listGrid.appendPostData({ BookId: $("#BookId").val() });
|
||||
listGrid.appendPostData({ ValueDate: $("#ValueDate").val() });
|
||||
if ($("#ParentFlag").prop("checked"))
|
||||
listGrid.appendPostData({ ParentFlag: true });
|
||||
@@ -108,11 +93,7 @@ function searchPositionDetials(isSearchclick) {
|
||||
// main.message("结束日期不能大于当前系统日期!");
|
||||
// return;
|
||||
//}
|
||||
if ($("#ValueDate").val() < $("#ValueDateFrom").val()) {
|
||||
main.message("起始日期不能大于结束日期!");
|
||||
return;
|
||||
}
|
||||
listGrid.appendPostData({ ValueDateFrom: $("#ValueDateFrom").val() });
|
||||
listGrid.appendPostData({ ValueDateFrom: $("#ValueDate").val() });
|
||||
listGrid.appendPostData({ ValueDate: $("#ValueDate").val() });
|
||||
listGrid.appendPostData({ StructureType: page.StructureType });
|
||||
if (typeof (isSearchclick) != "undefined" && isSearchclick) {
|
||||
@@ -132,6 +113,7 @@ function onSortCol(index, icol, sortorder) {
|
||||
|
||||
var i = 0;
|
||||
var colModelGrid = [
|
||||
//隐藏列
|
||||
{
|
||||
name: 'position.EncryptId',
|
||||
label: 'EncryptId',
|
||||
@@ -153,14 +135,6 @@ var colModelGrid = [
|
||||
index: 'UnwindDate',
|
||||
hidden: true,
|
||||
optionHide: true
|
||||
}, {
|
||||
name: 'TradeNumber',
|
||||
label: '交易编号',
|
||||
index: 'TradeNumber',
|
||||
sortIndex: i++,
|
||||
width: 180,
|
||||
align: 'center',
|
||||
sortable: false
|
||||
}, {
|
||||
name: 'ConfrimNo',
|
||||
label: '确认书编号',
|
||||
@@ -170,11 +144,11 @@ var colModelGrid = [
|
||||
align: 'center',
|
||||
sortable: false
|
||||
}, {
|
||||
name: 'ClientName',
|
||||
label: '交易对手',
|
||||
index: 'ClientName',
|
||||
name: 'TradeNumber',
|
||||
label: '交易编号',
|
||||
index: 'TradeNumber',
|
||||
sortIndex: i++,
|
||||
width: 120,
|
||||
width: 180,
|
||||
align: 'center',
|
||||
sortable: false
|
||||
}, {
|
||||
@@ -186,6 +160,15 @@ var colModelGrid = [
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
formatter:'date',
|
||||
}, {
|
||||
name: 'MaturitySettlementDate',
|
||||
label: '到期结算日',
|
||||
index: 'MaturitySettlementDate',
|
||||
sortIndex: i++,
|
||||
width: 100,
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
formatter: 'date',
|
||||
}, {
|
||||
name: 'position.ValueDate',
|
||||
label: '估值日',
|
||||
@@ -209,22 +192,7 @@ var colModelGrid = [
|
||||
width: 120,
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
formatter: RateFormat
|
||||
}, {
|
||||
name: 'position.FloatRateUnderlyingCode',
|
||||
label: '基准利率',
|
||||
index: 'position.FloatRateUnderlyingCode',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
sortable: false
|
||||
}, {
|
||||
name: 'position.FloatRate',
|
||||
label: '当日适用基准利率',
|
||||
index: 'position.FloatRate',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
formatter: RateFormat
|
||||
formatter: SpreadRateFormat
|
||||
}, {
|
||||
name: 'position.PosiNotionalValue',
|
||||
label: '标的名义金额',
|
||||
@@ -232,7 +200,7 @@ var colModelGrid = [
|
||||
width: 150,
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
formatter: StockEqvNotionalFormat,
|
||||
formatter: AmountFormat,
|
||||
}, {
|
||||
name: 'position.PosiQuantity',
|
||||
label: '标的数量',
|
||||
@@ -247,6 +215,15 @@ var colModelGrid = [
|
||||
index: 'PeriodAmount',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
formatter: NullableAmountFormat,
|
||||
sortable: false
|
||||
}, {
|
||||
name: 'DividendAmount',
|
||||
label: '期间分红',
|
||||
index: 'DividendAmount',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
formatter: NullableAmountFormat,
|
||||
sortable: false
|
||||
}, {
|
||||
name: 'position.PosiGrossPrice',
|
||||
@@ -263,12 +240,7 @@ var colModelGrid = [
|
||||
width: 150,
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
formatter: function (cellValue, options, rowObject) {
|
||||
if (cellValue == null) {
|
||||
return "";
|
||||
}
|
||||
return otcformat.trading.premiumRateP(cellValue);
|
||||
}
|
||||
formatter: YieldRateFormat
|
||||
},
|
||||
{
|
||||
name: 'position.UnderlyingPrice',
|
||||
@@ -299,15 +271,7 @@ var colModelGrid = [
|
||||
index: 'InterestAmount',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
sortable: false,
|
||||
}, {
|
||||
name: 'position.PosiFeePending',
|
||||
label: '开仓交易费用',
|
||||
index: 'position.PosiFeePending',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
formatter: FixedAmountFormat,
|
||||
sortable: false,
|
||||
}, {
|
||||
name: 'position.PosiProfitSum',
|
||||
@@ -315,7 +279,47 @@ var colModelGrid = [
|
||||
index: 'position.PosiProfitSum',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
formatter: FixedAmountFormat,
|
||||
sortable: false,
|
||||
}, {
|
||||
name: 'position.PosiFeePending',
|
||||
label: '开平仓交易费用',
|
||||
index: 'position.PosiFeePending',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: FixedAmountFormat,
|
||||
sortable: false,
|
||||
}, {
|
||||
name: 'OpenMarginRate',
|
||||
label: '预付金利率',
|
||||
index: 'OpenMarginRate',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: TrimmedRateFormat,
|
||||
sortable: false,
|
||||
}, {
|
||||
name: 'MarginInterestAmount',
|
||||
label: '预付金利息',
|
||||
index: 'MarginInterestAmount',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: FixedAmountFormat,
|
||||
sortable: false,
|
||||
}, {
|
||||
name: 'OpenMarginAmount',
|
||||
label: '期初预付金',
|
||||
index: 'OpenMarginAmount',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: FixedAmountFormat,
|
||||
sortable: false,
|
||||
}, {
|
||||
name: 'AdditionalMarginAmount',
|
||||
label: '追加预付金',
|
||||
index: 'AdditionalMarginAmount',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: FixedAmountFormat,
|
||||
sortable: false,
|
||||
}, {
|
||||
name: 'NetSettmentAmount',
|
||||
@@ -323,10 +327,23 @@ var colModelGrid = [
|
||||
index: 'NetSettmentAmount',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: StockEqvNotionalFormat,
|
||||
formatter: FixedAmountFormat,
|
||||
sortable: false,
|
||||
}, {
|
||||
name: 'TrsValue',
|
||||
label: 'TRS估值',
|
||||
index: 'TrsValue',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: FixedAmountFormat,
|
||||
sortable: false,
|
||||
}
|
||||
];
|
||||
// 页面字段顺序以《估值模块V1》第二部分为准;历史个人列配置只能控制显隐,不能打乱业务列顺序。
|
||||
var defaultVisibleColumnNames = colModelGrid
|
||||
.filter(function (column) { return column.hidden !== true; })
|
||||
.map(function (column) { return column.name; });
|
||||
var documentColumnOrder = colModelGrid.map(function (column) { return column.name; });
|
||||
|
||||
function formatter6(cellvalue, options, rowObject) {
|
||||
return main.formatNumber(cellvalue, 6);
|
||||
@@ -356,11 +373,37 @@ function gridComplete() {
|
||||
}
|
||||
}
|
||||
//jgrid.sortGrid(g_sort.name, g_sort.order);
|
||||
main.setcolumnChooser(jgrid, page.configcolumn);
|
||||
$.when(main.setcolumnChooser(jgrid, page.configcolumn)).always(function () {
|
||||
restoreDocumentColumnOrder(jgrid);
|
||||
ensureBusinessColumnsVisible(jgrid);
|
||||
});
|
||||
$(".selftooltip").tooltip({ html: true, show: 50000, trigger: "hover" });
|
||||
$(window).off('resize.jqGrid');
|
||||
}
|
||||
|
||||
function restoreDocumentColumnOrder(jgrid) {
|
||||
var colModel = jgrid.jqGrid('getGridParam', 'colModel') || [];
|
||||
var currentNames = colModel.map(function (column) { return column.name; });
|
||||
var targetNames = currentNames
|
||||
.filter(function (name) { return documentColumnOrder.indexOf(name) < 0; })
|
||||
.concat(documentColumnOrder);
|
||||
var permutation = targetNames.map(function (name) { return currentNames.indexOf(name); });
|
||||
if (permutation.length === currentNames.length
|
||||
&& permutation.every(function (index) { return index >= 0; })
|
||||
&& permutation.some(function (index, targetIndex) { return index !== targetIndex; })) {
|
||||
jgrid.jqGrid('remapColumns', permutation, true);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBusinessColumnsVisible(jgrid) {
|
||||
var colModel = jgrid.jqGrid('getGridParam', 'colModel') || [];
|
||||
var hasVisibleBusinessColumn = colModel.some(function (column) {
|
||||
return defaultVisibleColumnNames.indexOf(column.name) >= 0 && column.hidden !== true;
|
||||
});
|
||||
if (!hasVisibleBusinessColumn) {
|
||||
jgrid.jqGrid('showCol', defaultVisibleColumnNames);
|
||||
}
|
||||
}
|
||||
|
||||
function reloadTradeMarketReport() {
|
||||
//重新加载
|
||||
@@ -400,10 +443,6 @@ function SendReport() {
|
||||
main.message("结束日期不能大于当前系统日期!");
|
||||
return;
|
||||
}
|
||||
if ($("#ValueDate").val() < $("#ValueDateFrom").val()) {
|
||||
main.message("起始日期不能大于结束日期!");
|
||||
return;
|
||||
}
|
||||
main.open("向{0}发送报告".template(clientName), "/clientbalance/TradeMarketClientSend?clientid=" + param.ClientId + "&ParentFlag=" + param.ParentFlag);
|
||||
}
|
||||
function DownLoadReport() {
|
||||
@@ -424,18 +463,16 @@ function DownLoadReport() {
|
||||
main.message("结束日期不能大于当前系统日期!");
|
||||
return;
|
||||
}
|
||||
if ($("#ValueDate").val() < $("#ValueDateFrom").val()) {
|
||||
main.message("起始日期不能大于结束日期!");
|
||||
return;
|
||||
}
|
||||
|
||||
main.post("/clientbalance/ViewTradeMarketFile", screenData()).done(function (res) {
|
||||
window.open(res.obj);
|
||||
});
|
||||
}
|
||||
function screenData() {
|
||||
var data = { From: $("#ValueDateFrom").val(), To: $("#ValueDate").val() };
|
||||
// 已移除起始日期控件,预览报告按单个估值日生成,From/To 保持同日。
|
||||
var data = { From: $("#ValueDate").val(), To: $("#ValueDate").val() };
|
||||
data.ClientId = $("#ClientId").val();
|
||||
// 报告下载和发送弹窗均从 screenData 取参数,必须保留当前簿记账户筛选。
|
||||
data.BookId = $("#BookId").val();
|
||||
if ($("#ParentFlag").prop("checked"))
|
||||
data.ParentFlag = true;
|
||||
else
|
||||
@@ -447,12 +484,35 @@ function showChiCang() {
|
||||
main.showcolumnChooser(jQuery('#listGrid'), page.configcolumn);
|
||||
}
|
||||
function PriceFormat(cellValue, options, rowObject) {
|
||||
return otcformat.trading.umprice(cellValue);
|
||||
return main.formatNumber(cellValue, 9, { grouping: true });
|
||||
}
|
||||
|
||||
function StockEqvNotionalFormat(cellValue, options, rowObject) {
|
||||
return otcformat.trading.StockEqvNotional(cellValue);
|
||||
}
|
||||
function AmountFormat(cellValue, options, rowObject) {
|
||||
return main.formatNumber(cellValue, 2, { trimTailZeros: true });
|
||||
}
|
||||
function NullableAmountFormat(cellValue, options, rowObject) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '';
|
||||
}
|
||||
return AmountFormat(cellValue, options, rowObject);
|
||||
}
|
||||
function FixedAmountFormat(cellValue, options, rowObject) {
|
||||
return main.formatNumber(cellValue, 2);
|
||||
}
|
||||
function YieldRateFormat(cellValue, options, rowObject) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '';
|
||||
}
|
||||
// percent 格式会在数字末尾添加 %,通用 trimTailZeros 无法识别其后的 0。
|
||||
// 先将小数收益率转为百分比数值,再格式化并追加 %,确保最多保留四位小数且去尾零。
|
||||
return main.formatNumber(cellValue * 100, 4, { trimTailZeros: true }) + '%';
|
||||
}
|
||||
function TrimmedRateFormat(cellValue, options, rowObject) {
|
||||
return main.formatNumber(cellValue, 4, { percent: true, trimTailZeros: true });
|
||||
}
|
||||
function RateFormat(cellValue, options, rowObject) {
|
||||
if (cellValue) {
|
||||
var num = new Number(cellValue) * 100;
|
||||
@@ -461,19 +521,23 @@ function RateFormat(cellValue, options, rowObject) {
|
||||
return "0.0000%";
|
||||
}
|
||||
}
|
||||
function SpreadRateFormat(cellValue, options, rowObject) {
|
||||
if (cellValue) {
|
||||
var num = new Number(cellValue) * 100;
|
||||
return num.toFixed(2) + "%";
|
||||
} else {
|
||||
return "0.00%";
|
||||
}
|
||||
}
|
||||
function locationChange(tab) {
|
||||
if ($("#ValueDate").val() > page.valueDate) {
|
||||
main.message("结束日期不能大于当前系统日期!");
|
||||
return;
|
||||
}
|
||||
if ($("#ValueDate").val() < $("#ValueDateFrom").val()) {
|
||||
main.message("起始日期不能大于结束日期!");
|
||||
return;
|
||||
}
|
||||
if ($("#ParentFlag").prop("checked"))
|
||||
ParentFlag = true;
|
||||
else
|
||||
ParentFlag = false;
|
||||
window.location.href = tab + "?clientId=" + $("#ClientId").val() + "&startTime=" + $("#ValueDateFrom").val() + "&endTime=" + $("#ValueDate").val() + "&ParentFlag=" + ParentFlag;
|
||||
window.location.href = tab + "?clientId=" + $("#ClientId").val() + "&startTime=" + $("#ValueDate").val() + "&endTime=" + $("#ValueDate").val() + "&ParentFlag=" + ParentFlag;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,19 @@ const inputFormatDividend = Object.freeze({ precision: 2, append: '', negative:
|
||||
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
|
||||
const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true });
|
||||
let ValueDate = model.ValueDate;
|
||||
let MaxIncomeValueDate = model.MaxIncomeValueDate ? model.MaxIncomeValueDate.substr(0, 10) : ValueDate;
|
||||
|
||||
function parseLocalDate(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
var parts = value.substr(0, 10).split('-');
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
|
||||
}
|
||||
|
||||
const vue = new Vue({
|
||||
el: '#vueDiv',
|
||||
data: {
|
||||
@@ -22,7 +35,7 @@ const vue = new Vue({
|
||||
},
|
||||
computed: {
|
||||
maxUnwindDate() {
|
||||
return ValueDate;
|
||||
return MaxIncomeValueDate;
|
||||
},
|
||||
minStartDate() {
|
||||
return this.deal.StartDate;
|
||||
@@ -33,9 +46,36 @@ const vue = new Vue({
|
||||
this.initDeal();
|
||||
this.setValueDate();
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
var $incomeValueDatePicker = $(this.$refs.incomeValueDatePicker.$el);
|
||||
$incomeValueDatePicker.datepicker("option", "maxDate", parseLocalDate(MaxIncomeValueDate));
|
||||
if (this.isAfterMaxIncomeValueDate(this.deal.ValueDate)) {
|
||||
this.validateIncomeValueDate(this.deal.ValueDate);
|
||||
} else {
|
||||
$incomeValueDatePicker.val(this.deal.ValueDate);
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
isAfterMaxIncomeValueDate(valueDate) {
|
||||
return valueDate && MaxIncomeValueDate && valueDate > MaxIncomeValueDate;
|
||||
},
|
||||
validateIncomeValueDate(valueDate) {
|
||||
if (!this.isAfterMaxIncomeValueDate(valueDate)) {
|
||||
return true;
|
||||
}
|
||||
main.message("手动互换结算日期不能晚于当前交易结束日期T-1:" + MaxIncomeValueDate);
|
||||
this.deal.ValueDate = MaxIncomeValueDate;
|
||||
this.deal.UnwindDate = MaxIncomeValueDate;
|
||||
this.floatPosition.UnwindDate = MaxIncomeValueDate;
|
||||
$(this.$refs.incomeValueDatePicker.$el).val(MaxIncomeValueDate);
|
||||
return false;
|
||||
},
|
||||
// 守卫: 价格缩放因子(债券 multiplier=100 时界面为百分比态, 计算用相对价需 ÷100)
|
||||
// 计算已外置到 swapCalc.getPriceScale; 改动需同步 swapCalc.test.js
|
||||
getPriceScale() {
|
||||
return this.multiplier == 100 ? 0.01 : 1;
|
||||
return SwapCalc.getPriceScale(this.multiplier);
|
||||
},
|
||||
initDeal() {
|
||||
var positions = model.FlowEvents.filter((item) => {
|
||||
@@ -46,7 +86,13 @@ const vue = new Vue({
|
||||
this.initPosiGrossPrice = this.floatPosition.PosiGrossPrice;
|
||||
// 互换标的价格固定为期初净价,与平仓不同不需要用户填写
|
||||
// 期初净价入库为相对价(如1.02),需转换为界面百分比形态(102),与平仓页保持一致
|
||||
this.floatPosition.TradingAmountAvg = this.initPosiGrossPrice * this.multiplier;
|
||||
// 普通打开时用期初全价作为默认值;审批打开时必须保留已提交的期末全价,不能再被期初全价覆盖。
|
||||
// 两种来源入库时都是相对价,债券统一 ×100 还原为界面百分比态。
|
||||
this.floatPosition.TradingAmountAvg = SwapCalc.resolveIncomeTradingAmountAvg(
|
||||
this.initPosiGrossPrice,
|
||||
this.floatPosition.TradingAmountAvg,
|
||||
this.multiplier,
|
||||
isUseApproval);
|
||||
this.interestList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
});
|
||||
@@ -93,6 +139,9 @@ const vue = new Vue({
|
||||
},
|
||||
setValueDate(e) {//修改平仓日期
|
||||
if (e) {
|
||||
if (!this.validateIncomeValueDate(e)) {
|
||||
e = MaxIncomeValueDate;
|
||||
}
|
||||
this.deal.ValueDate = e;
|
||||
this.deal.UnwindDate = e;
|
||||
this.floatPosition.UnwindDate = e;
|
||||
@@ -132,10 +181,14 @@ const vue = new Vue({
|
||||
let TradingFeePending = thisObj.floatPosition.TradingFeePending == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFeePending);
|
||||
let DividendIn = thisObj.floatPosition.DividendIn == "" ? 0 : parseFloat(thisObj.floatPosition.DividendIn ?? 0);
|
||||
let scale = thisObj.getPriceScale();
|
||||
//thisObj.floatPosition.MarkClosePnl = thisObj.deal.CloseNotionalValue * (thisObj.floatPosition.TradingAmountAvg * scale - thisObj.initPosiNetPrice) * floatRatio;
|
||||
thisObj.floatPosition.MarkClosePnl = thisObj.deal.CloseNotionalValue * (thisObj.floatPosition.TradingAmountAvg * scale - thisObj.initPosiGrossPrice) * floatRatio;
|
||||
// 债券全价是单位价格,价差盈亏应按持仓数量×合约乘数计算;
|
||||
// CloseNotionalValue 是期初全价折算后的名义本金,直接乘价差会重复包含期初价格。
|
||||
let positionAmount = parseFloat(thisObj.floatPosition.Quantity) * parseFloat(thisObj.floatPosition.ContractSize || 1);
|
||||
thisObj.floatPosition.MarkClosePnl = positionAmount * (thisObj.floatPosition.TradingAmountAvg * scale - thisObj.initPosiGrossPrice) * floatRatio;
|
||||
thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红
|
||||
thisObj.floatPosition.FloatPnlSum = (parseFloat(thisObj.floatPosition.MarkClosePnl) + TradingFee + TradingFeePending + DividendIn).toFixed(2);
|
||||
// 守卫: 浮动盈亏合计必须保留 2 位小数 → 对应历史 bug 3c5f25a5(原代码缺精度保留)
|
||||
// 数值由 swapCalc.calcFloatPnlSum 计算, 此处 .toFixed(2) 仅保留字符串类型以兼容下游
|
||||
thisObj.floatPosition.FloatPnlSum = SwapCalc.calcFloatPnlSum(thisObj.floatPosition.MarkClosePnl, TradingFee, TradingFeePending, DividendIn).toFixed(2);
|
||||
thisObj.calcCloseAmount();
|
||||
},
|
||||
//calcClosePnL() {//计算浮动端平仓盈亏
|
||||
@@ -212,6 +265,9 @@ const vue = new Vue({
|
||||
main.message("请输入平仓日期");
|
||||
return;
|
||||
}
|
||||
if (!thisObj.validateIncomeValueDate(thisObj.deal.ValueDate)) {
|
||||
return;
|
||||
}
|
||||
let reqObj = _.cloneDeep(thisObj.deal);
|
||||
let marginCloneList = _.cloneDeep(thisObj.marginList);
|
||||
reqObj.FlowEvents = _.cloneDeep(thisObj.interestList);
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* swapCalc.js — 互换结算/平仓纯计算函数(与 C# FrontendCalcReference 对齐)
|
||||
* ============================================================================
|
||||
* 设计要点:
|
||||
* - 无 Vue / otcformat / jQuery / lodash 依赖,全部为纯函数,便于 jest 直接 import。
|
||||
* - 浏览器:挂到 window.SwapCalc(需在 incomeSwapTrade.js / swapTradeEdit.js 之前加载)。
|
||||
* - Node: module.exports(UMD 包装),供 fe-tests/*.test.js 使用。
|
||||
* - 公式与 YLErpDAL/Helpers/FrontendCalcReference.cs 保持一致,是前后端同一份金标准。
|
||||
*
|
||||
* 生产接线状态(tested == used):incomeSwapTrade.js / swapTradeEdit.js 已调用
|
||||
* getPriceScale / deriveTradingAmountAvg / calcFloatPnlSum / calcStockEqvNotional
|
||||
* 这 4 个叶子函数(对应真实出过的 4 个 bug:20ea93d8 / dcf649f2 / 3c5f25a5 / f873239a)。
|
||||
* calcUnwind / calcIncome 仅用于 swapCalc.test.js 的前后端金标准交叉校验,未接入生产代码。
|
||||
*
|
||||
* 守卫的 bug(见 git 历史):
|
||||
* - 20ea93d8 / dcf649f2:deriveTradingAmountAvg 必须用 PosiGrossPrice(全价) 且债券 ×100
|
||||
* - 3c5f25a5:calcFloatPnlSum 必须 .toFixed(2)(保留 2 位小数)
|
||||
* - f873239a:calcStockEqvNotional 必须 round 到 2 位
|
||||
* ============================================================================
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory();
|
||||
} else {
|
||||
root.SwapCalc = factory();
|
||||
}
|
||||
})(typeof self !== 'undefined' ? self : this, function () {
|
||||
'use strict';
|
||||
|
||||
// 四舍五入(远离零),对齐 C# MidpointRounding.AwayFromZero
|
||||
function roundHalfAwayFromZero(value, digits) {
|
||||
var f = Math.pow(10, digits);
|
||||
var n = Number(value) * f;
|
||||
var sign = n < 0 ? -1 : 1;
|
||||
var r = Math.round(Math.abs(n)) * sign;
|
||||
var result = r / f;
|
||||
return result === 0 ? 0 : result; // 消除 -0
|
||||
}
|
||||
|
||||
// 价格缩放因子:债券(multiplier=100)界面为百分比态,计算用相对价需 ÷100
|
||||
function getPriceScale(multiplier) {
|
||||
return multiplier === 100 ? 0.01 : 1;
|
||||
}
|
||||
|
||||
// 期末全价(界面态) = 期初全价(相对价) × multiplier
|
||||
// 必须用 PosiGrossPrice(全价),非 PosiNetPrice(净价);债券 ×100 转界面百分比态
|
||||
function deriveTradingAmountAvg(posiGrossPrice, multiplier) {
|
||||
return posiGrossPrice * multiplier;
|
||||
}
|
||||
|
||||
// 普通打开时以期初全价作为期末价默认值;审批打开时回显已提交的期末相对价。
|
||||
function resolveIncomeTradingAmountAvg(posiGrossPrice, submittedTradingAmountAvg, multiplier, isUseApproval) {
|
||||
var relativePrice = isUseApproval && submittedTradingAmountAvg !== undefined && submittedTradingAmountAvg !== null
|
||||
? submittedTradingAmountAvg
|
||||
: posiGrossPrice;
|
||||
return relativePrice * multiplier;
|
||||
}
|
||||
|
||||
// 金额四舍五入到指定小数位(避免 0.1+0.2 类浮点误差)
|
||||
function roundMoney(value, digits) {
|
||||
return roundHalfAwayFromZero(value, digits);
|
||||
}
|
||||
|
||||
// 浮动盈亏合计 = (平仓盈亏 + 交易费用 + 待结算费用 + 分红).toFixed(2)
|
||||
function calcFloatPnlSum(markClosePnl, tradingFee, tradingFeePending, dividendIn) {
|
||||
var sum = (+markClosePnl) + (+tradingFee) + (+tradingFeePending) + (+dividendIn);
|
||||
return roundHalfAwayFromZero(sum, 2);
|
||||
}
|
||||
|
||||
// 名义本金 = 期初全价 × 因子,保留 2 位(EQD-6090)
|
||||
// factor 在前端 = 数量 × 乘数(national)
|
||||
function calcStockEqvNotional(posiGrossPrice, factor) {
|
||||
return roundHalfAwayFromZero(posiGrossPrice * factor, 2);
|
||||
}
|
||||
|
||||
// 平仓名义本金 = 平仓比例 × 剩余持仓名义本金(PosiNotionalValue)
|
||||
// 多次部分平仓后必须用剩余本金 PosiNotionalValue,不能用原始 NotionalValue,否则偏大
|
||||
function calcCloseNotionalByRemaining(closePercent, posiNotionalValue) {
|
||||
return roundHalfAwayFromZero(Number(closePercent) * Number(posiNotionalValue), 2);
|
||||
}
|
||||
|
||||
// 平仓比例 = 平仓名义本金 / 剩余持仓名义本金(PosiNotionalValue)
|
||||
// 多次部分平仓后必须以剩余本金为分母,否则比例偏小,导致后端预付金返还本金计算错误
|
||||
function calcClosePercentByRemaining(closeNotionalValue, posiNotionalValue) {
|
||||
if (Number(posiNotionalValue) === 0) return 0;
|
||||
return roundHalfAwayFromZero(Number(closeNotionalValue) / Number(posiNotionalValue), 6);
|
||||
}
|
||||
|
||||
// 平仓数量(占期初口径 A):CloseQty = PositionQty × (closePercent / oriClosePercent)
|
||||
// closePercent 是"占期初名义本金比例"(A),需先除以 oriClosePercent(=剩余/期初) 转成"占剩余比例"(B),
|
||||
// 再乘以剩余持仓数量 PositionQty。
|
||||
// 多次部分平仓后必须这样转换,否则全部↔部分切换时 ClosePercent 没变但 CloseQty 会变(不自洽)。
|
||||
// 除零保护:oriClosePercent=0(剩余为0,已全部平完)时返回 0。
|
||||
function calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, positionQty) {
|
||||
var ori = Number(oriClosePercent);
|
||||
if (ori === 0) return 0;
|
||||
return roundHalfAwayFromZero(Number(positionQty) * (Number(closePercent) / ori), 2);
|
||||
}
|
||||
|
||||
// 平仓比例(占期初口径 A)= (CloseQty / PositionQty) × oriClosePercent
|
||||
// CloseQty/PositionQty 得到"占剩余比例"(B),乘以 oriClosePercent(=剩余/期初) 转成"占期初比例"(A)。
|
||||
// 除零保护:PositionQty=0 时返回 0。
|
||||
function calcOriginalClosePercentByQty(closeQty, positionQty, oriClosePercent) {
|
||||
var qty = Number(positionQty);
|
||||
if (qty === 0) return 0;
|
||||
return roundHalfAwayFromZero((Number(closeQty) / qty) * Number(oriClosePercent), 6);
|
||||
}
|
||||
|
||||
// 盯市平仓盈亏(unwind):CloseQty × (期末全价×scale − 期初全价) × floatRatio × longRatio
|
||||
// 对齐 FrontendCalcReference.CalcUnwind:先 ×10000 取整再 ÷10000,最后 toFixed(2)
|
||||
// 干净输入下等价于直接 round(.., 2)
|
||||
function calcMarkClosePnl(closeQty, tradingAmountAvg, scale, entryPrice, floatRatio, longRatio) {
|
||||
var product = closeQty * (tradingAmountAvg * scale - entryPrice) * floatRatio * longRatio;
|
||||
var step = Math.round(product * 10000) / 10000; // 对齐 C# Math.Round(.. * 10000) / 10000
|
||||
return roundHalfAwayFromZero(step, 2);
|
||||
}
|
||||
|
||||
// ---- 组合函数:对齐 C# FrontendCalcReference.CalcUnwind / CalcIncome ----
|
||||
// 用途:作为「前端 JS 完整盈亏聚合公式」与「后端 C# 金标准」的交叉校验
|
||||
// (见 swapCalc.test.js 的 FC_001~FC_008 八个冻结场景)。
|
||||
// 注意:以下 calcUnwind / calcIncome **未接入生产代码**——生产 Vue 组件只调用上方
|
||||
// 4 个叶子函数。它们是冻结完整聚合逻辑的参考规格;若要让生产聚合逻辑也被自动守卫,
|
||||
// 需把 incomeSwapTrade.js / swapTradeEdit.js / unwindSwapTrade.js 的聚合计算也改调它们。
|
||||
|
||||
function parseOrZero(s) {
|
||||
return (s === undefined || s === null || s === '') ? 0 : Number(s);
|
||||
}
|
||||
|
||||
function sumLegs(legs) {
|
||||
return (legs || []).reduce(function (acc, l) { return acc + parseOrZero(l.interestClosePnL); }, 0);
|
||||
}
|
||||
|
||||
// 平仓页(unwind)盈亏汇总 — 对齐 FrontendCalcReference.CalcUnwind
|
||||
function calcUnwind(input) {
|
||||
var entryPrice = input.posiGrossPrice;
|
||||
var scale = input.multiplier === 100 ? 0.01 : 1;
|
||||
var floatRatio = input.payDirection === 1 ? 1 : -1;
|
||||
var longRatio = input.positionType === 1 ? 1 : -1;
|
||||
|
||||
var tradingFee = parseOrZero(input.tradingFee);
|
||||
var tradingFeePending = parseOrZero(input.tradingFeePending);
|
||||
var dividendIn = parseOrZero(input.dividendIn);
|
||||
|
||||
var markClosePnl = calcMarkClosePnl(
|
||||
input.closeQty, input.tradingAmountAvg, scale, entryPrice, floatRatio, longRatio);
|
||||
markClosePnl = roundHalfAwayFromZero(markClosePnl, 2);
|
||||
|
||||
var floatPnlSum = roundHalfAwayFromZero(markClosePnl + tradingFee + tradingFeePending + dividendIn, 2);
|
||||
|
||||
var swapRealizedPnL = floatPnlSum + sumLegs(input.interestLegs) + sumLegs(input.marginLegs);
|
||||
var swapCloseAmount = floatPnlSum + sumLegs(input.interestLegs) + sumLegs(input.marginLegs);
|
||||
var swapMarginRebatePnl = sumLegs(input.marginLegs);
|
||||
|
||||
var ratio = input.positionType === 1 ? 1 : -1;
|
||||
var tradingAmountFeeAvg = input.closeQty === 0 ? 0
|
||||
: input.tradingAmountAvg * scale + (tradingFee / input.closeQty) * ratio;
|
||||
|
||||
return {
|
||||
MarkClosePnl: roundHalfAwayFromZero(markClosePnl, 2),
|
||||
FloatPnlSum: floatPnlSum,
|
||||
SwapRealizedPnL: roundHalfAwayFromZero(swapRealizedPnL, 2),
|
||||
SwapCloseAmount: roundHalfAwayFromZero(swapCloseAmount, 2),
|
||||
SwapMarginRebatePnl: roundHalfAwayFromZero(swapMarginRebatePnl, 2),
|
||||
TradingAmountFeeAvg: tradingAmountFeeAvg
|
||||
};
|
||||
}
|
||||
|
||||
// 结息页(income)盈亏汇总 — 对齐 FrontendCalcReference.CalcIncome
|
||||
function calcIncome(input) {
|
||||
var entryPrice = input.posiGrossPrice;
|
||||
var scale = input.multiplier === 100 ? 0.01 : 1;
|
||||
var floatRatio = input.payDirection === 1 ? 1 : -1;
|
||||
|
||||
var tradingFee = parseOrZero(input.tradingFee);
|
||||
var tradingFeePending = parseOrZero(input.tradingFeePending);
|
||||
var dividendIn = parseOrZero(input.dividendIn);
|
||||
|
||||
var contractSize = input.contractSize === undefined || input.contractSize === null
|
||||
? 1 : Number(input.contractSize);
|
||||
var markClosePnl = roundHalfAwayFromZero(
|
||||
input.positionQty * contractSize * (input.tradingAmountAvg * scale - entryPrice) * floatRatio, 2);
|
||||
|
||||
var floatPnlSum = roundHalfAwayFromZero(markClosePnl + tradingFee + tradingFeePending + dividendIn, 2);
|
||||
|
||||
var swapRealizedPnL = floatPnlSum + sumLegs(input.interestLegs) + sumLegs(input.marginLegs);
|
||||
var swapCloseAmount = floatPnlSum + sumLegs(input.interestLegs) + sumLegs(input.marginLegs);
|
||||
var swapMarginRebatePnl = sumLegs(input.marginLegs);
|
||||
|
||||
var tradingAmountFeeAvg = input.closeQty > 0
|
||||
? input.tradingAmountAvg * scale + (tradingFee / input.closeQty) * floatRatio
|
||||
: input.tradingAmountAvg * scale;
|
||||
|
||||
return {
|
||||
MarkClosePnl: markClosePnl,
|
||||
FloatPnlSum: floatPnlSum,
|
||||
SwapRealizedPnL: roundHalfAwayFromZero(swapRealizedPnL, 2),
|
||||
SwapCloseAmount: roundHalfAwayFromZero(swapCloseAmount, 2),
|
||||
SwapMarginRebatePnl: roundHalfAwayFromZero(swapMarginRebatePnl, 2),
|
||||
TradingAmountFeeAvg: tradingAmountFeeAvg
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
roundHalfAwayFromZero: roundHalfAwayFromZero,
|
||||
getPriceScale: getPriceScale,
|
||||
deriveTradingAmountAvg: deriveTradingAmountAvg,
|
||||
resolveIncomeTradingAmountAvg: resolveIncomeTradingAmountAvg,
|
||||
roundMoney: roundMoney,
|
||||
calcFloatPnlSum: calcFloatPnlSum,
|
||||
calcStockEqvNotional: calcStockEqvNotional,
|
||||
calcCloseNotionalByRemaining: calcCloseNotionalByRemaining,
|
||||
calcClosePercentByRemaining: calcClosePercentByRemaining,
|
||||
calcCloseQtyByOriginalPercent: calcCloseQtyByOriginalPercent,
|
||||
calcOriginalClosePercentByQty: calcOriginalClosePercentByQty,
|
||||
calcMarkClosePnl: calcMarkClosePnl,
|
||||
calcUnwind: calcUnwind,
|
||||
calcIncome: calcIncome
|
||||
};
|
||||
});
|
||||
@@ -357,7 +357,8 @@ const vue = new Vue({
|
||||
this.getSpotPrice(payItem.UnderlyingCode, this.trade.StartDate, payItem);
|
||||
}
|
||||
var national = payItem.PosiQuantity * payItem.ContractSize;
|
||||
var stockEqvNotional = _.round(payItem.PosiGrossPrice * national, 2);//名义本金=期初价格*数量*乘数
|
||||
// 守卫: 名义本金必须 round 到 2 位 → 对应历史 bug f873239a(缺 _.round); 外置到 swapCalc.calcStockEqvNotional
|
||||
var stockEqvNotional = SwapCalc.calcStockEqvNotional(payItem.PosiGrossPrice, national);//名义本金=期初价格*数量*乘数
|
||||
this.trade.StockEqvNotional = otcformat.trading.stockEqvNotional(stockEqvNotional);
|
||||
payItem.PosiNotionalValue = this.trade.StockEqvNotional;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ const vue = new Vue({
|
||||
marginList: [],
|
||||
initPosiNetPrice: 0,
|
||||
multiplier: 1,
|
||||
oriClosePercent: model.ClosePercent,
|
||||
// 平仓比例展示/输入均为"占期初(original)"语义(A):默认与每次重开都基于原始名义本金。
|
||||
// oriClosePercent = 剩余名义本金/期初名义本金 = 最多可平比例(不能平超过剩余持仓)。
|
||||
oriClosePercent: 1,
|
||||
ratio: 1,
|
||||
shortRatio: 1,
|
||||
},
|
||||
@@ -53,6 +55,9 @@ const vue = new Vue({
|
||||
this.ratio = this.floatPosition.PayDirection == 1 ? -1 : 1;
|
||||
this.shortRatio = this.floatPosition.PositionType == 1 ? 1 : -1;
|
||||
this.TradeStartDate = model.TradeStartDate;
|
||||
// 最多可平比例(占期初口径) = 剩余名义本金 / 期初名义本金;分母为 0 时兜底为 1
|
||||
this.oriClosePercent = (this.deal.NotionalValue && this.deal.PosiNotionalValue)
|
||||
? this.deal.PosiNotionalValue / this.deal.NotionalValue : 1;
|
||||
// 转换期末标的价格为百分比形式
|
||||
if (this.floatPosition.TradingAmountAvg) {
|
||||
this.floatPosition.TradingAmountAvg = this.floatPosition.TradingAmountAvg * this.multiplier;
|
||||
@@ -126,12 +131,20 @@ const vue = new Vue({
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.PosiNotionalValue));
|
||||
this.deal.CloseQty = this.deal.PositionQty;
|
||||
} else {
|
||||
this.deal.CloseQty = otcformat.trading.notional(parseFloat(this.deal.PositionQty) * parseFloat(this.deal.ClosePercent));
|
||||
// ClosePercent 是占期初口径(A),需除以 oriClosePercent 转占剩余(B) 再乘剩余数量
|
||||
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
|
||||
}
|
||||
this.calcTradingFeePending();
|
||||
this.getInterestList();
|
||||
this.calcFloatClosePnl();
|
||||
},
|
||||
// 按"占期初口径(A)"的 ClosePercent 反算平仓数量:CloseQty = PositionQty × (ClosePercent / oriClosePercent)
|
||||
// 多次部分平仓后必须这样转换,否则全部↔部分切换时 ClosePercent 没变但 CloseQty 会变(不自洽)
|
||||
// 使用 swapCalc.calcCloseQtyByOriginalPercent 的 roundHalfAwayFromZero 避免 JS 浮点精度偏差
|
||||
// (如 32500000*(0.5/0.65)=24999999.999999996 而非 25000000)
|
||||
calcCloseQtyByPercent(closePercent) {
|
||||
return SwapCalc.calcCloseQtyByOriginalPercent(closePercent, this.oriClosePercent, this.deal.PositionQty);
|
||||
},
|
||||
calcTradingFeePending() {
|
||||
this.floatPosition.TradingFeePending = this.floatPosition.BeforeCloseFee * parseFloat(this.deal.ClosePercent);
|
||||
},
|
||||
@@ -140,12 +153,15 @@ const vue = new Vue({
|
||||
main.message("平仓数量不能超过持仓数量");
|
||||
return;
|
||||
}
|
||||
this.deal.ClosePercent = otcformat.fixed6(parseFloat(this.deal.CloseQty) / parseFloat(this.deal.PositionQty));
|
||||
// CloseQty/PositionQty 得占剩余(B),× oriClosePercent 转回占期初(A)
|
||||
var ori = parseFloat(this.oriClosePercent) || 0;
|
||||
this.deal.ClosePercent = otcformat.fixed6((parseFloat(this.deal.CloseQty) / parseFloat(this.deal.PositionQty)) * ori);
|
||||
if (parseFloat(this.deal.CloseQty) == parseFloat(this.deal.PositionQty)) {
|
||||
this.deal.CloseMethod = 1;
|
||||
} else {
|
||||
this.deal.CloseMethod = 2;
|
||||
}
|
||||
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
this.calcTradingFeePending();
|
||||
this.getInterestList();
|
||||
@@ -157,12 +173,13 @@ const vue = new Vue({
|
||||
this.deal.ClosePercent = this.oriClosePercent;
|
||||
return;
|
||||
}
|
||||
this.deal.CloseQty = otcformat.trading.notional(parseFloat(this.deal.PositionQty) * parseFloat(this.deal.ClosePercent));
|
||||
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
|
||||
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
if (parseFloat(this.deal.CloseNotionalValue) == parseFloat(this.deal.PosiNotionalValue)) {
|
||||
this.floatPosition.CloseMethod = 1;
|
||||
if (parseFloat(this.deal.ClosePercent) == parseFloat(this.oriClosePercent)) {
|
||||
this.deal.CloseMethod = 1;
|
||||
} else {
|
||||
this.floatPosition.CloseMethod = 2;
|
||||
this.deal.CloseMethod = 2;
|
||||
}
|
||||
this.calcTradingFeePending();
|
||||
this.getInterestList();
|
||||
@@ -174,8 +191,14 @@ const vue = new Vue({
|
||||
this.deal.CloseNotionalValue = this.deal.PosiNotionalValue;
|
||||
return;
|
||||
}
|
||||
// 占期初口径:平仓比例 = 平仓名义本金 / 期初名义本金(NotionalValue)
|
||||
this.deal.ClosePercent = otcformat.fixed6(parseFloat(this.deal.CloseNotionalValue) / parseFloat(this.deal.NotionalValue));
|
||||
this.deal.CloseQty = otcformat.trading.notional(parseFloat(this.deal.PositionQty) * parseFloat(this.deal.ClosePercent));
|
||||
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
|
||||
if (parseFloat(this.deal.ClosePercent) == parseFloat(this.oriClosePercent)) {
|
||||
this.deal.CloseMethod = 1;
|
||||
} else {
|
||||
this.deal.CloseMethod = 2;
|
||||
}
|
||||
this.calcTradingFeePending();
|
||||
this.getInterestList();
|
||||
this.calcFloatClosePnl();
|
||||
@@ -261,7 +284,8 @@ const vue = new Vue({
|
||||
},
|
||||
getInterestList() {//根据平仓日期获取利息腿信息
|
||||
var thisObj = this;
|
||||
var postData = { valueDate: thisObj.deal.ValueDate, unwindDate: thisObj.deal.UnwindDate, tradeId: thisObj.deal.SwapTradeId, closePercent: thisObj.deal.ClosePercent, eventType: 2 }
|
||||
// closePercent 按"占期初(original)"语义(A)传给后端,由 GetUnwindInterestList 转为"占剩余(B)"计算
|
||||
var postData = { valueDate: thisObj.deal.ValueDate, unwindDate: thisObj.deal.UnwindDate, tradeId: thisObj.deal.SwapTradeId, closePercent: thisObj.deal.ClosePercent, eventType: 2, notionalValue: thisObj.deal.NotionalValue, posiNotionalValue: thisObj.deal.PosiNotionalValue }
|
||||
main.post("/swaptrade2/GetUnwindInterestList", postData, { async: true }).done(function (resp) {
|
||||
thisObj.interestList = resp.obj.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -432,7 +432,15 @@
|
||||
function __onInput() {
|
||||
if (_ctrlV) {
|
||||
_ctrlV = false;
|
||||
return setValue(this.value);
|
||||
// 粘贴进来的是显示值(可能带 %/‱ 后缀),先按 __change 同款口径解析为模型值再回显,
|
||||
// 避免 setValue 把显示值再乘以 100/10000(#EQD-5914 债券价格粘贴 ×100)
|
||||
let f = '';
|
||||
if (this.value && this.value !== _options.append) {
|
||||
f = parseFloat(this.value.replaceAll(",", "")) || 0;
|
||||
if (_options.append === '%' || _options.percent == true) f /= 100;
|
||||
else if (_options.append === '‱') f /= 10000;
|
||||
}
|
||||
return setValue(f);
|
||||
}
|
||||
if (_chnInput >= 0) {
|
||||
__onChineseInput.call(this, _chnInput);
|
||||
|
||||
@@ -1091,4 +1091,216 @@ main.checkEmail = function (email) {
|
||||
const regex = /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/;
|
||||
return regex.test(email);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 可折叠分组表头工具(适用于 jqGrid 4.5.4,不依赖 free-jqGrid / Guriddo 商业版)
|
||||
*
|
||||
* 背景:jqGrid 4.5.4 的 setGroupHeaders 只能生成静态合并表头,不支持点击折叠。
|
||||
* 本方法在 setGroupHeaders 基础上补齐"点击分组表头折叠/展开"交互,
|
||||
* 并复用本文件 setcolumnChooser2 已验证的 destroyGroupHeader → 显隐列 → setGroupHeaders 重建模式。
|
||||
*
|
||||
* 用法:
|
||||
* main.initCollapsibleGroupHeaders($('#listGrid'), [
|
||||
* { title: '基本信息', columns: ['交易日', '簿记账户'] },
|
||||
* { title: '名义本金', columns: ['合约名义本金', '多头', '空头'] }
|
||||
* ]);
|
||||
*
|
||||
* @param {jQuery} jgrid jqGrid 容器(如 $('#listGrid'))
|
||||
* @param {Array} groupConfig 分组配置,每项 { title: string, columns: string[] }
|
||||
* - title 分组表头显示名
|
||||
* - columns 该组包含的列(使用 colModel.name)
|
||||
* 注意:columns 里若包含已被 setcolumnChooser 隐藏的列,会被自动跳过,不影响重建。
|
||||
*/
|
||||
main.initCollapsibleGroupHeaders = function (jgrid, groupConfig) {
|
||||
if (!groupConfig || !groupConfig.length) return;
|
||||
|
||||
// 存储各分组的折叠状态,key=title,value=true 表示已折叠
|
||||
var collapseState = {};
|
||||
|
||||
// 根据 groupConfig + 当前 colModel 可见性,构造 setGroupHeaders 所需的 groupHeaders 参数。
|
||||
// 关键:折叠的分组仍需保留一个表头单元格作为"展开锚点",否则用户无法再次展开。
|
||||
function buildGroupHeaders() {
|
||||
var colModel = jgrid.jqGrid('getGridParam', 'colModel');
|
||||
var visibleNames = colModel.filter(function (c) { return !c.hidden; }).map(function (c) { return c.name; });
|
||||
var groupHeaders = [];
|
||||
for (var g = 0; g < groupConfig.length; g++) {
|
||||
var group = groupConfig[g];
|
||||
var visibleColsInGroup = group.columns.filter(function (n) { return visibleNames.indexOf(n) > -1; });
|
||||
//整组都不可见:若是被折叠的分组,仍需保留锚点(下方 applyGroupHeaders 已保留首列);
|
||||
//若是被 setcolumnChooser 主动隐藏的分组,则跳过不渲染表头
|
||||
if (!visibleColsInGroup.length && !collapseState[group.title]) continue;
|
||||
//锚点列:折叠态下 numberOfColumns=1(首列被保留);展开态为该组可见列数
|
||||
var count = visibleColsInGroup.length || 1;
|
||||
var icon = collapseState[group.title] ? '▶' : '▼';
|
||||
var titleClass = collapseState[group.title] ? 'group-header-title group-collapsed' : 'group-header-title';
|
||||
groupHeaders.push({
|
||||
startColumnName: group.columns[0], //锚点列始终是首列(折叠时首列被保留)
|
||||
numberOfColumns: count,
|
||||
titleText: '<span class="' + titleClass + '" data-group="' + g + '">' + icon + ' ' + group.title + '</span>'
|
||||
});
|
||||
}
|
||||
return groupHeaders;
|
||||
}
|
||||
|
||||
// 应用一次分组表头(含折叠态显隐),复用 destroyGroupHeader → setGroupHeaders 模式
|
||||
function applyGroupHeaders() {
|
||||
jgrid.jqGrid('destroyGroupHeader', true); // true: 不恢复为单行表头
|
||||
// 按折叠状态显隐列:折叠的分组保留首列作为锚点(避免整组表头消失无法展开)
|
||||
for (var g = 0; g < groupConfig.length; g++) {
|
||||
var group = groupConfig[g];
|
||||
if (collapseState[group.title]) {
|
||||
//折叠:隐藏除首列外的所有列,保留首列作为锚点
|
||||
if (group.columns.length > 1) {
|
||||
jgrid.setGridParam().hideCol(group.columns.slice(1));
|
||||
}
|
||||
jgrid.setGridParam().showCol([group.columns[0]]);
|
||||
} else {
|
||||
//展开:恢复该组所有列
|
||||
jgrid.setGridParam().showCol(group.columns);
|
||||
}
|
||||
}
|
||||
var groupHeaders = buildGroupHeaders();
|
||||
if (groupHeaders.length) {
|
||||
jgrid.jqGrid('setGroupHeaders', { useColSpanStyle: true, groupHeaders: groupHeaders });
|
||||
}
|
||||
//保存到 jqGrid 参数,便于与 setcolumnChooser 协调
|
||||
jgrid.jqGrid('setGridParam', { collapsibleGroupConfig: groupConfig, collapseState: collapseState });
|
||||
bindHeaderClick();
|
||||
}
|
||||
|
||||
// 给分组表头单元格绑定 click(事件委托,重建表头后仍生效)
|
||||
function bindHeaderClick() {
|
||||
var hbox = jgrid.closest('.ui-jqgrid').find('.ui-jqgrid-hdiv');
|
||||
hbox.off('click.collapsibleGroup').on('click.collapsibleGroup', '.group-header-title', function (e) {
|
||||
e.stopPropagation();
|
||||
var idx = $(this).attr('data-group');
|
||||
var group = groupConfig[idx];
|
||||
if (!group) return;
|
||||
collapseState[group.title] = !collapseState[group.title];
|
||||
applyGroupHeaders();
|
||||
});
|
||||
}
|
||||
|
||||
applyGroupHeaders();
|
||||
};
|
||||
|
||||
/**
|
||||
* 重建已初始化的可折叠分组表头。
|
||||
* 供 setcolumnChooser / 列重排后调用,确保分组边界与最新的列顺序/可见性同步。
|
||||
* @param {jQuery} jgrid
|
||||
*/
|
||||
main.refreshCollapsibleGroupHeaders = function (jgrid) {
|
||||
var cfg = jgrid.jqGrid('getGridParam', 'collapsibleGroupConfig');
|
||||
if (!cfg) return;
|
||||
// 复用已有的 collapseState
|
||||
var state = jgrid.jqGrid('getGridParam', 'collapseState') || {};
|
||||
// 重新初始化(groupConfig 同引用,collapseState 重建以剔除已失效的分组)
|
||||
main.initCollapsibleGroupHeaders(jgrid, cfg);
|
||||
// 回填状态
|
||||
var freshState = jgrid.jqGrid('getGridParam', 'collapseState') || {};
|
||||
Object.keys(state).forEach(function (k) { if (k in freshState) freshState[k] = state[k]; });
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出 jqGrid 可见列为 Excel(零依赖,基于 HTML table + ms-excel MIME)。
|
||||
*
|
||||
* 设计目标:导出内容与前端表格"当前可见列"完全一致(需求《估值模块V1》4.2)。
|
||||
* 不依赖第三方库,不调用后端导出接口,避免与 C# 模板导出口径混淆。
|
||||
*
|
||||
* @param {jQuery} jgrid jqGrid 容器
|
||||
* @param {string} fileName 导出文件名(不含扩展名)
|
||||
* @param {Array} groupConfig 可选,分组表头配置,每项 { title: string, columns: string[] }
|
||||
* @param {Array} exportRows 可选,后端返回的全部筛选结果;未传时导出当前页
|
||||
* @param {Array} exportColumnNames 可选,指定导出的列名及顺序;未传时导出当前可见列
|
||||
*/
|
||||
main.exportVisibleColumnsToExcel = function (jgrid, fileName, groupConfig, exportRows, exportColumnNames) {
|
||||
var colModel = jgrid.jqGrid('getGridParam', 'colModel');
|
||||
var exportCols;
|
||||
if (Array.isArray(exportColumnNames)) {
|
||||
exportCols = exportColumnNames.map(function (name) {
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
if (colModel[i].name === name) return colModel[i];
|
||||
}
|
||||
}).filter(function (col) { return col && col.name !== 'cb' && col.name !== 'rn'; });
|
||||
} else {
|
||||
// 只导出可见列(hidden !== true),与折叠状态联动:收起的列自动不可见
|
||||
exportCols = colModel.filter(function (c) { return c.hidden !== true && c.name !== 'cb' && c.name !== 'rn'; });
|
||||
}
|
||||
if (!exportCols.length) { main.message && main.message("没有可导出的列"); return; }
|
||||
|
||||
var rows = exportRows || jgrid.jqGrid('getRowData');
|
||||
if (exportRows) {
|
||||
var gridElement = jgrid[0];
|
||||
rows = exportRows.map(function (row, rowIndex) {
|
||||
var formattedRow = {};
|
||||
exportCols.forEach(function (col) {
|
||||
var colIndex = colModel.indexOf(col);
|
||||
var rawValue = $.jgrid.getAccessor(row, col.name);
|
||||
var formattedValue = gridElement && gridElement.formatter
|
||||
? gridElement.formatter(rowIndex + 1, rawValue, colIndex, row, 'add')
|
||||
: rawValue;
|
||||
formattedRow[col.name] = $('<div>').html(formattedValue == null ? '' : String(formattedValue)).text().replace(/\u00a0/g, '');
|
||||
});
|
||||
return formattedRow;
|
||||
});
|
||||
}
|
||||
var headerLabels = exportCols.map(function (c) { return c.label || c.name; });
|
||||
|
||||
// 构建 HTML table,用 style 保持 mso-number-format 让金额不被科学计数法破坏
|
||||
var html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">';
|
||||
html += '<head><meta charset="UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>Sheet1</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head>';
|
||||
html += '<body><table border="1">';
|
||||
//表头:有分组配置时导出两级表头(分类 + 字段),否则保持原单级表头。
|
||||
if (groupConfig && groupConfig.length) {
|
||||
html += buildGroupHeaderHtml(exportCols, groupConfig);
|
||||
}
|
||||
html += '<tr>' + headerLabels.map(function (l) {
|
||||
return '<th style="background:#f0f0f0;font-weight:bold;">' + escapeXml(l) + '</th>';
|
||||
}).join('') + '</tr>';
|
||||
//数据行
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
html += '<tr>';
|
||||
for (var c = 0; c < exportCols.length; c++) {
|
||||
var val = rows[i][exportCols[c].name];
|
||||
if (val === undefined || val === null) val = '';
|
||||
html += '<td>' + escapeXml(String(val)) + '</td>';
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</table></body></html>';
|
||||
|
||||
var blob = new Blob(['\ufeff' + html], { type: 'application/vnd.ms-excel;charset=utf-8' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = (fileName || 'export') + '.xls';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
|
||||
|
||||
function escapeXml(s) {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function buildGroupHeaderHtml(cols, config) {
|
||||
var titleByColumn = {};
|
||||
config.forEach(function (group) {
|
||||
(group.columns || []).forEach(function (name) {
|
||||
titleByColumn[name] = group.title || '';
|
||||
});
|
||||
});
|
||||
|
||||
var cells = [];
|
||||
for (var i = 0; i < cols.length; i++) {
|
||||
var title = titleByColumn[cols[i].name] || '';
|
||||
var colspan = 1;
|
||||
while (i + colspan < cols.length && (titleByColumn[cols[i + colspan].name] || '') === title) {
|
||||
colspan++;
|
||||
}
|
||||
cells.push('<th colspan="' + colspan + '" style="background:#d9edf7;font-weight:bold;text-align:center;">' + escapeXml(title) + '</th>');
|
||||
i += colspan - 1;
|
||||
}
|
||||
return '<tr>' + cells.join('') + '</tr>';
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user