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'] },