update:修改应用图标,TypeScript改为JavaScript

main
蒋尚宏 2 weeks ago
parent 673df25522
commit 1d41132a29

4
.gitignore vendored

@ -37,3 +37,7 @@ src-tauri/icons/*.icns
# Certificates (generated) # Certificates (generated)
src-tauri/certs/ src-tauri/certs/
# Unnecessary files
vc_redist*.exe
package-lock.json

@ -8,6 +8,6 @@
</head> </head>
<body class="bg-surface-50"> <body class="bg-surface-50">
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

2778
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"tauri": "tauri" "tauri": "tauri"
}, },
@ -22,9 +22,7 @@
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.0.0", "@tauri-apps/cli": "^2.0.0",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"typescript": "^5.4.2",
"vite": "^5.1.6", "vite": "^5.1.6",
"vue-tsc": "^2.0.6",
"autoprefixer": "^10.4.18", "autoprefixer": "^10.4.18",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"

@ -48,15 +48,9 @@ importers:
tailwindcss: tailwindcss:
specifier: ^3.4.1 specifier: ^3.4.1
version: 3.4.18 version: 3.4.18
typescript:
specifier: ^5.4.2
version: 5.9.3
vite: vite:
specifier: ^5.1.6 specifier: ^5.1.6
version: 5.4.21 version: 5.4.21
vue-tsc:
specifier: ^2.0.6
version: 2.2.12(typescript@5.9.3)
packages: packages:
@ -463,15 +457,6 @@ packages:
vite: ^5.0.0 || ^6.0.0 vite: ^5.0.0 || ^6.0.0
vue: ^3.2.25 vue: ^3.2.25
'@volar/language-core@2.4.15':
resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==}
'@volar/source-map@2.4.15':
resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==}
'@volar/typescript@2.4.15':
resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==}
'@vue/compiler-core@3.5.25': '@vue/compiler-core@3.5.25':
resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==}
@ -484,20 +469,9 @@ packages:
'@vue/compiler-ssr@3.5.25': '@vue/compiler-ssr@3.5.25':
resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==} resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==}
'@vue/compiler-vue2@2.7.16':
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
'@vue/devtools-api@6.6.4': '@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
'@vue/language-core@2.2.12':
resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
'@vue/reactivity@3.5.25': '@vue/reactivity@3.5.25':
resolution: {integrity: sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==} resolution: {integrity: sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==}
@ -515,9 +489,6 @@ packages:
'@vue/shared@3.5.25': '@vue/shared@3.5.25':
resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==}
alien-signals@1.0.13:
resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==}
any-promise@1.3.0: any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@ -535,9 +506,6 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.1.0 postcss: ^8.1.0
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
baseline-browser-mapping@2.8.32: baseline-browser-mapping@2.8.32:
resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==}
hasBin: true hasBin: true
@ -546,9 +514,6 @@ packages:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'} engines: {node: '>=8'}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
braces@3.0.3: braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -581,9 +546,6 @@ packages:
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
didyoumean@1.2.2: didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@ -652,10 +614,6 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
is-binary-path@2.1.0: is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -703,13 +661,6 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
mz@2.7.0: mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
@ -737,9 +688,6 @@ packages:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
path-parse@1.0.7: path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@ -926,9 +874,6 @@ packages:
terser: terser:
optional: true optional: true
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-demi@0.14.10: vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -945,12 +890,6 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.5.0 vue: ^3.5.0
vue-tsc@2.2.12:
resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
vue@3.5.25: vue@3.5.25:
resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==} resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==}
peerDependencies: peerDependencies:
@ -1205,18 +1144,6 @@ snapshots:
vite: 5.4.21 vite: 5.4.21
vue: 3.5.25(typescript@5.9.3) vue: 3.5.25(typescript@5.9.3)
'@volar/language-core@2.4.15':
dependencies:
'@volar/source-map': 2.4.15
'@volar/source-map@2.4.15': {}
'@volar/typescript@2.4.15':
dependencies:
'@volar/language-core': 2.4.15
path-browserify: 1.0.1
vscode-uri: 3.1.0
'@vue/compiler-core@3.5.25': '@vue/compiler-core@3.5.25':
dependencies: dependencies:
'@babel/parser': 7.28.5 '@babel/parser': 7.28.5
@ -1247,26 +1174,8 @@ snapshots:
'@vue/compiler-dom': 3.5.25 '@vue/compiler-dom': 3.5.25
'@vue/shared': 3.5.25 '@vue/shared': 3.5.25
'@vue/compiler-vue2@2.7.16':
dependencies:
de-indent: 1.0.2
he: 1.2.0
'@vue/devtools-api@6.6.4': {} '@vue/devtools-api@6.6.4': {}
'@vue/language-core@2.2.12(typescript@5.9.3)':
dependencies:
'@volar/language-core': 2.4.15
'@vue/compiler-dom': 3.5.25
'@vue/compiler-vue2': 2.7.16
'@vue/shared': 3.5.25
alien-signals: 1.0.13
minimatch: 9.0.5
muggle-string: 0.4.1
path-browserify: 1.0.1
optionalDependencies:
typescript: 5.9.3
'@vue/reactivity@3.5.25': '@vue/reactivity@3.5.25':
dependencies: dependencies:
'@vue/shared': 3.5.25 '@vue/shared': 3.5.25
@ -1291,8 +1200,6 @@ snapshots:
'@vue/shared@3.5.25': {} '@vue/shared@3.5.25': {}
alien-signals@1.0.13: {}
any-promise@1.3.0: {} any-promise@1.3.0: {}
anymatch@3.1.3: anymatch@3.1.3:
@ -1312,16 +1219,10 @@ snapshots:
postcss: 8.5.6 postcss: 8.5.6
postcss-value-parser: 4.2.0 postcss-value-parser: 4.2.0
balanced-match@1.0.2: {}
baseline-browser-mapping@2.8.32: {} baseline-browser-mapping@2.8.32: {}
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
braces@3.0.3: braces@3.0.3:
dependencies: dependencies:
fill-range: 7.1.1 fill-range: 7.1.1
@ -1356,8 +1257,6 @@ snapshots:
csstype@3.2.3: {} csstype@3.2.3: {}
de-indent@1.0.2: {}
didyoumean@1.2.2: {} didyoumean@1.2.2: {}
dlv@1.1.3: {} dlv@1.1.3: {}
@ -1435,8 +1334,6 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
he@1.2.0: {}
is-binary-path@2.1.0: is-binary-path@2.1.0:
dependencies: dependencies:
binary-extensions: 2.3.0 binary-extensions: 2.3.0
@ -1474,12 +1371,6 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.1
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
muggle-string@0.4.1: {}
mz@2.7.0: mz@2.7.0:
dependencies: dependencies:
any-promise: 1.3.0 any-promise: 1.3.0
@ -1498,8 +1389,6 @@ snapshots:
object-hash@3.0.0: {} object-hash@3.0.0: {}
path-browserify@1.0.1: {}
path-parse@1.0.7: {} path-parse@1.0.7: {}
picocolors@1.1.1: {} picocolors@1.1.1: {}
@ -1670,7 +1559,8 @@ snapshots:
ts-interface-checker@0.1.13: {} ts-interface-checker@0.1.13: {}
typescript@5.9.3: {} typescript@5.9.3:
optional: true
update-browserslist-db@1.1.4(browserslist@4.28.0): update-browserslist-db@1.1.4(browserslist@4.28.0):
dependencies: dependencies:
@ -1688,8 +1578,6 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
vscode-uri@3.1.0: {}
vue-demi@0.14.10(vue@3.5.25(typescript@5.9.3)): vue-demi@0.14.10(vue@3.5.25(typescript@5.9.3)):
dependencies: dependencies:
vue: 3.5.25(typescript@5.9.3) vue: 3.5.25(typescript@5.9.3)
@ -1699,12 +1587,6 @@ snapshots:
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
vue: 3.5.25(typescript@5.9.3) vue: 3.5.25(typescript@5.9.3)
vue-tsc@2.2.12(typescript@5.9.3):
dependencies:
'@volar/typescript': 2.4.15
'@vue/language-core': 2.2.12(typescript@5.9.3)
typescript: 5.9.3
vue@3.5.25(typescript@5.9.3): vue@3.5.25(typescript@5.9.3):
dependencies: dependencies:
'@vue/compiler-dom': 3.5.25 '@vue/compiler-dom': 3.5.25

@ -1,3 +1,8 @@
# Windows 静态链接 MSVC 运行时,无需用户安装 VC++ Redistributable # Windows 静态链接 MSVC 运行时,无需用户安装 VC++ Redistributable
[target.x86_64-pc-windows-msvc] [target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
# Cargo 配置
[build]
# 编译输出目录(减少项目目录占用)
target-dir = "E:/rust-target/flash-send"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 B

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 929 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 929 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 929 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 881 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 24 KiB

@ -43,7 +43,8 @@ impl DiscoveryService {
pub fn new(local_device: DeviceInfo) -> Self { pub fn new(local_device: DeviceInfo) -> Self {
let (event_tx, _) = broadcast::channel(100); let (event_tx, _) = broadcast::channel(100);
Self { Self {
port: local_device.ws_port.saturating_sub(1).max(DEFAULT_UDP_PORT), // UDP 广播端口必须固定,所有实例使用同一端口才能互相发现
port: DEFAULT_UDP_PORT,
local_device, local_device,
event_tx, event_tx,
stop_tx: None, stop_tx: None,

@ -5,7 +5,7 @@
"identifier": "com.flashsend.app", "identifier": "com.flashsend.app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:5173",
"beforeBuildCommand": "npm run build", "beforeBuildCommand": "npm run build",
"frontendDist": "../dist" "frontendDist": "../dist"
}, },
@ -28,7 +28,7 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": ["app", "nsis"],
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",

@ -1,16 +1,15 @@
<script setup lang="ts"> <script setup>
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
import { listen, UnlistenFn } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
import { useDeviceStore } from '@/stores/deviceStore' import { useDeviceStore } from '@/stores/deviceStore'
import { useChatStore } from '@/stores/chatStore' import { useChatStore } from '@/stores/chatStore'
import { EventNames } from '@/types' import { EventNames } from '@/types'
import type { DeviceInfo, ChatMessage, TransferProgressEvent } from '@/types'
import Sidebar from '@/components/Sidebar.vue' import Sidebar from '@/components/Sidebar.vue'
const deviceStore = useDeviceStore() const deviceStore = useDeviceStore()
const chatStore = useChatStore() const chatStore = useChatStore()
let unlistenFns: UnlistenFn[] = [] let unlistenFns = []
// //
function initTheme() { function initTheme() {
@ -33,42 +32,42 @@ onMounted(async () => {
// //
unlistenFns.push( unlistenFns.push(
await listen<DeviceInfo>(EventNames.DEVICE_FOUND, (event) => { await listen(EventNames.DEVICE_FOUND, (event) => {
deviceStore.addDevice(event.payload) deviceStore.addDevice(event.payload)
}) })
) )
// 线 // 线
unlistenFns.push( unlistenFns.push(
await listen<{ deviceId: string }>(EventNames.DEVICE_LOST, (event) => { await listen(EventNames.DEVICE_LOST, (event) => {
deviceStore.removeDevice(event.payload.deviceId) deviceStore.removeDevice(event.payload.deviceId)
}) })
) )
// //
unlistenFns.push( unlistenFns.push(
await listen<ChatMessage>(EventNames.MESSAGE_RECEIVED, (event) => { await listen(EventNames.MESSAGE_RECEIVED, (event) => {
chatStore.receiveMessage(event.payload) chatStore.receiveMessage(event.payload)
}) })
) )
// WebSocket // WebSocket
unlistenFns.push( unlistenFns.push(
await listen<{ deviceId: string }>(EventNames.WEBSOCKET_CONNECTED, (event) => { await listen(EventNames.WEBSOCKET_CONNECTED, (event) => {
deviceStore.setDeviceConnected(event.payload.deviceId, true) deviceStore.setDeviceConnected(event.payload.deviceId, true)
}) })
) )
// WebSocket // WebSocket
unlistenFns.push( unlistenFns.push(
await listen<{ deviceId: string }>(EventNames.WEBSOCKET_DISCONNECTED, (event) => { await listen(EventNames.WEBSOCKET_DISCONNECTED, (event) => {
deviceStore.setDeviceConnected(event.payload.deviceId, false) deviceStore.setDeviceConnected(event.payload.deviceId, false)
}) })
) )
// //
unlistenFns.push( unlistenFns.push(
await listen<TransferProgressEvent>(EventNames.TRANSFER_PROGRESS, (event) => { await listen(EventNames.TRANSFER_PROGRESS, (event) => {
chatStore.updateTransferProgress(event.payload) chatStore.updateTransferProgress(event.payload)
}) })
) )

@ -0,0 +1,89 @@
/**
* API 模块
* 封装所有 Tauri 命令调用
*/
import { invoke } from '@tauri-apps/api/core'
/** 发现服务 API */
export const discoveryApi = {
/** 启动设备发现服务 */
start: () => invoke('start_discovery_service'),
/** 停止设备发现服务 */
stop: () => invoke('stop_discovery_service'),
/** 获取已发现的设备列表 */
getDevices: () => invoke('get_discovered_devices'),
}
/** WebSocket API */
export const websocketApi = {
/** 启动 WebSocket 服务端 */
startServer: () => invoke('start_websocket_server'),
/** 停止 WebSocket 服务端 */
stopServer: () => invoke('stop_websocket_server'),
/** 连接到指定设备 */
connect: (deviceId) => invoke('connect_to_peer', { deviceId }),
/** 断开与指定设备的连接 */
disconnect: (deviceId) => invoke('disconnect_from_peer', { deviceId }),
}
/** 聊天 API */
export const chatApi = {
/** 发送聊天消息 */
sendMessage: (deviceId, content, messageType) =>
invoke('send_chat_message', { deviceId, content, messageType }),
/** 获取聊天历史 */
getHistory: (deviceId, limit, offset) =>
invoke('get_chat_history', { deviceId, limit, offset }),
/** 删除聊天历史 */
deleteHistory: (deviceId) =>
invoke('delete_chat_history', { deviceId }),
}
/** 文件传输 API */
export const fileApi = {
/** 启动 HTTP 服务端 */
startServer: () => invoke('start_http_server'),
/** 停止 HTTP 服务端 */
stopServer: () => invoke('stop_http_server'),
/** 选择要发送的文件 */
selectFile: () => invoke('select_file_to_send'),
/** 发送文件 */
sendFile: (deviceId, fileId, filePath) =>
invoke('send_file', { deviceId, fileId, filePath }),
/** 获取传输历史 */
getHistory: (deviceId, limit) =>
invoke('get_transfer_history', { deviceId, limit }),
/** 打开文件位置 */
openLocation: (path) => invoke('open_file_location', { path }),
/** 取消传输 */
cancelTransfer: (fileId) => invoke('cancel_transfer', { fileId }),
}
/** 配置 API */
export const configApi = {
/** 获取应用配置 */
get: () => invoke('get_app_config'),
/** 更新设备名称 */
updateDeviceName: (name) => invoke('update_device_name', { name }),
/** 更新下载目录 */
updateDownloadDir: (dir) => invoke('update_download_dir', { dir }),
/** 获取本机设备信息 */
getLocalDevice: () => invoke('get_local_device_info'),
}

@ -1,96 +0,0 @@
/**
* API
* Tauri
*/
import { invoke } from '@tauri-apps/api/core'
import type {
DeviceInfo,
ChatMessage,
FileTransfer,
FileMetadata,
AppConfig,
} from '@/types'
/** 发现服务 API */
export const discoveryApi = {
/** 启动设备发现服务 */
start: () => invoke('start_discovery_service'),
/** 停止设备发现服务 */
stop: () => invoke('stop_discovery_service'),
/** 获取已发现的设备列表 */
getDevices: () => invoke<DeviceInfo[]>('get_discovered_devices'),
}
/** WebSocket API */
export const websocketApi = {
/** 启动 WebSocket 服务端 */
startServer: () => invoke('start_websocket_server'),
/** 停止 WebSocket 服务端 */
stopServer: () => invoke('stop_websocket_server'),
/** 连接到指定设备 */
connect: (deviceId: string) => invoke('connect_to_peer', { deviceId }),
/** 断开与指定设备的连接 */
disconnect: (deviceId: string) => invoke('disconnect_from_peer', { deviceId }),
}
/** 聊天 API */
export const chatApi = {
/** 发送聊天消息 */
sendMessage: (deviceId: string, content: string, messageType?: string) =>
invoke<ChatMessage>('send_chat_message', { deviceId, content, messageType }),
/** 获取聊天历史 */
getHistory: (deviceId: string, limit?: number, offset?: number) =>
invoke<ChatMessage[]>('get_chat_history', { deviceId, limit, offset }),
/** 删除聊天历史 */
deleteHistory: (deviceId: string) =>
invoke<number>('delete_chat_history', { deviceId }),
}
/** 文件传输 API */
export const fileApi = {
/** 启动 HTTP 服务端 */
startServer: () => invoke('start_http_server'),
/** 停止 HTTP 服务端 */
stopServer: () => invoke('stop_http_server'),
/** 选择要发送的文件 */
selectFile: () => invoke<FileMetadata | null>('select_file_to_send'),
/** 发送文件 */
sendFile: (deviceId: string, fileId: string, filePath: string) =>
invoke<FileTransfer>('send_file', { deviceId, fileId, filePath }),
/** 获取传输历史 */
getHistory: (deviceId: string, limit?: number) =>
invoke<FileTransfer[]>('get_transfer_history', { deviceId, limit }),
/** 打开文件位置 */
openLocation: (path: string) => invoke('open_file_location', { path }),
/** 取消传输 */
cancelTransfer: (fileId: string) => invoke('cancel_transfer', { fileId }),
}
/** 配置 API */
export const configApi = {
/** 获取应用配置 */
get: () => invoke<AppConfig>('get_app_config'),
/** 更新设备名称 */
updateDeviceName: (name: string) => invoke('update_device_name', { name }),
/** 更新下载目录 */
updateDownloadDir: (dir: string) => invoke('update_download_dir', { dir }),
/** 获取本机设备信息 */
getLocalDevice: () => invoke<DeviceInfo>('get_local_device_info'),
}

@ -1,15 +1,11 @@
<script setup lang="ts"> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { Send, Paperclip, Image } from 'lucide-vue-next' import { Send, Paperclip, Image } from 'lucide-vue-next'
const emit = defineEmits<{ const emit = defineEmits(['send', 'selectFile', 'selectImage'])
(e: 'send', content: string): void
(e: 'selectFile'): void
(e: 'selectImage'): void
}>()
const inputText = ref('') const inputText = ref('')
const inputRef = ref<HTMLTextAreaElement | null>(null) const inputRef = ref(null)
// //
function sendMessage() { function sendMessage() {
@ -24,7 +20,7 @@ function sendMessage() {
} }
// //
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e) {
// Enter Shift+Enter // Enter Shift+Enter
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault() e.preventDefault()
@ -33,8 +29,8 @@ function handleKeydown(e: KeyboardEvent) {
} }
// //
function autoResize(e: Event) { function autoResize(e) {
const target = e.target as HTMLTextAreaElement const target = e.target
target.style.height = 'auto' target.style.height = 'auto'
target.style.height = Math.min(target.scrollHeight, 120) + 'px' target.style.height = Math.min(target.scrollHeight, 120) + 'px'
} }

@ -1,14 +1,16 @@
<script setup lang="ts"> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useDeviceStore } from '@/stores/deviceStore' import { useDeviceStore } from '@/stores/deviceStore'
import { useChatStore } from '@/stores/chatStore' import { useChatStore } from '@/stores/chatStore'
import { Monitor, Smartphone, Laptop, MessageSquare, Send } from 'lucide-vue-next' import { Monitor, Smartphone, Laptop, MessageSquare, Send } from 'lucide-vue-next'
import type { DeviceInfo } from '@/types'
const props = defineProps<{ const props = defineProps({
device: DeviceInfo device: {
}>() type: Object,
required: true
}
})
const router = useRouter() const router = useRouter()
const deviceStore = useDeviceStore() const deviceStore = useDeviceStore()

@ -1,20 +1,22 @@
<script setup lang="ts"> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { File, CheckCircle, XCircle, Loader } from 'lucide-vue-next' import { File, CheckCircle, XCircle, Loader } from 'lucide-vue-next'
import type { FileTransfer } from '@/types'
const props = defineProps<{ const props = defineProps({
transfer: FileTransfer transfer: {
isReceiver?: boolean // "" type: Object,
}>() required: true
},
isReceiver: {
type: Boolean,
default: false
}
})
const emit = defineEmits<{ const emit = defineEmits(['open', 'cancel'])
(e: 'open'): void
(e: 'cancel', fileId: string): void
}>()
// //
function formatSize(bytes: number): string { function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B' if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB' if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB' if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB'

@ -1,8 +1,14 @@
<script setup lang="ts"> <script setup>
defineProps<{ defineProps({
size?: 'sm' | 'md' | 'lg' size: {
text?: string type: String,
}>() default: 'md'
},
text: {
type: String,
default: ''
}
})
const sizeClasses = { const sizeClasses = {
sm: 'w-4 h-4 border-2', sm: 'w-4 h-4 border-2',
@ -14,9 +20,9 @@ const sizeClasses = {
<template> <template>
<div class="flex flex-col items-center justify-center gap-3"> <div class="flex flex-col items-center justify-center gap-3">
<div <div
class="rounded-full border-surface-200 border-t-primary-500 animate-spin" class="rounded-full border-surface-200 dark:border-gray-600 border-t-primary-500 dark:border-t-primary-400 animate-spin"
:class="sizeClasses[size || 'md']" :class="sizeClasses[size || 'md']"
/> />
<span v-if="text" class="text-sm text-gray-500">{{ text }}</span> <span v-if="text" class="text-sm text-gray-500 dark:text-gray-400">{{ text }}</span>
</div> </div>
</template> </template>

@ -1,16 +1,19 @@
<script setup lang="ts"> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { Check, CheckCheck, Clock, AlertCircle, FileText } from 'lucide-vue-next' import { Check, CheckCheck, Clock, AlertCircle, FileText } from 'lucide-vue-next'
import type { ChatMessage } from '@/types'
const props = defineProps<{ const props = defineProps({
message: ChatMessage message: {
isSent: boolean type: Object,
}>() required: true
},
isSent: {
type: Boolean,
default: false
}
})
const emit = defineEmits<{ const emit = defineEmits(['file-click'])
(e: 'file-click', fileName: string): void
}>()
// //
const formattedTime = computed(() => { const formattedTime = computed(() => {

@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup>
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useDeviceStore } from '@/stores/deviceStore' import { useDeviceStore } from '@/stores/deviceStore'
import { useChatStore } from '@/stores/chatStore' import { useChatStore } from '@/stores/chatStore'
@ -21,7 +21,7 @@ const navItems = [
{ path: '/settings', icon: Settings, label: '设置' }, { path: '/settings', icon: Settings, label: '设置' },
] ]
const isActive = (path: string) => route.path.startsWith(path) const isActive = (path) => route.path.startsWith(path)
</script> </script>
<template> <template>

@ -3,21 +3,18 @@
*/ */
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted } from 'vue'
import { listen, UnlistenFn } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
/** /**
* 监听 Tauri 事件 * 监听 Tauri 事件
* @param eventName 事件名称 * @param {string} eventName 事件名称
* @param handler 事件处理器 * @param {Function} handler 事件处理器
*/ */
export function useEventListener<T>( export function useEventListener(eventName, handler) {
eventName: string, let unlisten = null
handler: (payload: T) => void
) {
let unlisten: UnlistenFn | null = null
onMounted(async () => { onMounted(async () => {
unlisten = await listen<T>(eventName, (event) => { unlisten = await listen(eventName, (event) => {
handler(event.payload) handler(event.payload)
}) })
}) })
@ -29,12 +26,10 @@ export function useEventListener<T>(
/** /**
* 监听多个事件 * 监听多个事件
* @param events 事件配置数组 * @param {Array<{name: string, handler: Function}>} events 事件配置数组
*/ */
export function useMultipleEventListeners( export function useMultipleEventListeners(events) {
events: Array<{ name: string; handler: (payload: any) => void }> const unlisteners = []
) {
const unlisteners: UnlistenFn[] = []
onMounted(async () => { onMounted(async () => {
for (const event of events) { for (const event of events) {

@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup>
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useDeviceStore } from '@/stores/deviceStore' import { useDeviceStore } from '@/stores/deviceStore'
@ -13,14 +13,14 @@ const router = useRouter()
const deviceStore = useDeviceStore() const deviceStore = useDeviceStore()
const chatStore = useChatStore() const chatStore = useChatStore()
const deviceId = computed(() => route.params.deviceId as string) const deviceId = computed(() => route.params.deviceId)
const device = computed(() => deviceStore.getDevice(deviceId.value)) const device = computed(() => deviceStore.getDevice(deviceId.value))
const messages = computed(() => chatStore.messages.get(deviceId.value) || []) const messages = computed(() => chatStore.messages.get(deviceId.value) || [])
const localDeviceId = computed(() => deviceStore.localDevice?.deviceId || '') const localDeviceId = computed(() => deviceStore.localDevice?.deviceId || '')
// //
const deviceTransfers = computed(() => { const deviceTransfers = computed(() => {
const result: [string, typeof chatStore.transfers extends Map<string, infer V> ? V : never][] = [] const result = []
chatStore.transfers.forEach((transfer, id) => { chatStore.transfers.forEach((transfer, id) => {
// //
const isRelevant = const isRelevant =
@ -35,7 +35,7 @@ const deviceTransfers = computed(() => {
return result return result
}) })
const messagesContainer = ref<HTMLElement | null>(null) const messagesContainer = ref(null)
const showMenu = ref(false) const showMenu = ref(false)
// //
@ -48,7 +48,7 @@ function scrollToBottom() {
} }
// //
async function sendMessage(content: string) { async function sendMessage(content) {
try { try {
await chatStore.sendMessage(deviceId.value, content) await chatStore.sendMessage(deviceId.value, content)
scrollToBottom() scrollToBottom()
@ -91,7 +91,7 @@ function goBack() {
} }
// //
async function handleFileClick(fileName: string) { async function handleFileClick(fileName) {
// //
for (const [, transfer] of chatStore.transfers) { for (const [, transfer] of chatStore.transfers) {
// toDevice // toDevice

@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useDeviceStore } from '@/stores/deviceStore' import { useDeviceStore } from '@/stores/deviceStore'
import DeviceCard from '@/components/DeviceCard.vue' import DeviceCard from '@/components/DeviceCard.vue'
@ -71,7 +71,7 @@ async function refresh() {
v-if="deviceStore.isDiscovering && deviceStore.deviceCount === 0" v-if="deviceStore.isDiscovering && deviceStore.deviceCount === 0"
class="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500" class="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500"
> >
<div class="w-16 h-16 border-4 border-gray-200 dark:border-gray-700 border-t-primary-500 rounded-full animate-spin mb-4" /> <div class="w-16 h-16 border-4 border-gray-200 dark:border-gray-600 border-t-primary-500 dark:border-t-primary-400 rounded-full animate-spin mb-4" />
<p>正在搜索附近设备...</p> <p>正在搜索附近设备...</p>
</div> </div>

@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useChatStore } from '@/stores/chatStore' import { useChatStore } from '@/stores/chatStore'
import { useDeviceStore } from '@/stores/deviceStore' import { useDeviceStore } from '@/stores/deviceStore'
@ -8,7 +8,7 @@ import { File } from 'lucide-vue-next'
const chatStore = useChatStore() const chatStore = useChatStore()
const deviceStore = useDeviceStore() const deviceStore = useDeviceStore()
const activeTab = ref<'all' | 'sending' | 'receiving'>('all') const activeTab = ref('all')
const localDeviceId = computed(() => deviceStore.localDevice?.deviceId || '') const localDeviceId = computed(() => deviceStore.localDevice?.deviceId || '')
// //
@ -41,12 +41,12 @@ const stats = computed(() => {
}) })
// //
function openLocation(path: string) { function openLocation(path) {
chatStore.openFileLocation(path) chatStore.openFileLocation(path)
} }
// //
async function cancelTransfer(fileId: string) { async function cancelTransfer(fileId) {
await chatStore.cancelTransfer(fileId) await chatStore.cancelTransfer(fileId)
} }
@ -93,7 +93,7 @@ onMounted(async () => {
{ key: 'receiving', label: '接收' }, { key: 'receiving', label: '接收' },
]" ]"
:key="tab.key" :key="tab.key"
@click="activeTab = tab.key as any" @click="activeTab = tab.key"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors" class="px-4 py-2 rounded-lg text-sm font-medium transition-colors"
:class="[ :class="[
activeTab === tab.key activeTab === tab.key

@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup>
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { useDeviceStore } from '@/stores/deviceStore' import { useDeviceStore } from '@/stores/deviceStore'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
@ -20,12 +20,11 @@ const isEditingName = ref(false)
const editedName = ref('') const editedName = ref('')
const isSaving = ref(false) const isSaving = ref(false)
// // ('system' | 'light' | 'dark')
type ThemeMode = 'system' | 'light' | 'dark' const themeMode = ref(localStorage.getItem('theme') || 'system')
const themeMode = ref<ThemeMode>((localStorage.getItem('theme') as ThemeMode) || 'system')
// //
function applyTheme(mode: ThemeMode) { function applyTheme(mode) {
const root = document.documentElement const root = document.documentElement
if (mode === 'system') { if (mode === 'system') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
@ -36,7 +35,7 @@ function applyTheme(mode: ThemeMode) {
} }
// //
function setTheme(mode: ThemeMode) { function setTheme(mode) {
themeMode.value = mode themeMode.value = mode
localStorage.setItem('theme', mode) localStorage.setItem('theme', mode)
applyTheme(mode) applyTheme(mode)
@ -73,7 +72,7 @@ async function selectDownloadDir() {
}) })
if (selected) { if (selected) {
await deviceStore.updateDownloadDir(selected as string) await deviceStore.updateDownloadDir(selected)
} }
} }
@ -266,23 +265,9 @@ onMounted(() => {
<span class="text-gray-900 dark:text-white">1.0.0</span> <span class="text-gray-900 dark:text-white">1.0.0</span>
</div> </div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-400">技术栈</span>
<span class="text-gray-900 dark:text-white">Tauri 2 + Vue 3 + Rust</span>
</div>
</div> </div>
</section> </section>
<!-- 安全提示 -->
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-xl flex gap-3">
<Shield class="text-blue-500 dark:text-blue-400 flex-shrink-0" :size="20" />
<div>
<p class="text-sm text-blue-800 dark:text-blue-300 font-medium">安全通信</p>
<p class="text-sm text-blue-600 dark:text-blue-400 mt-1">
所有通信均使用 TLS 加密自签名证书在首次启动时自动生成并持久化存储
</p>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>

@ -5,15 +5,14 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { chatApi, fileApi } from '@/api' import { chatApi, fileApi } from '@/api'
import type { ChatMessage, FileTransfer, FileMetadata, TransferProgressEvent } from '@/types'
export const useChatStore = defineStore('chat', () => { export const useChatStore = defineStore('chat', () => {
// 状态 // 状态
const messages = ref<Map<string, ChatMessage[]>>(new Map()) const messages = ref(new Map())
const transfers = ref<Map<string, FileTransfer>>(new Map()) const transfers = ref(new Map())
const currentDeviceId = ref<string | null>(null) const currentDeviceId = ref(null)
const pendingFile = ref<FileMetadata | null>(null) const pendingFile = ref(null)
const unreadCounts = ref<Map<string, number>>(new Map()) const unreadCounts = ref(new Map())
// 计算属性 // 计算属性
const currentMessages = computed(() => { const currentMessages = computed(() => {
@ -28,7 +27,7 @@ export const useChatStore = defineStore('chat', () => {
}) })
// 设置当前聊天设备 // 设置当前聊天设备
function setCurrentDevice(deviceId: string | null) { function setCurrentDevice(deviceId) {
currentDeviceId.value = deviceId currentDeviceId.value = deviceId
if (deviceId) { if (deviceId) {
// 清除未读计数 // 清除未读计数
@ -37,7 +36,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 加载聊天历史 // 加载聊天历史
async function loadHistory(deviceId: string) { async function loadHistory(deviceId) {
try { try {
const history = await chatApi.getHistory(deviceId, 100, 0) const history = await chatApi.getHistory(deviceId, 100, 0)
// 按时间正序排列 // 按时间正序排列
@ -49,7 +48,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 发送消息 // 发送消息
async function sendMessage(deviceId: string, content: string, type?: string) { async function sendMessage(deviceId, content, type) {
try { try {
const message = await chatApi.sendMessage(deviceId, content, type) const message = await chatApi.sendMessage(deviceId, content, type)
@ -66,7 +65,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 接收消息 // 接收消息
function receiveMessage(message: ChatMessage) { function receiveMessage(message) {
const deviceId = message.from const deviceId = message.from
const deviceMessages = messages.value.get(deviceId) || [] const deviceMessages = messages.value.get(deviceId) || []
@ -84,7 +83,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 删除聊天历史 // 删除聊天历史
async function deleteHistory(deviceId: string) { async function deleteHistory(deviceId) {
try { try {
await chatApi.deleteHistory(deviceId) await chatApi.deleteHistory(deviceId)
messages.value.delete(deviceId) messages.value.delete(deviceId)
@ -96,7 +95,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 选择文件 // 选择文件
async function selectFile(): Promise<FileMetadata | null> { async function selectFile() {
try { try {
const file = await fileApi.selectFile() const file = await fileApi.selectFile()
pendingFile.value = file pendingFile.value = file
@ -108,7 +107,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 发送文件 // 发送文件
async function sendFile(deviceId: string, file: FileMetadata) { async function sendFile(deviceId, file) {
try { try {
const transfer = await fileApi.sendFile(deviceId, file.fileId, file.path || '') const transfer = await fileApi.sendFile(deviceId, file.fileId, file.path || '')
transfers.value.set(transfer.fileId, transfer) transfers.value.set(transfer.fileId, transfer)
@ -120,7 +119,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 更新传输进度 // 更新传输进度
function updateTransferProgress(event: TransferProgressEvent) { function updateTransferProgress(event) {
let transfer = transfers.value.get(event.fileId) let transfer = transfers.value.get(event.fileId)
if (transfer) { if (transfer) {
transfer.progress = event.progress transfer.progress = event.progress
@ -148,7 +147,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 加载传输历史 // 加载传输历史
async function loadTransferHistory(deviceId: string, force = false) { async function loadTransferHistory(deviceId, force = false) {
try { try {
const history = await fileApi.getHistory(deviceId, 50) const history = await fileApi.getHistory(deviceId, 50)
history.forEach(transfer => { history.forEach(transfer => {
@ -168,7 +167,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 打开文件位置 // 打开文件位置
async function openFileLocation(path: string) { async function openFileLocation(path) {
try { try {
await fileApi.openLocation(path) await fileApi.openLocation(path)
} catch (error) { } catch (error) {
@ -177,7 +176,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 取消传输 // 取消传输
async function cancelTransfer(fileId: string) { async function cancelTransfer(fileId) {
try { try {
await fileApi.cancelTransfer(fileId) await fileApi.cancelTransfer(fileId)
const transfer = transfers.value.get(fileId) const transfer = transfers.value.get(fileId)
@ -190,7 +189,7 @@ export const useChatStore = defineStore('chat', () => {
} }
// 获取设备的未读消息数 // 获取设备的未读消息数
function getUnreadCount(deviceId: string): number { function getUnreadCount(deviceId) {
return unreadCounts.value.get(deviceId) || 0 return unreadCounts.value.get(deviceId) || 0
} }

@ -5,16 +5,15 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { discoveryApi, websocketApi, fileApi, configApi } from '@/api' import { discoveryApi, websocketApi, fileApi, configApi } from '@/api'
import type { DeviceInfo, AppConfig } from '@/types'
export const useDeviceStore = defineStore('device', () => { export const useDeviceStore = defineStore('device', () => {
// 状态 // 状态
const devices = ref<Map<string, DeviceInfo>>(new Map()) const devices = ref(new Map())
const localDevice = ref<DeviceInfo | null>(null) const localDevice = ref(null)
const config = ref<AppConfig | null>(null) const config = ref(null)
const isDiscovering = ref(false) const isDiscovering = ref(false)
const isServerRunning = ref(false) const isServerRunning = ref(false)
const connectedDevices = ref<Set<string>>(new Set()) const connectedDevices = ref(new Set())
// 计算属性 // 计算属性
const deviceList = computed(() => Array.from(devices.value.values())) const deviceList = computed(() => Array.from(devices.value.values()))
@ -79,23 +78,23 @@ export const useDeviceStore = defineStore('device', () => {
} }
// 添加设备 // 添加设备
function addDevice(device: DeviceInfo) { function addDevice(device) {
devices.value.set(device.deviceId, device) devices.value.set(device.deviceId, device)
} }
// 移除设备 // 移除设备
function removeDevice(deviceId: string) { function removeDevice(deviceId) {
devices.value.delete(deviceId) devices.value.delete(deviceId)
connectedDevices.value.delete(deviceId) connectedDevices.value.delete(deviceId)
} }
// 获取设备 // 获取设备
function getDevice(deviceId: string): DeviceInfo | undefined { function getDevice(deviceId) {
return devices.value.get(deviceId) return devices.value.get(deviceId)
} }
// 设置设备连接状态 // 设置设备连接状态
function setDeviceConnected(deviceId: string, connected: boolean) { function setDeviceConnected(deviceId, connected) {
if (connected) { if (connected) {
connectedDevices.value.add(deviceId) connectedDevices.value.add(deviceId)
} else { } else {
@ -109,12 +108,12 @@ export const useDeviceStore = defineStore('device', () => {
} }
// 检查设备是否已连接 // 检查设备是否已连接
function isConnected(deviceId: string): boolean { function isConnected(deviceId) {
return connectedDevices.value.has(deviceId) return connectedDevices.value.has(deviceId)
} }
// 连接到设备 // 连接到设备
async function connectToDevice(deviceId: string) { async function connectToDevice(deviceId) {
try { try {
await websocketApi.connect(deviceId) await websocketApi.connect(deviceId)
setDeviceConnected(deviceId, true) setDeviceConnected(deviceId, true)
@ -125,7 +124,7 @@ export const useDeviceStore = defineStore('device', () => {
} }
// 断开设备连接 // 断开设备连接
async function disconnectFromDevice(deviceId: string) { async function disconnectFromDevice(deviceId) {
try { try {
await websocketApi.disconnect(deviceId) await websocketApi.disconnect(deviceId)
setDeviceConnected(deviceId, false) setDeviceConnected(deviceId, false)
@ -135,7 +134,7 @@ export const useDeviceStore = defineStore('device', () => {
} }
// 更新设备名称 // 更新设备名称
async function updateDeviceName(name: string) { async function updateDeviceName(name) {
await configApi.updateDeviceName(name) await configApi.updateDeviceName(name)
if (localDevice.value) { if (localDevice.value) {
localDevice.value.deviceName = name localDevice.value.deviceName = name
@ -146,7 +145,7 @@ export const useDeviceStore = defineStore('device', () => {
} }
// 更新下载目录 // 更新下载目录
async function updateDownloadDir(dir: string) { async function updateDownloadDir(dir) {
await configApi.updateDownloadDir(dir) await configApi.updateDownloadDir(dir)
if (config.value) { if (config.value) {
config.value.downloadDir = dir config.value.downloadDir = dir

@ -0,0 +1,110 @@
/**
* 类型定义模块
* JavaScript 版本 - 使用 JSDoc 注释提供类型提示
*/
/**
* @typedef {'text' | 'image' | 'file'} DeviceCapability
*/
/**
* @typedef {Object} DeviceInfo
* @property {string} deviceId
* @property {string} deviceName
* @property {string} ip
* @property {number} wsPort
* @property {number} httpPort
* @property {boolean} online
* @property {DeviceCapability[]} capabilities
* @property {number} lastSeen
*/
/**
* @typedef {'text' | 'image' | 'file' | 'event' | 'ack' | 'ping' | 'pong'} MessageType
*/
/**
* @typedef {'pending' | 'sent' | 'delivered' | 'read' | 'failed'} MessageStatus
*/
/**
* @typedef {Object} ChatMessage
* @property {string} id
* @property {MessageType} type
* @property {string} content
* @property {string} from
* @property {string} to
* @property {number} timestamp
* @property {MessageStatus} status
*/
/**
* @typedef {'pending' | 'transferring' | 'completed' | 'failed' | 'cancelled'} TransferStatus
*/
/**
* @typedef {Object} FileTransfer
* @property {string} fileId
* @property {string} name
* @property {number} size
* @property {number} progress
* @property {TransferStatus} status
* @property {string} mimeType
* @property {string} fromDevice
* @property {string} toDevice
* @property {string} [localPath]
* @property {number} createdAt
* @property {number} [completedAt]
* @property {number} transferredBytes
*/
/**
* @typedef {Object} FileMetadata
* @property {string} fileId
* @property {string} name
* @property {number} size
* @property {string} mimeType
* @property {string} [thumbnail]
* @property {string} [path]
*/
/**
* @typedef {Object} AppConfig
* @property {string} deviceId
* @property {string} deviceName
* @property {number} udpPort
* @property {number} wsPort
* @property {number} httpPort
* @property {string} downloadDir
* @property {boolean} autoAcceptFiles
* @property {number} maxFileSize
*/
/**
* @typedef {Object} TransferProgressEvent
* @property {string} fileId
* @property {number} progress
* @property {number} transferredBytes
* @property {number} totalBytes
* @property {TransferStatus} status
* @property {string} [fileName]
* @property {string} [localPath]
*/
/**
* @typedef {Object} CommandError
* @property {string} code
* @property {string} message
*/
/** 事件名称 */
export const EventNames = {
DEVICE_FOUND: 'device_found',
DEVICE_LOST: 'device_lost',
MESSAGE_RECEIVED: 'message_received',
FILE_RECEIVED: 'file_received',
TRANSFER_PROGRESS: 'transfer_progress',
WEBSOCKET_CONNECTED: 'websocket_connected',
WEBSOCKET_DISCONNECTED: 'websocket_disconnected',
MESSAGE_STATUS_UPDATED: 'message_status_updated',
}

@ -1,106 +0,0 @@
/**
*
* 使 TypeScript
*/
/** 设备能力 */
export type DeviceCapability = 'text' | 'image' | 'file'
/** 设备信息 */
export interface DeviceInfo {
deviceId: string
deviceName: string
ip: string
wsPort: number
httpPort: number
online: boolean
capabilities: DeviceCapability[]
lastSeen: number
}
/** 消息类型 */
export type MessageType = 'text' | 'image' | 'file' | 'event' | 'ack' | 'ping' | 'pong'
/** 消息状态 */
export type MessageStatus = 'pending' | 'sent' | 'delivered' | 'read' | 'failed'
/** 聊天消息 */
export interface ChatMessage {
id: string
type: MessageType
content: string
from: string
to: string
timestamp: number
status: MessageStatus
}
/** 文件传输状态 */
export type TransferStatus = 'pending' | 'transferring' | 'completed' | 'failed' | 'cancelled'
/** 文件传输信息 */
export interface FileTransfer {
fileId: string
name: string
size: number
progress: number
status: TransferStatus
mimeType: string
fromDevice: string
toDevice: string
localPath?: string
createdAt: number
completedAt?: number
transferredBytes: number
}
/** 文件元数据 */
export interface FileMetadata {
fileId: string
name: string
size: number
mimeType: string
thumbnail?: string
path?: string // 文件路径
}
/** 应用配置 */
export interface AppConfig {
deviceId: string
deviceName: string
udpPort: number
wsPort: number
httpPort: number
downloadDir: string
autoAcceptFiles: boolean
maxFileSize: number
}
/** 传输进度事件 */
export interface TransferProgressEvent {
fileId: string
progress: number
transferredBytes: number
totalBytes: number
status: TransferStatus
fileName?: string // 文件名(接收方有)
localPath?: string // 本地路径(接收方有)
}
/** 命令错误 */
export interface CommandError {
code: string
message: string
}
/** 事件名称 */
export const EventNames = {
DEVICE_FOUND: 'device_found',
DEVICE_LOST: 'device_lost',
MESSAGE_RECEIVED: 'message_received',
FILE_RECEIVED: 'file_received',
TRANSFER_PROGRESS: 'transfer_progress',
WEBSOCKET_CONNECTED: 'websocket_connected',
WEBSOCKET_DISCONNECTED: 'websocket_disconnected',
MESSAGE_STATUS_UPDATED: 'message_status_updated',
} as const

7
src/vite-env.d.ts vendored

@ -1,7 +0,0 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

@ -1,25 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

Binary file not shown.

@ -1,6 +1,15 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { resolve } from 'path' import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
// ES Module 中获取 __dirname
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// 从环境变量读取端口偏移(用于多实例测试)
const portOffset = parseInt(process.env.FLASH_SEND_PORT_OFFSET || '0', 10)
const devPort = 5173 + portOffset
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@ -13,7 +22,7 @@ export default defineConfig({
// Tauri 开发服务器配置 // Tauri 开发服务器配置
clearScreen: false, clearScreen: false,
server: { server: {
port: 1420, port: devPort,
strictPort: true, strictPort: true,
watch: { watch: {
ignored: ['**/src-tauri/**'], ignored: ['**/src-tauri/**'],
Loading…
Cancel
Save