预警线:
diff --git a/YLErpWeb/wwwroot/Scripts/app/marginTemplate/marginTemplateV2DefaultEdit.js b/YLErpWeb/wwwroot/Scripts/app/marginTemplate/marginTemplateV2DefaultEdit.js
index 8585ebda..6e5e71d1 100644
--- a/YLErpWeb/wwwroot/Scripts/app/marginTemplate/marginTemplateV2DefaultEdit.js
+++ b/YLErpWeb/wwwroot/Scripts/app/marginTemplate/marginTemplateV2DefaultEdit.js
@@ -32,6 +32,13 @@ const UnderlyingSeperateTypeEnum = {
//与后端 ConsMarginTerm.TermTierEnabledUnderlyingTypes 保持一致(本期仅 利率债 TBonds=1<<4=16)
const TermTierEnabledUnderlyingMask = 16;
+//基金及基金专户标志位(ETF 子类行的 UnderlyingType,不加新枚举位)
+const FundTypeMask = 32768;
+
+//允许配置期限档的 ETF 子类(SpanConfig.EtfKind 值,取值=数据字典"ETF 子类"),
+//与后端 ConsMarginTerm.TermTierEnabledEtfKinds 保持一致;其余子类与"不区分"的基金行不分档
+const TermTierEnabledEtfKinds = ['可转债 ETF', '科创债 ETF'];
+
//利率债区块固定展开的4个期限档([BondTerm值, 展示文案]),与 docx 原型 4.3.1 一致
const SpanBondTerms = [['<5y', '≤5y'], ['5y-10y', '(5y-10y]'], ['10y-30y', '(10y-30y]'], ['>30y', '>30y']];
@@ -51,7 +58,8 @@ const vue = new Vue({
el: '#marginTemplateV2Form',
data: {
isAdd: page.isAdd,
- marginTemplate: page.marginTemplate
+ marginTemplate: page.marginTemplate,
+ etfSubtypeItems: page.etfSubtypeItems || []
},
created: function () {
//区间追保结构:加载时按 资产类型+期限 排序一次,再重建资产类型区块(利率债区块补齐固定4档),
@@ -98,8 +106,9 @@ const vue = new Vue({
}
return rows;
},
- //区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)区块固定展开4个期限档段(每段一条 detail),
- //其他区块单段(BondTerm 为空)。段的 detail 缺失时模板侧 v-if 兜底不渲染该段
+ //区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)与分档 ETF 子类
+ //(ut=基金 + EtfKind∈可转债/科创债 ETF)区块固定展开4个期限档段(每段一条 detail),其他区块单段(BondTerm 为空)。
+ //段的 detail 缺失时模板侧 v-if 兜底不渲染该段
ruleRangeBlocks() {
if (this.marginTemplate.RuleType != MarginRuleTypeEnum.区间追保结构) return [];
var blocks = [];
@@ -109,9 +118,12 @@ const vue = new Vue({
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 isTieredEtf = ut === FundTypeMask && TermTierEnabledEtfKinds.indexOf(kind) >= 0;
+ var isTiered = isTBond || isTieredEtf;
var sections = [];
- if (isTBond) {
+ if (isTiered) {
for (var t = 0; t < SpanBondTerms.length; t++) {
var found = null;
for (var j = head.index; j < head.index + head.rowspan; j++) {
@@ -123,7 +135,7 @@ const vue = new Vue({
} 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, isTBond: isTBond, sections: sections });
+ blocks.push({ key: head.detail._bk || ('i' + head.index), no: blocks.length + 1, headIndex: head.index, ut: ut, etfKind: kind, isTBond: isTBond, isTieredEtf: isTieredEtf, isFundBlock: ut === FundTypeMask, sections: sections });
}
return blocks;
}
@@ -382,7 +394,12 @@ const vue = new Vue({
details.splice.apply(details, [index, oldCount].concat(merged));
} else {
headDetail.UnderlyingType = ut;
- if (headDetail.SpanConfig) headDetail.SpanConfig.BondTerm = '';
+ 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();
@@ -395,16 +412,70 @@ const vue = new Vue({
$('.chosen-select').trigger('chosen:updated');
});
},
- //新建一条规则15 detail:克隆 page.detail 模板行,设置资产类型/期限档,并补齐区间录入结构
- newSpanDetail(ut, bondTerm) {
+ //新建一条规则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;
},
- //存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行归为一个区块并按固定4档补齐(缺档补空行);
- //同档重复/异常档的利率债行、非利率债行均各自独立成块展示,不丢数据
+ //ETF 子类选择器变更(仅纯基金区块渲染):选可转债/科创债 ETF → 区块展开固定4档(缺档补行、异常档行删除);
+ //选其他子类/不区分 → 收敛为单行(BondTerm 置空,多余档位行删除,首行承载 EtfKind)
+ onEtfKindChange(blk) {
+ var head = blk.sections[0].detail;
+ var kind = (head.SpanConfig && head.SpanConfig.EtfKind) || '';
+ var tiered = TermTierEnabledEtfKinds.indexOf(kind) >= 0;
+ var details = this.marginTemplate.Details;
+ var bk = head._bk;
+ var rows = details.filter(function (d) { return d._bk === bk; });
+ if (tiered) {
+ var byTerm = {};
+ rows.forEach(function (r) {
+ var bt = (r.SpanConfig && r.SpanConfig.BondTerm) || '';
+ if (byTerm[bt] === undefined) byTerm[bt] = r;
+ });
+ var that = this;
+ var merged = [];
+ SpanBondTerms.forEach(function (t) {
+ var r = byTerm[t[0]];
+ if (!r) r = that.newSpanDetail(FundTypeMask, t[0], kind);
+ r.UnderlyingType = FundTypeMask;
+ r.SpanConfig.EtfKind = kind;
+ r.SpanConfig.BondTerm = t[0];
+ Vue.set(r, '_bk', bk);
+ merged.push(r);
+ var idx = details.indexOf(r);
+ if (idx >= 0 && r !== head) details.splice(idx, 1);
+ });
+ //异常档/重复行全部移除后按档序回插到区块首行位置
+ var headIdx = details.indexOf(head);
+ if (headIdx < 0) headIdx = details.length;
+ details.splice.apply(details, [headIdx + 1, 0].concat(merged.filter(function (r) { return r !== head; })));
+ } else {
+ 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';基金 + 分档 ETF 子类 → 'etf:子类'(同一子类的连续行归一个区块)
+ spanTierKey(d) {
+ var ut = d.UnderlyingType || 0;
+ var kind = (d.SpanConfig && d.SpanConfig.EtfKind) || '';
+ if (ut === TermTierEnabledUnderlyingMask) return 'tbond';
+ if (ut === FundTypeMask && TermTierEnabledEtfKinds.indexOf(kind) >= 0) return 'etf:' + kind;
+ return null;
+ },
+ //存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行、基金+分档ETF子类行各自归为区块并按固定4档补齐(缺档补空行);
+ //同档重复/异常档的分档行、非分档行(含基金+不分档子类、基金通配行)均各自独立成块展示,不丢数据
rebuildSpanBlocks(details) {
var termValues = SpanBondTerms.map(function (t) { return t[0]; });
var result = [];
@@ -412,10 +483,10 @@ const vue = new Vue({
var i = 0;
while (i < details.length) {
var d = details[i];
- var ut = d.UnderlyingType || 0;
- if (ut === TermTierEnabledUnderlyingMask) {
+ var tk = this.spanTierKey(d);
+ if (tk) {
var rows = [];
- while (i < details.length && (details[i].UnderlyingType || 0) === TermTierEnabledUnderlyingMask) {
+ while (i < details.length && this.spanTierKey(details[i]) === tk) {
rows.push(details[i]);
i++;
}
@@ -426,10 +497,13 @@ const vue = new Vue({
if (termValues.indexOf(bt) >= 0 && !byTerm[bt]) byTerm[bt] = r; else anomalies.push(r);
});
var bk = 'bk_' + (++spanBlockSeq);
+ var utv = tk === 'tbond' ? TermTierEnabledUnderlyingMask : FundTypeMask;
+ var kind = tk === 'tbond' ? '' : tk.substring(4);
termValues.forEach(function (t) {
var r = byTerm[t];
- if (!r) r = that.newSpanDetail(TermTierEnabledUnderlyingMask, t);
- r.UnderlyingType = TermTierEnabledUnderlyingMask;
+ if (!r) r = that.newSpanDetail(utv, t, kind);
+ r.UnderlyingType = utv;
+ if (kind) r.SpanConfig.EtfKind = kind;
r.SpanConfig.BondTerm = t;
Vue.set(r, '_bk', bk);
result.push(r);
@@ -489,13 +563,14 @@ const vue = new Vue({
for (var i = 0; i < 4; i++) tiers.push({ Lower: null, Upper: null, AmountRate: null });
return tiers;
},
- //规则15:补齐区间录入结构——SpanConfig 缺省时补空对象,WarnLine/CloseLine 缺键时补 null,
+ //规则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());
diff --git a/YLErpWeb/wwwroot/Scripts/app/marginTemplate/marginTemplateV2Edit.js b/YLErpWeb/wwwroot/Scripts/app/marginTemplate/marginTemplateV2Edit.js
index 0e09bc36..c2eb75cf 100644
--- a/YLErpWeb/wwwroot/Scripts/app/marginTemplate/marginTemplateV2Edit.js
+++ b/YLErpWeb/wwwroot/Scripts/app/marginTemplate/marginTemplateV2Edit.js
@@ -32,6 +32,13 @@ const UnderlyingSeperateTypeEnum = {
//与后端 ConsMarginTerm.TermTierEnabledUnderlyingTypes 保持一致(本期仅 利率债 TBonds=1<<4=16)
const TermTierEnabledUnderlyingMask = 16;
+//基金及基金专户标志位(ETF 子类行的 UnderlyingType,不加新枚举位)
+const FundTypeMask = 32768;
+
+//允许配置期限档的 ETF 子类(SpanConfig.EtfKind 值,取值=数据字典"ETF 子类"),
+//与后端 ConsMarginTerm.TermTierEnabledEtfKinds 保持一致;其余子类与"不区分"的基金行不分档
+const TermTierEnabledEtfKinds = ['可转债 ETF', '科创债 ETF'];
+
//利率债区块固定展开的4个期限档([BondTerm值, 展示文案]),与 docx 原型 4.3.1 一致
const SpanBondTerms = [['<5y', '≤5y'], ['5y-10y', '(5y-10y]'], ['10y-30y', '(10y-30y]'], ['>30y', '>30y']];
@@ -51,7 +58,8 @@ const vue = new Vue({
el: '#marginTemplateV2Form',
data: {
isAdd: page.isAdd,
- marginTemplate: page.marginTemplate
+ marginTemplate: page.marginTemplate,
+ etfSubtypeItems: page.etfSubtypeItems || []
},
created: function () {
//区间追保结构:加载时按 资产类型+期限 排序一次,再重建资产类型区块(利率债区块补齐固定4档),
@@ -98,8 +106,9 @@ const vue = new Vue({
}
return rows;
},
- //区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)区块固定展开4个期限档段(每段一条 detail),
- //其他区块单段(BondTerm 为空)。段的 detail 缺失时模板侧 v-if 兜底不渲染该段
+ //区间追保结构录入区块:每个资产类型区块一个;纯利率债(ut=16)与分档 ETF 子类
+ //(ut=基金 + EtfKind∈可转债/科创债 ETF)区块固定展开4个期限档段(每段一条 detail),其他区块单段(BondTerm 为空)。
+ //段的 detail 缺失时模板侧 v-if 兜底不渲染该段
ruleRangeBlocks() {
if (this.marginTemplate.RuleType != MarginRuleTypeEnum.区间追保结构) return [];
var blocks = [];
@@ -109,9 +118,12 @@ const vue = new Vue({
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 isTieredEtf = ut === FundTypeMask && TermTierEnabledEtfKinds.indexOf(kind) >= 0;
+ var isTiered = isTBond || isTieredEtf;
var sections = [];
- if (isTBond) {
+ if (isTiered) {
for (var t = 0; t < SpanBondTerms.length; t++) {
var found = null;
for (var j = head.index; j < head.index + head.rowspan; j++) {
@@ -123,7 +135,7 @@ const vue = new Vue({
} 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, isTBond: isTBond, sections: sections });
+ blocks.push({ key: head.detail._bk || ('i' + head.index), no: blocks.length + 1, headIndex: head.index, ut: ut, etfKind: kind, isTBond: isTBond, isTieredEtf: isTieredEtf, isFundBlock: ut === FundTypeMask, sections: sections });
}
return blocks;
}
@@ -393,7 +405,12 @@ const vue = new Vue({
details.splice.apply(details, [index, oldCount].concat(merged));
} else {
headDetail.UnderlyingType = ut;
- if (headDetail.SpanConfig) headDetail.SpanConfig.BondTerm = '';
+ 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();
@@ -406,16 +423,70 @@ const vue = new Vue({
$('.chosen-select').trigger('chosen:updated');
});
},
- //新建一条规则15 detail:克隆 page.detail 模板行,设置资产类型/期限档,并补齐区间录入结构
- newSpanDetail(ut, bondTerm) {
+ //新建一条规则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;
},
- //存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行归为一个区块并按固定4档补齐(缺档补空行);
- //同档重复/异常档的利率债行、非利率债行均各自独立成块展示,不丢数据
+ //ETF 子类选择器变更(仅纯基金区块渲染):选可转债/科创债 ETF → 区块展开固定4档(缺档补行、异常档行删除);
+ //选其他子类/不区分 → 收敛为单行(BondTerm 置空,多余档位行删除,首行承载 EtfKind)
+ onEtfKindChange(blk) {
+ var head = blk.sections[0].detail;
+ var kind = (head.SpanConfig && head.SpanConfig.EtfKind) || '';
+ var tiered = TermTierEnabledEtfKinds.indexOf(kind) >= 0;
+ var details = this.marginTemplate.Details;
+ var bk = head._bk;
+ var rows = details.filter(function (d) { return d._bk === bk; });
+ if (tiered) {
+ var byTerm = {};
+ rows.forEach(function (r) {
+ var bt = (r.SpanConfig && r.SpanConfig.BondTerm) || '';
+ if (byTerm[bt] === undefined) byTerm[bt] = r;
+ });
+ var that = this;
+ var merged = [];
+ SpanBondTerms.forEach(function (t) {
+ var r = byTerm[t[0]];
+ if (!r) r = that.newSpanDetail(FundTypeMask, t[0], kind);
+ r.UnderlyingType = FundTypeMask;
+ r.SpanConfig.EtfKind = kind;
+ r.SpanConfig.BondTerm = t[0];
+ Vue.set(r, '_bk', bk);
+ merged.push(r);
+ var idx = details.indexOf(r);
+ if (idx >= 0 && r !== head) details.splice(idx, 1);
+ });
+ //异常档/重复行全部移除后按档序回插到区块首行位置
+ var headIdx = details.indexOf(head);
+ if (headIdx < 0) headIdx = details.length;
+ details.splice.apply(details, [headIdx + 1, 0].concat(merged.filter(function (r) { return r !== head; })));
+ } else {
+ 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';基金 + 分档 ETF 子类 → 'etf:子类'(同一子类的连续行归一个区块)
+ spanTierKey(d) {
+ var ut = d.UnderlyingType || 0;
+ var kind = (d.SpanConfig && d.SpanConfig.EtfKind) || '';
+ if (ut === TermTierEnabledUnderlyingMask) return 'tbond';
+ if (ut === FundTypeMask && TermTierEnabledEtfKinds.indexOf(kind) >= 0) return 'etf:' + kind;
+ return null;
+ },
+ //存量回显重建资产类型区块(分配区块键 _bk):连续的纯利率债行、基金+分档ETF子类行各自归为区块并按固定4档补齐(缺档补空行);
+ //同档重复/异常档的分档行、非分档行(含基金+不分档子类、基金通配行)均各自独立成块展示,不丢数据
rebuildSpanBlocks(details) {
var termValues = SpanBondTerms.map(function (t) { return t[0]; });
var result = [];
@@ -423,10 +494,10 @@ const vue = new Vue({
var i = 0;
while (i < details.length) {
var d = details[i];
- var ut = d.UnderlyingType || 0;
- if (ut === TermTierEnabledUnderlyingMask) {
+ var tk = this.spanTierKey(d);
+ if (tk) {
var rows = [];
- while (i < details.length && (details[i].UnderlyingType || 0) === TermTierEnabledUnderlyingMask) {
+ while (i < details.length && this.spanTierKey(details[i]) === tk) {
rows.push(details[i]);
i++;
}
@@ -437,10 +508,13 @@ const vue = new Vue({
if (termValues.indexOf(bt) >= 0 && !byTerm[bt]) byTerm[bt] = r; else anomalies.push(r);
});
var bk = 'bk_' + (++spanBlockSeq);
+ var utv = tk === 'tbond' ? TermTierEnabledUnderlyingMask : FundTypeMask;
+ var kind = tk === 'tbond' ? '' : tk.substring(4);
termValues.forEach(function (t) {
var r = byTerm[t];
- if (!r) r = that.newSpanDetail(TermTierEnabledUnderlyingMask, t);
- r.UnderlyingType = TermTierEnabledUnderlyingMask;
+ if (!r) r = that.newSpanDetail(utv, t, kind);
+ r.UnderlyingType = utv;
+ if (kind) r.SpanConfig.EtfKind = kind;
r.SpanConfig.BondTerm = t;
Vue.set(r, '_bk', bk);
result.push(r);
@@ -500,13 +574,14 @@ const vue = new Vue({
for (var i = 0; i < 4; i++) tiers.push({ Lower: null, Upper: null, AmountRate: null });
return tiers;
},
- //规则15:补齐区间录入结构——SpanConfig 缺省时补空对象,WarnLine/CloseLine 缺键时补 null,
+ //规则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());