415 lines
12 KiB
Vue
415 lines
12 KiB
Vue
|
<template>
|
|||
|
<div class="unit-converter" :style="{ width: width + 'px' }">
|
|||
|
<!-- 数值输入框 -->
|
|||
|
|
|||
|
<input v-model.number="inputValue" class="input-field" type="number" ref="inputRef" :style="{ width: inputWidth + 'px', height: height + 'px' }" @change="handleInputChange" />
|
|||
|
|
|||
|
<!-- 单位标签 -->
|
|||
|
<span v-if="enableConvert" ref="unitLabel" @click="cycleUnit" @dblclick="toggleUnitSelector" class="unit-label" :style="{ color: 'blue', height: height + 'px' }">
|
|||
|
{{ textUnitName }}
|
|||
|
</span>
|
|||
|
<span v-else ref="unitLabel" class="unit-label" :style="{ color: 'blue', height: height + 'px' }">
|
|||
|
{{ textUnitName }}
|
|||
|
</span>
|
|||
|
|
|||
|
<!-- 单位选择弹出窗口 -->
|
|||
|
<div v-if="showUnitSelector" class="unit-selector" ref="unitSelector" :style="{ left: unitSelectorLeft + 'px', top: unitSelectorTop + 'px' }">
|
|||
|
<div v-for="unit in sortedUnits" :key="unit.id" @click="selectUnit(unit)" class="unit-option">
|
|||
|
{{ showEnglishOnly ? unit.unitName.split('(')[0] : unit.unitName }}
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</template>
|
|||
|
|
|||
|
<script>
|
|||
|
import { listSysUnitConvert } from '@/api/system/sysUnitConvert.js';
|
|||
|
|
|||
|
export default {
|
|||
|
name: 'UnitConverter',
|
|||
|
props: {
|
|||
|
unitType: {
|
|||
|
// 当前单位类型 (required)
|
|||
|
type: String,
|
|||
|
required: true
|
|||
|
},
|
|||
|
unitOrder: {
|
|||
|
// 当前单位序号 (sync)
|
|||
|
type: Number,
|
|||
|
default: 0
|
|||
|
},
|
|||
|
value: {
|
|||
|
// 输入数值 (sync)
|
|||
|
type: Number,
|
|||
|
required: true
|
|||
|
},
|
|||
|
showEnglishOnly: {
|
|||
|
// 是否只显示英文
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
},
|
|||
|
decimalPlaces: {
|
|||
|
// 小数位数
|
|||
|
type: Number,
|
|||
|
default: 5,
|
|||
|
validator: (v) => v >= 0 && v <= 10
|
|||
|
},
|
|||
|
width: {
|
|||
|
type: Number,
|
|||
|
required: false,
|
|||
|
default: 180
|
|||
|
},
|
|||
|
height: {
|
|||
|
type: Number,
|
|||
|
required: false,
|
|||
|
default: 32
|
|||
|
},
|
|||
|
enableConvert: {
|
|||
|
//是否能够进行单位换算
|
|||
|
type: Boolean,
|
|||
|
default: true
|
|||
|
},
|
|||
|
userDefined: {
|
|||
|
//自定义输入单位显示
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
},
|
|||
|
userDefinedunitName: {
|
|||
|
//自定义输入单位显示
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
}
|
|||
|
},
|
|||
|
mounted() {
|
|||
|
// 在挂载时添加全局点击事件监听器
|
|||
|
document.addEventListener('click', this.handleClickOutside);
|
|||
|
},
|
|||
|
beforeUnmount() {
|
|||
|
// 在组件销毁前移除全局点击事件监听器
|
|||
|
document.removeEventListener('click', this.handleClickOutside);
|
|||
|
},
|
|||
|
data() {
|
|||
|
return {
|
|||
|
inputValue: this.value,
|
|||
|
unitData: [], // 原始单位数据
|
|||
|
showUnitSelector: false, // 显示选择器
|
|||
|
currentUnit: null, // 当前选中单位
|
|||
|
baseUnit: null, // 基准单位
|
|||
|
inputWidth: 100, //输入框的宽度
|
|||
|
textUnitName: '',
|
|||
|
unitSelectorLeft: 0,
|
|||
|
unitSelectorTop: 0,
|
|||
|
// 新增三个数据项
|
|||
|
originalValue: this.value, // 原始输入值
|
|||
|
originalUnit: null, // 原始输入单位
|
|||
|
isInternalUpdate: false // 更新锁定标志
|
|||
|
};
|
|||
|
},
|
|||
|
computed: {
|
|||
|
// 显示的单位文本
|
|||
|
displayUnitText() {
|
|||
|
if (!this.currentUnit) return '';
|
|||
|
return this.showEnglishOnly ? this.currentUnit.unitName.split('(')[0].trim() : this.currentUnit.unitName;
|
|||
|
},
|
|||
|
|
|||
|
// 当前类型的单位列表(按unitOrder排序)
|
|||
|
sortedUnits() {
|
|||
|
return this.unitData.filter((u) => u.unitType === this.unitType).sort((a, b) => a.unitOrder - b.unitOrder);
|
|||
|
},
|
|||
|
|
|||
|
// 弹窗定位样式
|
|||
|
popupPosition() {
|
|||
|
if (!this.$refs.unitLabel) return {};
|
|||
|
const rect = this.$refs.unitLabel.getBoundingClientRect();
|
|||
|
return {
|
|||
|
top: `${rect.bottom + window.scrollY}px`,
|
|||
|
left: `${rect.left + window.scrollX}px`
|
|||
|
};
|
|||
|
}
|
|||
|
},
|
|||
|
watch: {
|
|||
|
// 监听单位类型变化
|
|||
|
unitType: {
|
|||
|
immediate: true,
|
|||
|
async handler(newType) {
|
|||
|
if (this.userDefined) {
|
|||
|
this.textUnitName = this.userDefinedunitName;
|
|||
|
this.$nextTick(() => {
|
|||
|
this.updateInputWidth();
|
|||
|
});
|
|||
|
} else {
|
|||
|
await this.loadUnits(newType);
|
|||
|
this.initCurrentUnit();
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
// 监听外部单位序号变化
|
|||
|
unitOrder(newOrder) {
|
|||
|
if (this.userDefined) {
|
|||
|
this.textUnitName = this.userDefinedunitName;
|
|||
|
this.$nextTick(() => {
|
|||
|
this.updateInputWidth();
|
|||
|
});
|
|||
|
} else {
|
|||
|
const target = this.sortedUnits.find((u) => u.unitOrder === newOrder);
|
|||
|
if (target) this.currentUnit = target;
|
|||
|
this.textUnitName = this.showEnglishOnly ? target.unitName.split('(')[0].trim() : target.unitName;
|
|||
|
this.$nextTick(() => {
|
|||
|
this.updateInputWidth();
|
|||
|
});
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
// 监听输入值变化
|
|||
|
value(newVal) {
|
|||
|
if (!this.isInternalUpdate) {
|
|||
|
// 外部更新时重置原始值
|
|||
|
this.originalValue = newVal;
|
|||
|
this.originalUnit = this.currentUnit;
|
|||
|
this.inputValue = newVal;
|
|||
|
}
|
|||
|
this.isInternalUpdate = false;
|
|||
|
}
|
|||
|
},
|
|||
|
methods: {
|
|||
|
handleMouseMove(event) {
|
|||
|
if (this.showUnitSelector) {
|
|||
|
// 鼠标右下方偏移量
|
|||
|
const offsetX = 10;
|
|||
|
const offsetY = 10;
|
|||
|
this.unitSelectorLeft = event.clientX + offsetX;
|
|||
|
this.unitSelectorTop = event.clientY + offsetY;
|
|||
|
// 获取弹窗元素
|
|||
|
const unitSelector = this.$refs.unitSelector;
|
|||
|
if (unitSelector) {
|
|||
|
// 检查是否超出屏幕右边界
|
|||
|
if (this.unitSelectorLeft + unitSelector.offsetWidth > window.innerWidth) {
|
|||
|
this.unitSelectorLeft = window.innerWidth - unitSelector.offsetWidth;
|
|||
|
}
|
|||
|
// 检查是否超出屏幕下边界
|
|||
|
if (this.unitSelectorTop + unitSelector.offsetHeight > window.innerHeight) {
|
|||
|
this.unitSelectorTop = window.innerHeight - unitSelector.offsetHeight;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
updateInputWidth() {
|
|||
|
const spanWidth = this.$refs.unitLabel.offsetWidth - 5;
|
|||
|
this.inputWidth = this.width - spanWidth;
|
|||
|
},
|
|||
|
// 处理全局点击事件
|
|||
|
handleClickOutside(event) {
|
|||
|
if (this.$refs.unitSelector && !this.$refs.unitSelector.contains(event.target)) {
|
|||
|
this.showUnitSelector = false;
|
|||
|
}
|
|||
|
},
|
|||
|
// 切换单位选择器的显示状态
|
|||
|
toggleUnitSelector() {
|
|||
|
this.showUnitSelector = !this.showUnitSelector;
|
|||
|
},
|
|||
|
// 加载单位数据
|
|||
|
async loadUnits(unitType) {
|
|||
|
try {
|
|||
|
const res = await listSysUnitConvert({ unitType: unitType, status: 'Y' });
|
|||
|
console.log(res);
|
|||
|
this.unitData = res.rows;
|
|||
|
this.baseUnit = this.unitData.find((u) => u.baseUnit === 'Y');
|
|||
|
// console.log(this.baseUnit.unitName);
|
|||
|
} catch (e) {
|
|||
|
console.error('单位数据加载失败:', e);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
// 初始化当前单位
|
|||
|
initCurrentUnit() {
|
|||
|
const target = this.sortedUnits.find((u) => u.unitOrder === this.unitOrder);
|
|||
|
this.currentUnit = target || this.baseUnit || this.sortedUnits[0];
|
|||
|
// 初始化时设置原始单位(关键修复)
|
|||
|
this.originalUnit = this.currentUnit;
|
|||
|
this.textUnitName = this.showEnglishOnly ? target.unitName.split('(')[0].trim() : target.unitName;
|
|||
|
this.$nextTick(() => {
|
|||
|
this.updateInputWidth();
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
// 输入变化处理
|
|||
|
handleInputChange() {
|
|||
|
// 记录原始值和单位
|
|||
|
this.originalValue = this.inputValue;
|
|||
|
this.originalUnit = this.currentUnit;
|
|||
|
console.log(this.originalValue);
|
|||
|
this.$emit('input', this.inputValue);
|
|||
|
// this.convertAndEmit();
|
|||
|
},
|
|||
|
|
|||
|
// 循环切换单位
|
|||
|
// 修改单位切换方法
|
|||
|
cycleUnit() {
|
|||
|
const index = this.sortedUnits.findIndex((u) => u === this.currentUnit);
|
|||
|
const newUnit = this.sortedUnits[(index + 1) % this.sortedUnits.length];
|
|||
|
this.currentUnit = newUnit;
|
|||
|
this.textUnitName = this.showEnglishOnly ? newUnit.unitName.split('(')[0].trim() : newUnit.unitName;
|
|||
|
this.$nextTick(() => {
|
|||
|
this.updateInputWidth();
|
|||
|
this.convertAndEmit(); // 触发转换
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
// 切换单位选择器
|
|||
|
toggleUnitSelector() {
|
|||
|
this.showUnitSelector = !this.showUnitSelector;
|
|||
|
},
|
|||
|
|
|||
|
// 选择单位
|
|||
|
selectUnit(unit) {
|
|||
|
this.currentUnit = unit;
|
|||
|
this.showUnitSelector = false;
|
|||
|
this.$nextTick(() => {
|
|||
|
this.updateInputWidth();
|
|||
|
this.convertAndEmit(); // 触发转换
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
// 执行换算并提交事件
|
|||
|
convertAndEmit() {
|
|||
|
// 移除参数
|
|||
|
if (!this.currentUnit || !this.baseUnit || !this.originalUnit) return;
|
|||
|
|
|||
|
let newValue = 0;
|
|||
|
if (this.unitType === 'temperature') {
|
|||
|
newValue = this.handleTemperatureConversion(this.originalUnit, this.currentUnit);
|
|||
|
} else {
|
|||
|
// 通过基准单位进行两次精确转换
|
|||
|
const baseValue = this.originalValue / this.originalUnit.conversionFactor;
|
|||
|
newValue = baseValue * this.currentUnit.conversionFactor;
|
|||
|
}
|
|||
|
|
|||
|
const roundedValue = this.roundValue(newValue);
|
|||
|
this.isInternalUpdate = true; // 锁定更新
|
|||
|
this.inputValue = roundedValue;
|
|||
|
this.$emit('input', roundedValue);
|
|||
|
this.$emit('update:unitOrder', this.currentUnit.unitOrder);
|
|||
|
|
|||
|
this.$emit('conversion', {
|
|||
|
initialValue: this.originalValue,
|
|||
|
newValue: roundedValue,
|
|||
|
oldUnit: this.originalUnit.unitName,
|
|||
|
newUnit: this.currentUnit.unitName,
|
|||
|
oldOrder: this.originalUnit.unitOrder,
|
|||
|
newOrder: this.currentUnit.unitOrder
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
// 温度单位换算
|
|||
|
// 温度转换调整(使用原始值)
|
|||
|
handleTemperatureConversion(oldUnit, newUnit) {
|
|||
|
const oldOrder = oldUnit.unitOrder;
|
|||
|
const newOrder = newUnit.unitOrder;
|
|||
|
console.log(1111, this.originalValue);
|
|||
|
// 使用原始值计算
|
|||
|
let celsius;
|
|||
|
switch (oldOrder) {
|
|||
|
case 0:
|
|||
|
celsius = this.originalValue;
|
|||
|
break;
|
|||
|
case 1:
|
|||
|
celsius = ((this.originalValue - 32) * 5) / 9;
|
|||
|
break;
|
|||
|
case 2:
|
|||
|
celsius = this.originalValue - 273.15;
|
|||
|
break;
|
|||
|
default:
|
|||
|
throw new Error('无效温度单位');
|
|||
|
}
|
|||
|
|
|||
|
switch (newOrder) {
|
|||
|
case 0:
|
|||
|
return celsius;
|
|||
|
case 1:
|
|||
|
return (celsius * 9) / 5 + 32;
|
|||
|
case 2:
|
|||
|
return celsius + 273.15;
|
|||
|
default:
|
|||
|
throw new Error('无效温度单位');
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
// 四舍六入五成双
|
|||
|
roundValue(value) {
|
|||
|
const multiplier = Math.pow(10, this.decimalPlaces);
|
|||
|
const val = value * multiplier;
|
|||
|
const intVal = Math.trunc(val);
|
|||
|
const decimalPart = val - intVal;
|
|||
|
if (decimalPart < 0.5) {
|
|||
|
return intVal / multiplier;
|
|||
|
} else if (decimalPart > 0.5) {
|
|||
|
return (intVal + 1) / multiplier;
|
|||
|
} else {
|
|||
|
return intVal % 2 === 0 ? intVal / multiplier : (intVal + 1) / multiplier;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
</script>
|
|||
|
|
|||
|
<style scoped>
|
|||
|
.unit-converter {
|
|||
|
display: flex;
|
|||
|
align-items: center;
|
|||
|
gap: 2px;
|
|||
|
position: relative;
|
|||
|
}
|
|||
|
|
|||
|
.input-field {
|
|||
|
border: 1px solid #d6d5d5;
|
|||
|
border-radius: 4px;
|
|||
|
font-size: 14px;
|
|||
|
}
|
|||
|
|
|||
|
.unit-label {
|
|||
|
cursor: pointer;
|
|||
|
font-weight: bold;
|
|||
|
border: 1px solid #d6d5d5;
|
|||
|
border-radius: 4px;
|
|||
|
font-size: 12px;
|
|||
|
padding-left: 10px;
|
|||
|
padding-right: 5px;
|
|||
|
white-space: nowrap;
|
|||
|
}
|
|||
|
|
|||
|
.unit-label:hover {
|
|||
|
background: #f0f8ff;
|
|||
|
}
|
|||
|
|
|||
|
.unit-selector {
|
|||
|
/* 设置容器的最大高度,当内容超出这个高度时会出现滚动条 */
|
|||
|
max-height: 200px;
|
|||
|
/* 超出内容时显示纵向滚动条 */
|
|||
|
overflow-y: auto;
|
|||
|
/* 横向内容不溢出,隐藏多余部分 */
|
|||
|
overflow-x: hidden;
|
|||
|
/* 其他样式保持不变 */
|
|||
|
position: absolute;
|
|||
|
z-index: 9999;
|
|||
|
background-color: white;
|
|||
|
border: 1px solid #ccc;
|
|||
|
border-radius: 4px;
|
|||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|||
|
padding: 2px 0;
|
|||
|
min-width: 100px;
|
|||
|
list-style-type: none;
|
|||
|
margin-left: 100px;
|
|||
|
}
|
|||
|
|
|||
|
.unit-option {
|
|||
|
padding: 2px 10px;
|
|||
|
cursor: pointer;
|
|||
|
font-size: 14px;
|
|||
|
}
|
|||
|
|
|||
|
.unit-option:hover {
|
|||
|
background-color: #f0f0f0;
|
|||
|
}
|
|||
|
</style>
|