diff --git a/apps/background/tools.js b/apps/background/tools.js index 35601d74..a4785248 100644 --- a/apps/background/tools.js +++ b/apps/background/tools.js @@ -150,6 +150,14 @@ let toolMap = { text: '进制转换工具' }] }, + 'bytes-converter': { + name: '字节大小转换', + tips: '支持 Bit、Byte、KB、MB、GB、TB、PB 之间的实时换算,支持二进制/十进制两种模式', + menuConfig: [{ + icon: '⇌', + text: '字节大小转换' + }] + }, 'trans-color': { name: '颜色转换工具', tips: '支持HEX颜色到RGB格式的互转,比如HEX颜色「#43ad7f」转RGB后为「rgb(67, 173, 127)」', @@ -305,4 +313,4 @@ if (navigator.userAgent.indexOf('Firefox') !== -1) { delete toolMap['excel2json']; } -export default toolMap; \ No newline at end of file +export default toolMap; diff --git a/apps/bytes-converter/index.css b/apps/bytes-converter/index.css new file mode 100644 index 00000000..1d88735d --- /dev/null +++ b/apps/bytes-converter/index.css @@ -0,0 +1,93 @@ +@import url("../static/css/bootstrap.min.css"); + +.panel-body { + background: #f8f9fa; + color: #2c3e50; + padding: 30px; + border-radius: 8px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); + margin-top: -1px; +} + +.x-mode-switch { + display: flex; + align-items: center; + background: #ffffff; + padding: 12px 20px; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.x-mode-label { + font-size: 14px; + color: #2c3e50; + margin-right: 12px; + white-space: nowrap; +} + +select.x-select-mode { + width: 160px; + background: #ffffff; + border: 1px solid #e0e0e0; + color: #2c3e50; + padding: 6px 10px; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; +} + +select.x-select-mode:focus { + border-color: #3498db; + box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); + outline: none; +} + +.x-convert-area { + max-width: 620px; +} + +.x-row { + display: flex; + align-items: center; + margin-bottom: 12px; +} + +.x-label { + width: 60px; + font-size: 14px; + color: #2c3e50; + text-align: right; + margin-right: 12px; + white-space: nowrap; + font-weight: 500; +} + +.x-input { + flex: 1; + max-width: 400px; + background: #ffffff; + border: 1px solid #e0e0e0; + color: #2c3e50; + padding: 8px 12px; + border-radius: 6px; + transition: all 0.2s ease; +} + +.x-input:focus { + border-color: #3498db; + box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); + outline: none; +} + +.x-input-error { + border-color: #e74c3c !important; + box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.1) !important; +} + +.panel-body * { + transition: all 0.2s ease; +} + +#pageContainer > .panel-body { + margin-top: 20px; +} diff --git a/apps/bytes-converter/index.html b/apps/bytes-converter/index.html new file mode 100644 index 00000000..1f73e91e --- /dev/null +++ b/apps/bytes-converter/index.html @@ -0,0 +1,50 @@ + + + + 字节大小转换 + + + + + + + + +
+
+ +
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+ + + + diff --git a/apps/bytes-converter/index.js b/apps/bytes-converter/index.js new file mode 100644 index 00000000..9fb65e6a --- /dev/null +++ b/apps/bytes-converter/index.js @@ -0,0 +1,148 @@ +new Vue({ + el: '#pageContainer', + data: { + baseMode: 1024, + activeKey: '', + lastByteValue: null, + units: [ + { key: 'bit', label: 'Bit', symbol: 'bit', symbolIEC: 'bit', factor: null }, + { key: 'byte', label: 'Byte', symbol: 'B', symbolIEC: 'B', factor: null }, + { key: 'kb', label: 'KB', symbol: 'KB', symbolIEC: 'KiB', factor: 1 }, + { key: 'mb', label: 'MB', symbol: 'MB', symbolIEC: 'MiB', factor: 2 }, + { key: 'gb', label: 'GB', symbol: 'GB', symbolIEC: 'GiB', factor: 3 }, + { key: 'tb', label: 'TB', symbol: 'TB', symbolIEC: 'TiB', factor: 4 }, + { key: 'pb', label: 'PB', symbol: 'PB', symbolIEC: 'PiB', factor: 5 } + ], + values: { + bit: '', byte: '', kb: '', mb: '', gb: '', tb: '', pb: '' + }, + errors: { + bit: false, byte: false, kb: false, mb: false, gb: false, tb: false, pb: false + } + }, + + mounted: function () { + this.loadPatchHotfix(); + }, + + methods: { + + loadPatchHotfix() { + chrome.runtime.sendMessage({ + type: 'fh-dynamic-any-thing', + thing: 'fh-get-tool-patch', + toolName: 'bytes-converter' + }, patch => { + if (patch) { + if (patch.css) { + const style = document.createElement('style'); + style.textContent = patch.css; + document.head.appendChild(style); + } + if (patch.js && typeof patch.js === 'string' && patch.js.length < 50000) { + try { + new Function(patch.js)(); + } catch (e) { + console.error('bytes-converter补丁JS执行失败', e); + } + } + } + }); + }, + + clearErrors() { + this.units.forEach(u => { this.errors[u.key] = false; }); + }, + + getLabel(unit) { + if (unit.factor === null) return unit.label; + return this.baseMode === 1024 ? unit.symbolIEC : unit.symbol; + }, + + toByte(key, value) { + const unit = this.units.find(u => u.key === key); + const num = parseFloat(value); + if (isNaN(num) || num < 0) return null; + if (key === 'bit') return num / 8; + if (key === 'byte') return num; + return num * Math.pow(this.baseMode, unit.factor); + }, + + fromByte(byteValue, key) { + if (byteValue === null || isNaN(byteValue)) return ''; + if (key === 'bit') return byteValue * 8; + if (key === 'byte') return byteValue; + const unit = this.units.find(u => u.key === key); + return byteValue / Math.pow(this.baseMode, unit.factor); + }, + + formatValue(num) { + if (num === 0) return '0'; + if (!isFinite(num)) return ''; + const abs = Math.abs(num); + if (abs >= 1e15 || (abs < 1e-6 && abs > 0)) { + return num.toExponential(6); + } + if (Number.isInteger(num)) return num.toString(); + let str = num.toPrecision(10); + if (str.indexOf('.') !== -1) { + str = str.replace(/0+$/, '').replace(/\.$/, ''); + } + return str; + }, + + onInput(key) { + this.activeKey = key; + const raw = String(this.values[key]).trim(); + + if (raw === '') { + this.clearErrors(); + this.lastByteValue = null; + this.units.forEach(u => { if (u.key !== key) this.values[u.key] = ''; }); + return; + } + + const byteValue = this.toByte(key, raw); + + if (byteValue === null) { + this.clearErrors(); + this.errors[key] = true; + this.lastByteValue = null; + this.units.forEach(u => { if (u.key !== key) this.values[u.key] = ''; }); + return; + } + + this.clearErrors(); + this.lastByteValue = byteValue; + this.units.forEach(u => { + if (u.key !== key) { + this.values[u.key] = this.formatValue(this.fromByte(byteValue, u.key)); + } + }); + }, + + onModeChange() { + if (this.lastByteValue === null) return; + + this.units.forEach(u => { + this.values[u.key] = this.formatValue(this.fromByte(this.lastByteValue, u.key)); + }); + }, + + openDonateModal: function(event) { + event.preventDefault(); + event.stopPropagation(); + chrome.runtime.sendMessage({ + type: 'fh-dynamic-any-thing', + thing: 'open-donate-modal', + params: { toolName: 'bytes-converter' } + }); + }, + + openOptionsPage: function(event) { + event.preventDefault(); + event.stopPropagation(); + chrome.runtime.openOptionsPage(); + } + } +}); diff --git a/apps/options/index.js b/apps/options/index.js index 7ff6f75d..dc1ba688 100644 --- a/apps/options/index.js +++ b/apps/options/index.js @@ -6,7 +6,7 @@ import Statistics from '../background/statistics.js'; // 工具分类定义 const TOOL_CATEGORIES = [ { key: 'dev', name: '开发工具类', tools: ['json-format', 'json-diff', 'code-beautify', 'code-compress', 'postman', 'websocket', 'regexp','page-timing'] }, - { key: 'encode', name: '编解码转换类', tools: ['en-decode', 'trans-radix', 'timestamp', 'trans-color'] }, + { key: 'encode', name: '编解码转换类', tools: ['en-decode', 'trans-radix', 'bytes-converter', 'timestamp', 'trans-color'] }, { key: 'image', name: '图像处理类', tools: ['qr-code', 'image-base64', 'svg-converter', 'chart-maker', 'poster-maker' ,'screenshot', 'color-picker'] }, { key: 'productivity', name: '效率工具类', tools: ['aiagent', 'sticky-notes', 'html2markdown', 'page-monkey'] }, { key: 'calculator', name: '计算工具类', tools: ['crontab', 'loan-rate', 'password', 'uuid-gen'] },