Files
zszq-trs/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js
T
张名锐 5b7f17727d refactor(swaptrade): 优化互换交易中的数量和金额格式化功能
- 引入UnderlyingInstrumentType字段用于精确控制格式化规则
- 添加quantityPrecision配置支持不同产品类型的数量精度设置
- 实现formatAmount和formatQuantity方法提供统一格式化接口
- 更新视图模板使用新的格式化方法替代直接数据绑定
- 重构swapPricePrecisionHelper.js支持按产品类型定制格式化规则
- 移除废弃的inputFormatTradeAmount等旧格式化配置
- 添加千分位分组显示功能提升数字可读性
2026-08-06 12:41:30 +08:00

367 lines
19 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.
var swapPricePrecision = (function (global) {
const defaults = Object.freeze({
common: {
amount: { precision: 2, grouping: true },
quantity: { precision: 2, grouping: true },
rate: { precision: 4 }
},
Stock: { integerDigits: 7, precision: 2, quantityPrecision: 2 },
StockIndex: { integerDigits: 7, precision: 2, quantityPrecision: 2 },
StockIF: { integerDigits: 7, precision: 4, quantityPrecision: 2 },
CommodityFutures: { integerDigits: 7, precision: 4, quantityPrecision: 2 },
CommoditySpot: { integerDigits: 7, precision: 4, quantityPrecision: 2 },
NewOtcStock: { integerDigits: 7, precision: 4, quantityPrecision: 2 },
HKStock: { integerDigits: 7, precision: 4, quantityPrecision: 2 },
HKStockIndex: { integerDigits: 7, precision: 4, quantityPrecision: 2 },
Fund: { integerDigits: 7, precision: 4, quantityPrecision: 4 },
Bond: {
quantityPrecision: 0,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
TBonds: {
quantityPrecision: 0,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
CreditBonds: {
quantityPrecision: 0,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
OtherBonds: {
quantityPrecision: 0,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
TBFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2 },
OtherFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2 },
GoldSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2 },
OtherSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2 },
AbroadFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2 },
AbroadSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2 },
AbroadStock: { integerDigits: 8, precision: 2, quantityPrecision: 2 },
AbroadStockIndex: { integerDigits: 8, precision: 4, quantityPrecision: 2 },
ExRate: { integerDigits: 2, precision: 8, quantityPrecision: 8 },
Shibor: { integerDigits: 2, precision: 4, quantityPrecision: 2 },
FixingRepoRate: { integerDigits: 2, precision: 4, quantityPrecision: 2 },
RateYield: {integerDigits: 6, precision: 8, quantityPrecision: 2},
BondIndex: {integerDigits: 6, precision: 4, quantityPrecision: 2},
});
function normalizeDecimal(value) {
if (value === null || value === undefined || value === '') return '';
let text = String(value).trim();
if (/[eE]/.test(text)) {
const number = Number(text);
if (!Number.isFinite(number)) return null;
text = number.toFixed(20).replace(/0+$/, '').replace(/\.$/, '');
}
if (!/^[+-]?(?:\d+|\d*\.\d+)$/.test(text)) return null;
const negative = text.charAt(0) === '-';
text = text.replace(/^[+-]/, '');
const parts = text.split('.');
const integerPart = parts[0].replace(/^0+(?=\d)/, '') || '0';
const decimalPart = parts.length > 1 ? parts[1] : '';
const result = integerPart + (decimalPart ? '.' + decimalPart : '');
return negative && !/^0(?:\.0*)?$/.test(result) ? '-' + result : result;
}
function shiftDecimal(value, places) {
let normalized = normalizeDecimal(value);
if (!normalized || !Number.isInteger(places) || places === 0) return normalized;
const negative = normalized.charAt(0) === '-';
const parts = (negative ? normalized.substring(1) : normalized).split('.');
const integerPart = parts[0];
const decimalPart = parts.length > 1 ? parts[1] : '';
const digits = integerPart + decimalPart;
const decimalIndex = integerPart.length + places;
let text;
if (decimalIndex <= 0) text = '0.' + '0'.repeat(-decimalIndex) + digits;
else if (decimalIndex >= digits.length) text = digits + '0'.repeat(decimalIndex - digits.length);
else text = digits.substring(0, decimalIndex) + '.' + digits.substring(decimalIndex);
return normalizeDecimal((negative ? '-' : '') + text);
}
function incrementDigits(value) {
let carry = 1;
let result = '';
for (let index = value.length - 1; index >= 0; index--) {
const digit = value.charCodeAt(index) - 48 + carry;
if (digit === 10) {
result = '0' + result;
carry = 1;
} else {
result = String(digit) + result;
carry = 0;
}
}
return carry ? '1' + result : result;
}
function roundDecimal(value, precision) {
const normalized = normalizeDecimal(value);
if (normalized === null || !Number.isInteger(precision) || precision < 0) return value;
const negative = normalized.charAt(0) === '-';
const parts = (negative ? normalized.substring(1) : normalized).split('.');
let integerPart = parts[0];
const decimalPart = parts.length > 1 ? parts[1] : '';
if (decimalPart.length <= precision) return normalized;
let digits = integerPart + decimalPart.substring(0, precision);
if (decimalPart.charAt(precision) >= '5') digits = incrementDigits(digits);
if (digits.length <= precision) digits = digits.padStart(precision + 1, '0');
integerPart = precision === 0 ? digits : digits.substring(0, digits.length - precision);
const roundedDecimal = precision === 0 ? '' : digits.substring(digits.length - precision);
return normalizeDecimal((negative ? '-' : '') + integerPart + (roundedDecimal ? '.' + roundedDecimal : ''));
}
function normalizeRule(rule) {
if (!rule || typeof rule !== 'object') return null;
const integerDigits = Number(rule.integerDigits);
const precision = Number(rule.precision);
if (!Number.isInteger(integerDigits) || integerDigits < 1 || integerDigits > 18
|| !Number.isInteger(precision) || precision < 0 || precision > 13) return null;
return { integerDigits: integerDigits, precision: precision };
}
function normalizeCommonRule(rule) {
if (!rule || typeof rule !== 'object') return null;
const precision = Number(rule.precision);
if (!Number.isInteger(precision) || precision < 0 || precision > 13) return null;
return { precision: precision, grouping: typeof rule.grouping === 'boolean' ? rule.grouping : undefined };
}
function findRule(source, instrumentType, field) {
const typeRule = source && source[instrumentType];
return typeRule ? normalizeRule(typeRule[field] || typeRule) : null;
}
function getRule(instrumentType, field) {
const fallback = findRule(defaults, instrumentType, field);
if (!fallback) return null;
return findRule(global.main && global.main.swapPricePrecision, instrumentType, field) || fallback;
}
function formatFixed(value, precision) {
const normalized = normalizeDecimal(roundDecimal(value, precision));
if (!normalized) return '';
const negative = normalized.charAt(0) === '-';
const parts = (negative ? normalized.substring(1) : normalized).split('.');
const integerPart = parts[0];
if (precision === 0) return (negative ? '-' : '') + integerPart;
return (negative ? '-' : '') + integerPart + '.' + (parts[1] || '').padEnd(precision, '0');
}
function groupDecimal(value) {
if (!value) return value;
const negative = value.charAt(0) === '-';
const source = negative ? value.substring(1) : value;
const parts = source.split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return (negative ? '-' : '') + parts.join('.');
}
function getQuantityPrecision(instrumentType, fallback) {
if (!instrumentType) return fallback;
const configured = global.main && global.main.swapPricePrecision;
const typeRule = (configured || defaults)[instrumentType];
if (!typeRule || !Object.prototype.hasOwnProperty.call(typeRule, 'quantityPrecision')) return fallback;
const precision = Number(typeRule.quantityPrecision);
return Number.isInteger(precision) && precision >= 0 && precision <= 13 ? precision : fallback;
}
function getCommonRule(kind, instrumentType) {
const fallback = normalizeCommonRule(defaults.common[kind]) || { precision: 2, grouping: false };
const configured = global.main && global.main.swapPricePrecision
&& global.main.swapPricePrecision.common;
const configuredRule = normalizeCommonRule(configured && configured[kind]);
const rule = configuredRule || fallback;
if (rule.grouping === undefined) rule.grouping = fallback.grouping;
if (kind === 'quantity') rule.precision = getQuantityPrecision(instrumentType, rule.precision);
return rule;
}
function formatCommon(kind, value, instrumentType, options) {
if (value === null || value === undefined || value === '') return '';
if (instrumentType && typeof instrumentType === 'object') {
options = instrumentType;
instrumentType = options.instrumentType;
}
const rule = getCommonRule(kind, instrumentType);
const displayValue = kind === 'rate' ? shiftDecimal(value, 2) : value;
const formatted = formatFixed(displayValue, rule.precision);
if (!formatted) return '';
const grouping = options && options.grouping !== undefined ? !!options.grouping : rule.grouping;
const text = grouping ? groupDecimal(formatted) : formatted;
return kind === 'rate' ? text + '%' : text;
}
function format(value, instrumentType, field) {
const rule = getRule(instrumentType, field);
if (value === null || value === undefined || value === '') return '';
if (!rule) return global.otcformat.trading.umprice(value);
const rounded = roundDecimal(value, rule.precision);
return rounded === null ? '' : rounded.replace(/(\.\d*?[1-9])0+$/, '$1').replace(/\.0+$/, '');
}
function normalizeInput(value, format, shouldRound) {
const options = format || {};
const maxIntegerDigits = Number(options.integerDigits) || 0;
const precision = Number(options.precision) || 0;
const source = String(value === null || value === undefined ? '' : value).trim().replaceAll(',', '').replaceAll('。', '.');
let negative = false;
let hasDot = false;
let integerPart = '';
let decimalPart = '';
for (let index = 0; index < source.length; index++) {
const ch = source.charAt(index);
if (ch >= '0' && ch <= '9') {
if (hasDot) {
if (shouldRound || decimalPart.length < precision) decimalPart += ch;
} else if (!maxIntegerDigits || integerPart.length < maxIntegerDigits) {
integerPart += ch;
}
} else if (ch === '.' && !hasDot && precision > 0) {
hasDot = true;
} else if (ch === '-' && index === 0 && options.negative) {
negative = true;
}
}
if (!integerPart && !decimalPart) return negative ? '-' : '';
const text = (negative ? '-' : '') + (integerPart || '0') + (hasDot ? '.' + decimalPart : '');
if (!shouldRound || text.endsWith('.')) return text;
return roundDecimal(text, precision);
}
function createVueInputComponent() {
return {
props: {
value: { type: [Number, String], default: '' },
format: { type: Object, default: function () { return {}; } },
disabled: { type: Boolean }
},
data: function () {
return { text: '', enterPressed: false, formatSnapshot: '' };
},
mounted: function () {
this.text = this.toDisplay(this.value);
this.formatSnapshot = JSON.stringify(this.format || {});
},
methods: {
toDisplay: function (value) {
if (value === null || value === undefined || value === '') return '';
const displayValue = this.format && this.format.percent ? shiftDecimal(value, 2) : String(value);
return normalizeInput(displayValue, this.format, true);
},
toModel: function (value) {
if (!value || value === '-') return '';
return this.format && this.format.percent ? shiftDecimal(value, -2) : value;
},
updateValue: function (value, shouldRound, shouldCommit) {
this.text = normalizeInput(value, this.format, shouldRound);
if (this.text.endsWith('.') && !shouldRound) return;
if (shouldCommit) this.$emit('input', this.toModel(this.text));
},
onInput: function (event) {
this.updateValue(event.target.value, false, false);
// When the normalized value is unchanged, Vue skips the DOM patch.
// Write it directly so excess digits do not remain in the native input.
event.target.value = this.text;
},
onPaste: function (event) {
const clipboard = event.clipboardData || global.clipboardData;
if (!clipboard) return;
event.preventDefault();
this.updateValue(clipboard.getData('text'), true, false);
event.target.value = this.text;
// 粘贴即显式赋值意图:直接派发 input,使「单击空白处标REV/其余清空」「回车前已标记」等行为生效。
// 原实现只把粘贴内容写回DOM、等失焦时的原生change才派发,而原值粘贴时change不触发→静默无反应。
this.$emit('input', this.toModel(this.text));
},
onKeydown: function (event) {
this.enterPressed = false;
this.$emit('keydown', event);
const keyCode = event.which || event.keyCode;
if (event.key !== 'Enter' && keyCode !== 13 && keyCode !== 108) return;
// 回车=用户显式提交意图:无论值是否相对聚焦时变化,都直接派发 input(标REV) + enter(触发计算)。
// 原实现把 enter 锁在原生 change 里,而 change 仅在值变化时触发,
// 导致「重输/重贴原值 + 回车」静默不计算(QA 高优 Bug)。
this.updateValue(this.text, true, false);
const value = this.toModel(this.text);
event.target.blur(); // 同步触发原生 change(值变化时再派发一次 input,幂等无害)
this.$emit('input', value);
this.$emit('enter', value);
this.enterPressed = false;
},
onChange: function (event) {
if (this.text.endsWith('.')) this.text = this.text.substring(0, this.text.length - 1);
this.updateValue(this.text, true, false);
const value = this.toModel(this.text);
// 失焦(非回车)→ 仅派发 input 标 REVenter(计算) 改由 onKeydown 直接派发,
// 避免 change 不触发时丢失,也避免与 onKeydown 重复派发 enter 造成重复计算。
this.$emit('input', value);
event.target.value = this.text;
}
},
watch: {
value: function (value) {
const display = this.toDisplay(value);
if (display !== this.text) this.text = display;
},
format: {
deep: true,
handler: function () {
const snapshot = JSON.stringify(this.format || {});
if (snapshot === this.formatSnapshot) return;
this.formatSnapshot = snapshot;
this.text = this.toDisplay(this.value);
}
}
},
template: '<input type="text" :disabled="disabled" :value="text" @input="onInput" @paste="onPaste" @keydown="onKeydown" @blur="onChange">'
};
}
return Object.freeze({
getRule: getRule,
getCommonPrecision: function (kind, instrumentType) {
return getCommonRule(kind, instrumentType).precision;
},
getCommonInputFormat: function (kind, options, instrumentType) {
const inputOptions = Object.assign({}, options || {});
const type = instrumentType || inputOptions.instrumentType;
delete inputOptions.instrumentType;
const rule = getCommonRule(kind, type);
return Object.assign(inputOptions, {
precision: rule.precision,
grouping: inputOptions.grouping === undefined ? !!rule.grouping : !!inputOptions.grouping,
trimTailZeros: false
});
},
formatCommon: formatCommon,
normalizeCommon: function (kind, value, instrumentType) {
return formatCommon(kind, value, instrumentType, { grouping: false });
},
getInputFormat: function (instrumentType, field, options) {
const rule = getRule(instrumentType, field);
return rule ? Object.assign({}, options, rule) : Object.assign({}, options);
},
format: format,
roundDecimal: roundDecimal,
roundForSubmit: function (value, instrumentType, field, storagePrecisionOffset) {
const rule = getRule(instrumentType, field);
if (value === null || value === undefined || value === '' || !rule) return value;
const offset = Number.isInteger(storagePrecisionOffset) ? storagePrecisionOffset : 0;
return roundDecimal(value, rule.precision + offset);
},
shiftDecimal: shiftDecimal,
createVueInputComponent: createVueInputComponent
});
}(window));