Files
zszq-trs/YLErpWeb/wwwroot/Scripts/app/marginTemplate/marginTemplateV2DefaultEdit.js
T
hjhan 6d0986f2aa fix: 预付金模板多选 chosen 有选中项即隐藏搜索框,空态恢复占位
- 上一版仅全选时隐藏,仍有空框在剩余空间不足时换行占一行(簿记账户/资产类型单元格);
  改为有任意选中项即隐藏(点选场景无需键盘过滤),全部取消后恢复占位提示
- 去掉搜索框 max-width 限宽:空态时全宽展示"请选择…"
- 期限档位表操作列按钮去左边距+不换行,fixed 布局窄列下不再竖排
2026-08-28 09:25:57 +08:00

654 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//定价格式化(来自配置)
const inputFormatPercent = Object.freeze({ precision: 2, append: '%' });
const inputFormatPercentNegative = Object.freeze({ precision: 2, append: '%', negative: true });
const inputFormatNegative = Object.freeze({ precision: 2, negative: true });
function normalizeBookIds(values) {
var ids = Array.isArray(values) ? values : (values ? [values] : []);
var normalized = [];
ids.forEach(function (id) {
id = (id || '').toString().trim();
if (id && normalized.indexOf(id) < 0) {
normalized.push(id);
}
});
return normalized.length ? normalized.join(',') : null;
}
const MarginRuleTypeEnum = {
无预付金: "0",
按固定利率: "1",
按浮动盈亏: "2",
类香草预付金规则: "3",
自动赎回买入预付金规则: "4",
触发条件预付金规则1: "5",
触发条件预付金规则2: "6",
保底预付金规则: "7",
含赔付预付金计算规则: "8",
触发条件预付金规则3: "9",
交易所基本规则: "10",
标准SPAN: "11",
买入自动赎回规则1: "12",
卖出自动赎回规则1: "13",
区间追保结构: "15",
默认预付金规则: "99"
};
const UnderlyingSeperateTypeEnum = {
None: "0",//不区分
CustomStock: "1",//按自定义权益分类
CustomInstrumentType: "2"//按资产类型分类
};
//允许配置期限档(SpanConfig.BondTerm)的标的资产类型标志位掩码,
//与后端 ConsMarginTerm.TermTierEnabledUnderlyingTypes 保持一致(本期仅 利率债 TBonds=1<<4=16
const TermTierEnabledUnderlyingMask = 16;
//基金及基金专户标志位(ETF 子类行的 UnderlyingType,不加新枚举位)
const FundTypeMask = 32768;
//利率债区块固定展开的4个期限档([BondTerm值, 展示文案]),与 docx 原型 4.3.1 一致
const SpanBondTerms = [['<5y', '≤5y'], ['5y-10y', '(5y-10y]'], ['10y-30y', '(10y-30y]'], ['>30y', '>30y']];
//区块键自增序号(仅前端分组用,不持久化)
var spanBlockSeq = 0;
//标的资产类型标志位 -> 名称(与明细表格 chosen 选项一致,用于规则15录入区块标题)
const UnderlyingTypeNames = [
[16, "利率债"], [32, "信用债"], [64, "其它债券"], [128, "股票"], [256, "股指"], [512, "股指期货"],
[1024, "商品期货"], [2048, "商品现货"], [4096, "新三板挂牌股票"], [8192, "香港股票"], [16384, "香港股指"],
[32768, "基金及基金专户"], [65536, "黄金期货"], [131072, "国债期货"], [262144, "其他期货"], [524288, "黄金现货"],
[1048576, "其他现货"], [2097152, "境外期货"], [4194304, "境外现货"], [8388608, "境外股票"], [16777216, "境外股指"],
[33554432, "汇率"], [67108864, "Shibor"], [134217728, "银行间回购定盘"], [268435456, "利率收益率"], [536870912, "债券指数"]
];
const vue = new Vue({
el: '#marginTemplateV2Form',
data: {
isAdd: page.isAdd,
marginTemplate: page.marginTemplate,
etfSubtypeItems: page.etfSubtypeItems || []
},
created: function () {
//留档进入页面时的生效状态:v-model 改写的就是 page.marginTemplate 同一对象(data 浅拷贝的是引用),
//原始值须在用户交互前记下,供保存时判断"生效中规则被改为否"弹失效确认;
//select 的 v-model 取值是字符串,统一 String 化比较
this.originalIsValid = String(this.marginTemplate.IsValid);
//区间追保结构:加载时按 资产类型+期限 排序一次,再重建资产类型区块(利率债区块补齐固定4档),
//编辑过程中不再重排,避免 chosen/选择器失序
if (this.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构 && this.marginTemplate.Details) {
var termOrder = ['', '<5y', '5y-10y', '10y-30y', '>30y'];
var that = this;
this.marginTemplate.Details.forEach(function (d) {
//兜底:历史数据可能没有 SpanConfig / 区间录入结构,补齐避免录入区块不渲染
that.ensureSpanDetail(d);
});
this.marginTemplate.Details.sort(function (a, b) {
var at = a.UnderlyingType || 0, bt = b.UnderlyingType || 0;
if (at !== bt) return at - bt;
var ai = termOrder.indexOf((a.SpanConfig && a.SpanConfig.BondTerm) || '');
var bi = termOrder.indexOf((b.SpanConfig && b.SpanConfig.BondTerm) || '');
return (ai < 0 ? termOrder.length : ai) - (bi < 0 ? termOrder.length : bi);
});
this.marginTemplate.Details = this.rebuildSpanBlocks(this.marginTemplate.Details);
}
this.changeRuleType(true);
},
computed: {
//区间追保结构:参数组按资产类型区块展示,相邻且区块键(_bk)相同的行合并(rowspan)。
//_bk 由 加载重建/新增/切换资产类型 时维护:利率债区块含固定4档行,其他区块单行。
ruleRangeDetailRows() {
var details = this.marginTemplate.Details || [];
var keyOf = function (d, i) {
return d._bk || ('idx_' + i);
};
var rows = [];
for (var i = 0; i < details.length; i++) {
var key = keyOf(details[i], i);
if (i > 0 && keyOf(details[i - 1], i - 1) === key) {
rows.push({ detail: details[i], index: i, rowspan: 0 });
continue;
}
var span = 1;
for (var j = i + 1; j < details.length; j++) {
if (keyOf(details[j], j) !== key) break;
span++;
}
rows.push({ detail: details[i], index: i, rowspan: span });
}
return rows;
},
//区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)区块固定展开4个期限档段(每段一条 detail),
//其他区块(含全部 ETF 子类,2026-08-27 裁定 ETF 无期限概念不再分档)单段(BondTerm 为空)。
//段的 detail 缺失时模板侧 v-if 兜底不渲染该段
ruleRangeBlocks() {
if (this.marginTemplate.RuleType != MarginRuleTypeEnum.区间追保结构) return [];
var blocks = [];
var rows = this.ruleRangeDetailRows;
var details = this.marginTemplate.Details || [];
for (var i = 0; i < rows.length; i++) {
if (rows[i].rowspan <= 0) continue;
var head = rows[i];
var ut = head.detail.UnderlyingType || 0;
var kind = (head.detail.SpanConfig && head.detail.SpanConfig.EtfKind) || '';
var isTBond = ut === 16;
var sections = [];
if (isTBond) {
for (var t = 0; t < SpanBondTerms.length; t++) {
var found = null;
for (var j = head.index; j < head.index + head.rowspan; j++) {
var bt = (details[j].SpanConfig && details[j].SpanConfig.BondTerm) || '';
if (bt === SpanBondTerms[t][0]) { found = details[j]; break; }
}
sections.push({ key: SpanBondTerms[t][0], termLabel: SpanBondTerms[t][1], detail: found });
}
} else {
sections.push({ key: 'single', termLabel: '', detail: head.detail });
}
blocks.push({ key: head.detail._bk || ('i' + head.index), no: blocks.length + 1, headIndex: head.index, ut: ut, etfKind: kind, isTBond: isTBond, isFundBlock: ut === FundTypeMask, sections: sections });
}
return blocks;
}
},
methods: {
changeRuleType(isInitial) {
switch (this.marginTemplate.RuleType.toString()) {
case MarginRuleTypeEnum.无预付金:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "预付金占用为零。";
break;
case MarginRuleTypeEnum.按固定利率:
this.marginTemplate.Comments = "预付金占用为成交/实时名义本金的固定比例。";
break;
case MarginRuleTypeEnum.按浮动盈亏:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "预付金占用为成交/实时名义本金的固定比例,加上对手方交易浮动亏损,扣除对手方交易浮动盈利。";
break;
case MarginRuleTypeEnum.类香草预付金规则:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "预付金占用为为成交/实时名义本金的浮动比例,加上对手方交易浮动亏损。浮动比例依据标的涨跌停情况判断。";
break;
case MarginRuleTypeEnum.自动赎回买入预付金规则:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "预付金占用为n+2(≤N)个票息之和,N为合约期内敲出观察日总数,n为当前经过的敲出观察日个数(当n+2>N时,n = N - 2)。";
break;
case MarginRuleTypeEnum.触发条件预付金规则1:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "预付金占用为成交名义本金的固定比例,加标的跌幅的市值(跌幅达到一定条件触发)";
break;
case MarginRuleTypeEnum.触发条件预付金规则2:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "预付金占用为成交/实时名义本金的固定比例,加标的跌幅的市值(跌幅达到一定条件触发)";
break;
case MarginRuleTypeEnum.触发条件预付金规则3:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "预付金占用为成交/实时名义本金的固定比例,加上对手方分段式交易浮动亏损(涨跌幅达到一定条件触发)";
break;
case MarginRuleTypeEnum.保底预付金规则:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "预付金占用为对手方最大亏损金额。";
break;
case MarginRuleTypeEnum.含赔付预付金计算规则:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "预付金占用为对手方最大赔付金额。";
break;
case MarginRuleTypeEnum.交易所基本规则:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "交易所基本规则";
break;
case MarginRuleTypeEnum.标准SPAN:
this.marginTemplate.Comments = "计算标的价格和波动率上涨下跌4个场景下的对手方最大亏损";
break;
case MarginRuleTypeEnum.买入自动赎回规则1:
this.marginTemplate.UnderlyingSeperateType = UnderlyingSeperateTypeEnum.None;
this.marginTemplate.Comments = "Minx*首次票息金额+y*Max(0,Pv),合约期间全部票息金额);\n若勾选了“取与保底金额孰小”,则还要与取较小的一个;\n保底金额 =ABS(执行价格 - 保底价格)* ABS(交易数量)。";
break;
case MarginRuleTypeEnum.卖出自动赎回规则1:
this.marginTemplate.Comments = "名义本金*Maxx,y-0.5*虚值比例)+ Flag*Max(0,PV)\n其中虚值比例 = max(0, P - 执行价)/期初价格,当计算日为成交日时,Flag = 0,P = 期初价格,当计算日不为成交日时,Flag = 1,P = 标的现价;\n若勾选了“取与保底金额孰小”,则取较小的一个;\n保底金额 =ABS(执行价格 - 保底价格)* ABS(交易数量)。";
break;
case MarginRuleTypeEnum.区间追保结构:
this.marginTemplate.Comments = "预付金占用为名义本金的固定比例。按利率债期限(<5y/5y-10y/10y-30y/>30y)分别设置预警线/平仓线与多空各4层追保价格区间及追保金额比例,支持再按标的资产类型(利率债/信用债/其它债券/股票/股指等全部资产类型)分组;以期初净价为基准,标的价格落入录入的追保区间时按对应比例追保,触及预警线/平仓线时触发预警或平仓。";
break;
case MarginRuleTypeEnum.默认预付金规则:
this.marginTemplate.Comments = "考虑了14种场景下的Span算法与Delta算法比较的通用预付金规则,该规则可以调整Span涨跌幅度";
break;
default:
this.marginTemplate.Comments = "";
break;
}
this.changeUnderlyingSeperateType(isInitial);
},
changeUnderlyingSeperateType(isInitial) {
if (isInitial && this.marginTemplate.Details.length >= 1) {
return;
}
if (this.marginTemplate.UnderlyingSeperateType == UnderlyingSeperateTypeEnum.None) {
this.marginTemplate.UnderlyingSeperateComments = "无论挂钩标的代码为何,都采用同一套参数";
} else if (this.marginTemplate.UnderlyingSeperateType == UnderlyingSeperateTypeEnum.CustomInstrumentType) {
this.marginTemplate.UnderlyingSeperateComments = "按标的的资产类型分类:在每组参数中选择适用的资产类型(利率债/信用债/其它债券/股票/股指/商品期货等全部资产类型),按标的资产类型匹配对应参数组的区间追保参数。";
} else {
this.marginTemplate.UnderlyingSeperateComments = "把所有标的根据代码所属区间段自动分为4类:\n【1-指数】沪深指数;\n【2-个股/ETF-核准制发行】主板股票、非科创版ETF;\n【3-个股/ETF-非核准制发行】科创版股票、科创版ETF、创业板股票;\n【4-其他】所有不满足以上分类的。";
}
let that = this
new Promise((reslove) => {
that.marginTemplate.Details = [];
reslove();
}).then(() => {
var needDetailRules = [];
needDetailRules.push(MarginRuleTypeEnum.按固定利率);
needDetailRules.push(MarginRuleTypeEnum.按浮动盈亏);
needDetailRules.push(MarginRuleTypeEnum.类香草预付金规则);
needDetailRules.push(MarginRuleTypeEnum.触发条件预付金规则1);
needDetailRules.push(MarginRuleTypeEnum.触发条件预付金规则2);
needDetailRules.push(MarginRuleTypeEnum.触发条件预付金规则3);
needDetailRules.push(MarginRuleTypeEnum.标准SPAN);
needDetailRules.push(MarginRuleTypeEnum.买入自动赎回规则1);
needDetailRules.push(MarginRuleTypeEnum.卖出自动赎回规则1);
needDetailRules.push(MarginRuleTypeEnum.区间追保结构);
needDetailRules.push(MarginRuleTypeEnum.默认预付金规则);
if (needDetailRules.indexOf(that.marginTemplate.RuleType.toString()) >= 0) {
new Promise((reslove) => {
//规则15:新增的是"资产类型区块"(含区间录入结构与独立区块键);其他规则维持原样
if (that.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构) {
var d = that.newSpanDetail(0, '');
Vue.set(d, '_bk', 'bk_' + (++spanBlockSeq));
that.marginTemplate.Details.push(d);
} else {
that.marginTemplate.Details.push(_.cloneDeep(page.detail));
}
reslove();
}).then(() => {
SetAceDropDown();
})
}
})
},
saveMarginTemplateV2(isForClient) {
//失效确认:编辑中把"生效中"规则改为否时先弹窗提示降级影响,确认后才真正提交;
//新增规则尚未生效、生效状态未翻转,不弹。自定义规则编辑页无"是否生效"开关,不经过此逻辑
if (!this.isAdd && this.originalIsValid === 'true' && String(this.marginTemplate.IsValid) === 'false') {
var that = this;
main.confirm("该规则失效后,适用范围内的持仓将降级匹配下一优先级规则(客户→全局),确认继续?", function () {
that.doSaveMarginTemplateV2(isForClient);
});
return;
}
this.doSaveMarginTemplateV2(isForClient);
},
doSaveMarginTemplateV2(isForClient) {
if (!this.marginTemplate.Name) {
main.message("模板名称不能为空");
return;
}
this.marginTemplate.TradeTypes = $("#tradeTypes").val().join(",");
this.marginTemplate.BookIds = normalizeBookIds($("#BookIds").val());
this.marginTemplate.IsDefault = true;
this.marginTemplate.IsForClient = isForClient;
this.marginTemplate.BuySellType = this.sum($("#BuySellType").val());
this.marginTemplate.MarginScenarios = this.sum($("#MarginScenarios").val());
var that = this;
this.marginTemplate.Details.forEach((detail, index) => {
if (that.marginTemplate.UnderlyingSeperateType == 0) {
detail.UnderlyingType = 15;
}
detail.SpanConfigJson = JSON.stringify(detail.SpanConfig);
});
if (that.marginTemplate.UnderlyingSeperateType != 0) {
if (that.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构) {
//区间追保结构表格:按区块取值——区块首行读选择器,被合并的档位行跟随本区块(区块内资产类型一致)
var groupValue = 0;
that.ruleRangeDetailRows.forEach((row) => {
if (row.rowspan > 0) {
var ut = $("#UnderlyingType" + row.index).val();
groupValue = ut ? that.sum(ut) : (row.detail.UnderlyingType || 0);
//与 onUnderlyingTypeChange 同口径兜底:含利率债(位16)时只能单选利率债
if ((groupValue & TermTierEnabledUnderlyingMask) > 0) groupValue = TermTierEnabledUnderlyingMask;
}
row.detail.UnderlyingType = groupValue;
});
}
else {
that.marginTemplate.Details.forEach((detail, index) => {
detail.UnderlyingType = that.sum($("#UnderlyingType" + index).val());
});
}
}
main.post("/margin_template_v2/saveMarginTemplateV2", { marginTemplate: this.marginTemplate }).done(function (resp) {
try {
window.parent.reloadmargin_template();
(parent || window).layer.closeAll();
} catch (e) { }
});
},
addDetail() {
let that = this
new Promise((reslove) => {
//规则15:新增参数组=新增一个资产类型区块(默认未选资产类型,单行;选成纯利率债时自动展开4档)
if (that.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构) {
var d = that.newSpanDetail(0, '');
Vue.set(d, '_bk', 'bk_' + (++spanBlockSeq));
that.marginTemplate.Details.push(d);
} else {
that.marginTemplate.Details.push(_.cloneDeep(page.detail));
}
reslove();
}).then(() => {
that.refreshChosen();
})
},
deleteDetail(index) {
//规则15:删除区块=删除该区块全部 detail 行(利率债区块4行、其他区块1行)
if (this.marginTemplate.RuleType == MarginRuleTypeEnum.区间追保结构) {
var rows = this.ruleRangeDetailRows;
for (var i = 0; i < rows.length; i++) {
if (rows[i].index === index) {
this.marginTemplate.Details.splice(index, Math.max(rows[i].rowspan, 1));
break;
}
}
} else {
this.marginTemplate.Details.splice(index, 1);
}
this.refreshChosen();
},
//资产类型 chosen 变更(区块级):含利率债(位16)时强制单选利率债;
//纯利率债区块改为不含利率债时先弹确认(4档收缩为1条,其余3档录入数据将丢弃);
//反向(非利率债 → 利率债展开4档)保留已有数据,无需确认
onUnderlyingTypeChange(index) {
var ut = this.sum($("#UnderlyingType" + index).val());
if ((ut & TermTierEnabledUnderlyingMask) > 0) ut = TermTierEnabledUnderlyingMask;
var rows = this.ruleRangeDetailRows;
var head = null;
for (var i = 0; i < rows.length; i++) {
if (rows[i].index === index) { head = rows[i]; break; }
}
if (!head) return;
if ((head.detail.UnderlyingType || 0) === TermTierEnabledUnderlyingMask && ut !== TermTierEnabledUnderlyingMask) {
var that = this;
main.confirm("该区块当前为利率债分档配置,改为其他资产类型后其余期限档的录入内容将被清除,确认修改?", function () {
that.applyUnderlyingTypeChange(index, ut);
}, function () {
//取消:Vue 数据未动,翻转一次 UnderlyingType 强制重渲染 select,再由 chosen 重读选中态回退为纯利率债
head.detail.UnderlyingType = 0;
Vue.nextTick(function () {
head.detail.UnderlyingType = TermTierEnabledUnderlyingMask;
that.refreshChosen();
});
});
return;
}
this.applyUnderlyingTypeChange(index, ut);
},
//区块资产类型变更的实际落地(onUnderlyingTypeChange 确认后调用):
//ut=纯利率债 → 区块固定展开4个期限档 detail(保留已有档位行数据,缺档补新行);
//否则 → 区块收敛为单条 detail(BondTerm 置空,保留首行数据)
applyUnderlyingTypeChange(index, ut) {
var rows = this.ruleRangeDetailRows;
var head = null;
for (var i = 0; i < rows.length; i++) {
if (rows[i].index === index) { head = rows[i]; break; }
}
if (!head) return;
var details = this.marginTemplate.Details;
var headDetail = head.detail;
var bk = headDetail._bk;
if (!bk) {
bk = 'bk_' + (++spanBlockSeq);
Vue.set(headDetail, '_bk', bk);
}
var oldCount = Math.max(head.rowspan, 1);
if (ut === TermTierEnabledUnderlyingMask) {
var byTerm = {};
details.slice(index, index + oldCount).forEach(function (r) {
var bt = (r.SpanConfig && r.SpanConfig.BondTerm) || '';
if (!byTerm[bt]) byTerm[bt] = r;
});
var merged = [];
for (var t = 0; t < SpanBondTerms.length; t++) {
var r = byTerm[SpanBondTerms[t][0]] || (t === 0 ? headDetail : null);
if (!r) r = this.newSpanDetail(TermTierEnabledUnderlyingMask, SpanBondTerms[t][0]);
r.UnderlyingType = TermTierEnabledUnderlyingMask;
r.SpanConfig.BondTerm = SpanBondTerms[t][0];
Vue.set(r, '_bk', bk);
merged.push(r);
}
details.splice.apply(details, [index, oldCount].concat(merged));
} else {
headDetail.UnderlyingType = ut;
if (headDetail.SpanConfig)
{
headDetail.SpanConfig.BondTerm = '';
//离开基金类型时清 ETF 子类(服务端校验:配置子类的行必须纯基金)
if (ut !== FundTypeMask) headDetail.SpanConfig.EtfKind = null;
}
if (oldCount > 1) details.splice(index + 1, oldCount - 1);
}
this.refreshChosen();
},
//行合并重组/删除后重渲染:SetAceDropDown 只增强新出现的 select(已初始化的会跳过,见 MyJs.js:137-138),
//chosen:updated 让被 Vue 复用的 select 按最新 :selected 重读选中态,两者需同时执行
refreshChosen() {
Vue.nextTick(function () {
SetAceDropDown();
$('.chosen-select').trigger('chosen:updated');
});
},
//新建一条规则15 detail:克隆 page.detail 模板行,设置资产类型/期限档/ETF子类,并补齐区间录入结构
newSpanDetail(ut, bondTerm, etfKind) {
var d = _.cloneDeep(page.detail);
d.UnderlyingType = ut;
this.ensureSpanDetail(d);
d.SpanConfig.BondTerm = bondTerm || '';
d.SpanConfig.EtfKind = etfKind || null;
return d;
},
//ETF 子类选择器变更(仅纯基金区块渲染):子类仅用于行区分(取数侧子类行优先),
//ETF 无期限概念不分档——任何子类切换后区块始终收敛为单行(BondTerm 置空,多余档位行删除,首行承载 EtfKind;
//存量 4 档子类数据在这里被收敛时仅保留首行录入内容)
onEtfKindChange(blk) {
var head = blk.sections[0].detail;
var kind = (head.SpanConfig && head.SpanConfig.EtfKind) || '';
var details = this.marginTemplate.Details;
var bk = head._bk;
var rows = details.filter(function (d) { return d._bk === bk; });
rows.forEach(function (r) {
if (r !== head) {
var idx = details.indexOf(r);
if (idx >= 0) details.splice(idx, 1);
}
});
head.UnderlyingType = FundTypeMask;
head.SpanConfig.BondTerm = '';
head.SpanConfig.EtfKind = kind || null;
this.refreshChosen();
},
//分档行判定与分档键:纯利率债 → 'tbond'(连续行归一个区块按固定4档补齐);ETF 子类不分档,基金行一律独立成块
spanTierKey(d) {
var ut = d.UnderlyingType || 0;
if (ut === TermTierEnabledUnderlyingMask) return 'tbond';
return null;
},
//存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行归为区块并按固定4档补齐(缺档补空行);
//同档重复/异常档的分档行、其余全部行(含基金+子类行、基金通配行)均各自独立成块展示,不丢数据——
//存量 ETF 子类 4 档行会以多个单行区块出现,保存时由服务端"ETF 子类不分期限档"校验拦截提示清理
rebuildSpanBlocks(details) {
var termValues = SpanBondTerms.map(function (t) { return t[0]; });
var result = [];
var that = this;
var i = 0;
while (i < details.length) {
var d = details[i];
var tk = this.spanTierKey(d);
if (tk) {
var rows = [];
while (i < details.length && this.spanTierKey(details[i]) === tk) {
rows.push(details[i]);
i++;
}
var byTerm = {};
var anomalies = [];
rows.forEach(function (r) {
var bt = (r.SpanConfig && r.SpanConfig.BondTerm) || '';
if (termValues.indexOf(bt) >= 0 && !byTerm[bt]) byTerm[bt] = r; else anomalies.push(r);
});
var bk = 'bk_' + (++spanBlockSeq);
termValues.forEach(function (t) {
var r = byTerm[t];
if (!r) r = that.newSpanDetail(TermTierEnabledUnderlyingMask, t);
r.UnderlyingType = TermTierEnabledUnderlyingMask;
r.SpanConfig.BondTerm = t;
Vue.set(r, '_bk', bk);
result.push(r);
});
anomalies.forEach(function (r) {
Vue.set(r, '_bk', 'bk_' + (++spanBlockSeq));
result.push(r);
});
} else {
Vue.set(d, '_bk', 'bk_' + (++spanBlockSeq));
result.push(d);
i++;
}
}
return result;
},
underlyingTypeNames(mask) {
mask = parseInt(mask) || 0;
if (mask === 0) return "全部";
var names = [];
for (var i = 0; i < UnderlyingTypeNames.length; i++) {
if ((mask & UnderlyingTypeNames[i][0]) > 0) names.push(UnderlyingTypeNames[i][1]);
}
return names.length > 0 ? names.join("、") : "全部";
},
bondTermLabel(detail) {
var v = (detail.SpanConfig && detail.SpanConfig.BondTerm) || '';
var map = { '': '全部', '<5y': '≤5y', '5y-10y': '(5y-10y]', '10y-30y': '(10y-30y]', '>30y': '>30y' };
return map[v] !== undefined ? map[v] : v;
},
//规则15录入区块文案:按 detail 资产类型切换计价口径。
//比较价格口径与计算侧对齐(MarginCalculationBase.CalcSwapSpanMaintenanceMarginIsBond→中债估值净价,其余→收盘价):
//纯债券类(利率债16/信用债32/其它债券64,可组合):期初净价/当前净价,金额=×期初全价×券面总额;
//其余全部类型(基金/债券指数/股票/股指等):参考标的期初净价/参考标的当前收盘价(多空各层统一收盘价),
//金额=×参考标的期初价格×参考标的名义份额。
spanText(detail) {
var ut = (detail && detail.UnderlyingType) || 0;
//非空且标志位全部落在债券三类内才按债券口径;混合行(债券|非债券)按收盘价口径显示,与取数侧标的实际类型判定方向一致
var bondMask = 16 | 32 | 64;
var isBond = ut !== 0 && (ut & ~bondMask) === 0;
if (!isBond) {
return {
priceInit: '参考标的期初净价',
priceCur: '参考标的当前收盘价',
priceCurShort1: '参考标的当前收盘价',
priceCurShort: '参考标的当前收盘价',
amountBase: '参考标的期初价格 × 参考标的名义份额'
};
}
return {
priceInit: '期初净价',
priceCur: '当前净价',
priceCurShort1: '当前净价',
priceCurShort: '当前净价',
amountBase: '期初全价 × 券面总额'
};
},
//4个空追保区间 tier(区间边界/金额比例均存小数,null=未填)
emptySpanTiers() {
var tiers = [];
for (var i = 0; i < 4; i++) tiers.push({ Lower: null, Upper: null, AmountRate: null });
return tiers;
},
//规则15:补齐区间录入结构——SpanConfig 缺省时补空对象,WarnLine/CloseLine/EtfKind 缺键时补 null
//LongSpans/ShortSpans 非数组或为空时初始化为4个空 tier,tier 缺键补齐(全程 Vue.set 保证响应式,v-model 绑定点必须预先存在)
ensureSpanDetail(detail) {
if (!detail.SpanConfig) Vue.set(detail, 'SpanConfig', { BondTerm: '' });
var sc = detail.SpanConfig;
if (sc.WarnLine === undefined) Vue.set(sc, 'WarnLine', null);
if (sc.CloseLine === undefined) Vue.set(sc, 'CloseLine', null);
if (sc.EtfKind === undefined) Vue.set(sc, 'EtfKind', null);
var that = this;
['LongSpans', 'ShortSpans'].forEach(function (key) {
if (!Array.isArray(sc[key]) || sc[key].length === 0) Vue.set(sc, key, that.emptySpanTiers());
for (var i = 0; i < 4; i++) {
if (!sc[key][i]) Vue.set(sc[key], i, { Lower: null, Upper: null, AmountRate: null });
if (sc[key][i].Lower === undefined) Vue.set(sc[key][i], 'Lower', null);
if (sc[key][i].Upper === undefined) Vue.set(sc[key][i], 'Upper', null);
if (sc[key][i].AmountRate === undefined) Vue.set(sc[key][i], 'AmountRate', null);
}
});
},
sum(items) {
var total = 0;
if (items) {
for (var index = 0; index < items.length; index++) {
total = total + parseFloat(items[index]);
}
}
return total;
}
},
mounted: function () {
/*console.log(this.marginTemplate.Details);*/
},
components: {
'vue-datepicker': FastVue.vueDatePicker(),
'vue-number-input': FastVue.vueNumberInput()
}
});
$(function () {
$(".datepicker").change(function () {
var dateVal = $(this).val();
if (!dateVal) return;
dateVal = dateVal.replace(/\D/g, '').padStart(4, '0');
switch (dateVal.length) {
case 4:
dateVal = new Date().getFullYear() + dateVal; break;
case 6:
dateVal = '20' + dateVal; break;
case 8: break;
default:
dateVal = dateVal.length > 8 ? dateVal.substring(0, 8) : ''; break;
}
if (dateVal) {
dateVal = moment(dateVal).format('YYYY-MM-DD');
!/^\d+/.test(dateVal) && (dateVal = '');
}
$(this).datepicker("setDate", dateVal);
});
//chosen 选中变化通过 jQuery trigger("change") 通知(chosen.jquery.js trigger_form_field_change),
//Vue 的 v-on:change(原生监听)收不到,这里用 jQuery 委托监听规则15区块的资产类型选择器
$(document).on('change', 'select.chosen-select[id^="UnderlyingType"]', function () {
var idx = parseInt(this.id.substring('UnderlyingType'.length));
if (!isNaN(idx)) vue.onUnderlyingTypeChange(idx);
});
//多选 chosen:已有选中项后隐藏搜索框——空框没有输入内容却占一行,放不下时还会换行留白
//(本页多选均为点选场景,无需键盘过滤);全部取消后恢复,显示"请选择…"占位。
//勾选变化会触发原 select 的 jQuery changetrigger_form_field_change),
//加载重建/增删区块后的 refreshChosen 会触发 chosen:updated,两处都走这里的委托统一刷新
function refreshChosenSearchField(select) {
var $sel = $(select);
var $container = $sel.next('.chosen-container-multi');
if ($container.length === 0) return;
var noneSelected = $sel.find('option:selected').length === 0;
$container.find('li.search-field').toggle(noneSelected);
}
$(document).on('change chosen:updated', 'select.chosen-select[multiple]', function () {
refreshChosenSearchField(this);
});
SetAceDropDown();
$('select.chosen-select[multiple]').each(function () {
refreshChosenSearchField(this);
});
});