Files
zszq-trs/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js
T
张名锐 486f02dcd9 feat(swaptrade): 添加数量显示尾随零去除功能
- 在 swapPricePrecisionHelper 中实现 trimTailZeros 选项支持
- 修改 formatCommon 函数以支持尾随零去除逻辑
- 在 swapTradeView 中为数量类型添加 trimTailZeros 配置
- 添加测试用例验证数量显示的尾随零去除功能
- 确保配置精度在尾随零去除后保持不变
2026-08-07 17:57:16 +08:00

422 lines
22 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: { integerDigits: 8, precision: 2, grouping: true },
rate: { precision: 4 }
},
Stock: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 8 },
StockIndex: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 8 },
StockIF: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
CommodityFutures: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
CommoditySpot: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
NewOtcStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
HKStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
HKStockIndex: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
Fund: { integerDigits: 7, precision: 4, quantityPrecision: 4, quantityIntegerDigits: 8 },
Bond: {
quantityPrecision: 0, quantityIntegerDigits: 12,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
TBonds: {
quantityPrecision: 0, quantityIntegerDigits: 12,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
CreditBonds: {
quantityPrecision: 0, quantityIntegerDigits: 12,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
OtherBonds: {
quantityPrecision: 0, quantityIntegerDigits: 12,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
TBFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
OtherFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
GoldFutures: { integerDigits: 6, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
GoldSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
OtherSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
AbroadFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
AbroadSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
AbroadStock: { integerDigits: 8, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 8 },
AbroadStockIndex: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
ExRate: { integerDigits: 2, precision: 8, quantityPrecision: 4, quantityIntegerDigits: 8 },
Shibor: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
FixingRepoRate: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8 },
RateYield: {integerDigits: 6, precision: 8, quantityPrecision: 2, quantityIntegerDigits: 8},
BondIndex: {integerDigits: 6, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 8},
});
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 multiplyDecimal(left, right) {
const normalizedLeft = normalizeDecimal(left);
const normalizedRight = normalizeDecimal(right);
if (normalizedLeft === null || normalizedRight === null) return null;
const leftNegative = normalizedLeft.charAt(0) === '-';
const rightNegative = normalizedRight.charAt(0) === '-';
const leftParts = (leftNegative ? normalizedLeft.substring(1) : normalizedLeft).split('.');
const rightParts = (rightNegative ? normalizedRight.substring(1) : normalizedRight).split('.');
const leftDigits = leftParts.join('');
const rightDigits = rightParts.join('');
const result = Array(leftDigits.length + rightDigits.length).fill(0);
for (let leftIndex = leftDigits.length - 1; leftIndex >= 0; leftIndex--) {
for (let rightIndex = rightDigits.length - 1; rightIndex >= 0; rightIndex--) {
const index = leftIndex + rightIndex + 1;
const product = (leftDigits.charCodeAt(leftIndex) - 48) * (rightDigits.charCodeAt(rightIndex) - 48) + result[index];
result[index] = product % 10;
result[index - 1] += Math.floor(product / 10);
}
}
const product = result.join('').replace(/^0+/, '') || '0';
const decimalPlaces = (leftParts[1] || '').length + (rightParts[1] || '').length;
return shiftDecimal((leftNegative !== rightNegative ? '-' : '') + product, -decimalPlaces);
}
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;
const integerDigits = Number(rule.integerDigits);
return {
precision: precision,
integerDigits: Number.isInteger(integerDigits) && integerDigits >= 1 && integerDigits <= 18 ? integerDigits : undefined,
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 getQuantityIntegerDigits(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, 'quantityIntegerDigits')) return fallback;
const integerDigits = Number(typeRule.quantityIntegerDigits);
return Number.isInteger(integerDigits) && integerDigits >= 1 && integerDigits <= 18 ? integerDigits : 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 (rule.integerDigits === undefined) rule.integerDigits = fallback.integerDigits;
if (kind === 'quantity') {
rule.precision = Math.min(8, getQuantityPrecision(instrumentType, rule.precision));
rule.integerDigits = Math.min(16, getQuantityIntegerDigits(instrumentType, rule.integerDigits));
}
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 trimmed = options && options.trimTailZeros === true
? formatted.replace(/(\.\d*?[1-9])0+$/, '$1').replace(/\.0+$/, '')
: formatted;
const text = grouping ? groupDecimal(trimmed) : trimmed;
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);
const result = Object.assign(inputOptions, {
precision: rule.precision,
grouping: inputOptions.grouping === undefined ? !!rule.grouping : !!inputOptions.grouping,
trimTailZeros: false
});
if (kind === 'quantity') result.stringMode = true;
if (rule.integerDigits !== undefined) result.integerDigits = rule.integerDigits;
return result;
},
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,
multiplyDecimal: multiplyDecimal,
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
});
}(typeof window !== 'undefined' ? window : globalThis));
if (typeof module === 'object' && module.exports) module.exports = swapPricePrecision;