- 绑定三个债券价格字段到当前Vue方法,确保正确的v-model和事件监听器 - 修复精度输入组件的事件契约,保留keydown、input、enter事件处理逻辑 - 解决格式刷新时不覆盖未提交输入的问题,维护用户输入状态一致性 - 将模板中的@change事件替换为@blur事件,改进输入焦点处理逻辑
266 lines
13 KiB
JavaScript
266 lines
13 KiB
JavaScript
var swapPricePrecision = (function (global) {
|
|
const defaults = Object.freeze({
|
|
Stock: { integerDigits: 7, precision: 2 },
|
|
StockIndex: { integerDigits: 7, precision: 2 },
|
|
StockIF: { integerDigits: 7, precision: 4 },
|
|
CommodityFutures: { integerDigits: 7, precision: 4 },
|
|
CommoditySpot: { integerDigits: 7, precision: 4 },
|
|
NewOtcStock: { integerDigits: 7, precision: 4 },
|
|
HKStock: { integerDigits: 7, precision: 4 },
|
|
HKStockIndex: { integerDigits: 7, precision: 4 },
|
|
Fund: { integerDigits: 7, precision: 4 },
|
|
Bond: {
|
|
grossPrice: { integerDigits: 6, precision: 9 },
|
|
netPrice: { integerDigits: 6, precision: 9 },
|
|
yield: { integerDigits: 2, precision: 4 }
|
|
},
|
|
TBonds: {
|
|
grossPrice: { integerDigits: 6, precision: 9 },
|
|
netPrice: { integerDigits: 6, precision: 9 },
|
|
yield: { integerDigits: 2, precision: 4 }
|
|
},
|
|
CreditBonds: {
|
|
grossPrice: { integerDigits: 6, precision: 9 },
|
|
netPrice: { integerDigits: 6, precision: 9 },
|
|
yield: { integerDigits: 2, precision: 4 }
|
|
},
|
|
OtherBonds: {
|
|
grossPrice: { integerDigits: 6, precision: 9 },
|
|
netPrice: { integerDigits: 6, precision: 9 },
|
|
yield: { integerDigits: 2, precision: 4 }
|
|
},
|
|
TBFutures: { integerDigits: 8, precision: 4 },
|
|
OtherFutures: { integerDigits: 8, precision: 4 },
|
|
GoldSpot: { integerDigits: 8, precision: 4 },
|
|
OtherSpot: { integerDigits: 8, precision: 4 },
|
|
AbroadFutures: { integerDigits: 8, precision: 4 },
|
|
AbroadSpot: { integerDigits: 8, precision: 4 },
|
|
AbroadStock: { integerDigits: 8, precision: 2 },
|
|
AbroadStockIndex: { integerDigits: 8, precision: 4 },
|
|
ExRate: { integerDigits: 2, precision: 8 },
|
|
Shibor: { integerDigits: 2, precision: 4 },
|
|
FixingRepoRate: { integerDigits: 2, precision: 4 }
|
|
// TODO: Add InterestYield, BondIndex and GoldFutures after their enum values are confirmed.
|
|
});
|
|
|
|
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 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 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;
|
|
},
|
|
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;
|
|
this.enterPressed = true;
|
|
event.target.blur();
|
|
},
|
|
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);
|
|
this.$emit('input', value);
|
|
if (this.enterPressed) this.$emit('enter', value);
|
|
this.enterPressed = false;
|
|
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,
|
|
getInputFormat: function (instrumentType, field, options) {
|
|
const rule = getRule(instrumentType, field);
|
|
return rule ? Object.assign({}, options, rule) : Object.assign({}, options);
|
|
},
|
|
format: format,
|
|
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));
|