新增i18n
This commit is contained in:
parent
a0a2a62d24
commit
71f4a91c3c
20
src/i18n/index.ts
Normal file
20
src/i18n/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { createI18n } from "vue-i18n";
|
||||||
|
import zhCN from "./locales/zh-CN";
|
||||||
|
import enUS from "./locales/en-US";
|
||||||
|
import jaJP from "./locales/ja-JP";
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
"zh-CN": zhCN,
|
||||||
|
"en-US": enUS,
|
||||||
|
"ja-JP": jaJP,
|
||||||
|
};
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
locale: 'zh-CN',
|
||||||
|
fallbackLocale: "zh-CN",
|
||||||
|
messages,
|
||||||
|
legacy: false,
|
||||||
|
globalInjection: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
219
src/i18n/locales/en-US.ts
Normal file
219
src/i18n/locales/en-US.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
export default {
|
||||||
|
common: {
|
||||||
|
confirm: 'Confirm',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
copy: 'Copy',
|
||||||
|
delete: 'Delete',
|
||||||
|
create: 'Create',
|
||||||
|
save: 'Save',
|
||||||
|
edit: 'Edit',
|
||||||
|
close: 'Close',
|
||||||
|
rename: 'Rename',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
download: 'Download',
|
||||||
|
send: 'Send',
|
||||||
|
receive: 'Receive',
|
||||||
|
share: 'Share',
|
||||||
|
connect: 'Connect',
|
||||||
|
connected: 'Connected',
|
||||||
|
connecting: 'Connecting...',
|
||||||
|
disconnect: 'Disconnect',
|
||||||
|
loading: 'Loading...',
|
||||||
|
noContent: 'No Content',
|
||||||
|
success: 'Success',
|
||||||
|
failed: 'Failed',
|
||||||
|
error: 'Error',
|
||||||
|
unknownError: 'Unknown Error',
|
||||||
|
pleaseWait: 'Please wait...',
|
||||||
|
inputPlaceholder: 'Please enter',
|
||||||
|
search: 'Search',
|
||||||
|
okText: 'OK',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
warning: 'Warning',
|
||||||
|
createSuccess: 'Created successfully',
|
||||||
|
deleteSuccess: 'Deleted successfully',
|
||||||
|
renameSuccess: 'Renamed successfully',
|
||||||
|
copySuccess: 'Copied successfully',
|
||||||
|
operationFailed: 'Operation failed',
|
||||||
|
},
|
||||||
|
|
||||||
|
fileView: {
|
||||||
|
parentDir: 'Parent Directory',
|
||||||
|
sortByName: 'By Name',
|
||||||
|
sortBySize: 'By Size',
|
||||||
|
sortByType: 'By Type',
|
||||||
|
sortByDate: 'By Date',
|
||||||
|
permissionSettings: 'Permission Settings',
|
||||||
|
initialize: 'Initialize',
|
||||||
|
selectDirHint: 'Please select a directory',
|
||||||
|
selectDirectory: 'Select Directory',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
newFolder: 'New Folder',
|
||||||
|
newFile: 'New File',
|
||||||
|
delete: 'Delete',
|
||||||
|
rename: 'Rename',
|
||||||
|
download: 'Download',
|
||||||
|
selectLocalDir: 'Select Local Directory',
|
||||||
|
confirmDelete: 'Confirm Delete',
|
||||||
|
deleteConfirmContent: 'Are you sure to delete the following files?<br>{files}<br>Total {count} file(s)',
|
||||||
|
renameTitle: 'Rename',
|
||||||
|
inputNewName: 'Please enter a new name',
|
||||||
|
inputFolderName: 'Please enter folder name',
|
||||||
|
inputFileName: 'Please enter file name',
|
||||||
|
createFolderTitle: 'New Folder',
|
||||||
|
createFileTitle: 'New File',
|
||||||
|
warningInputFolderName: 'Please enter folder name',
|
||||||
|
warningInputFileName: 'Please enter file name',
|
||||||
|
deleteBtn: 'Delete',
|
||||||
|
cancelBtn: 'Cancel',
|
||||||
|
createBtn: 'Create',
|
||||||
|
},
|
||||||
|
|
||||||
|
fileReader: {
|
||||||
|
remoteTransfer: 'Remote Transfer',
|
||||||
|
browserDownload: 'Browser Download',
|
||||||
|
save: 'Save',
|
||||||
|
edit: 'Edit',
|
||||||
|
close: 'Close',
|
||||||
|
autoDetect: 'Auto Detect',
|
||||||
|
other: 'Other',
|
||||||
|
videoNotSupported: 'Your browser does not support video playback',
|
||||||
|
audioNotSupported: 'Your browser does not support audio playback',
|
||||||
|
fileTooLarge: 'File too large to preview, please transfer to view',
|
||||||
|
unsupportedType: 'Preview not supported for this file type',
|
||||||
|
openAsText: 'Open as Text',
|
||||||
|
downloadTitle: 'Download File',
|
||||||
|
downloadConfirm: 'Are you sure to download {name}?',
|
||||||
|
saveFailed: 'Failed to save file',
|
||||||
|
editor: 'Editor',
|
||||||
|
preview: 'Preview',
|
||||||
|
},
|
||||||
|
|
||||||
|
index: {
|
||||||
|
myId: 'My ID',
|
||||||
|
waitingForConnection: 'Waiting for connection...',
|
||||||
|
copy: 'Copy',
|
||||||
|
share: 'Share',
|
||||||
|
connectedBy: 'Connected By',
|
||||||
|
connected: 'Connected',
|
||||||
|
traffic: 'Traffic',
|
||||||
|
packets: 'Packets',
|
||||||
|
enterPeerId: 'Enter peer ID',
|
||||||
|
connect: 'Connect',
|
||||||
|
connectedBtn: 'Connected',
|
||||||
|
send: 'Send',
|
||||||
|
receive: 'Receive',
|
||||||
|
desktopPreview: 'Desktop Preview',
|
||||||
|
voiceCall: 'Voice Call',
|
||||||
|
camera: 'Camera',
|
||||||
|
linkCopied: 'Link Copied',
|
||||||
|
linkCopiedDesc: 'Successfully copied to clipboard',
|
||||||
|
idCopied: 'ID Copied',
|
||||||
|
idCopiedDesc: 'Successfully copied to clipboard',
|
||||||
|
idCopiedConnectDesc: 'Successfully copied to clipboard, ready to connect',
|
||||||
|
copyFailed: 'Copy failed',
|
||||||
|
notCompleted: 'Not Completed',
|
||||||
|
inDevelopment: 'In development...',
|
||||||
|
connectSuccess: 'Connected successfully',
|
||||||
|
newConnection: 'New connection appeared',
|
||||||
|
errorOccurred: 'An error occurred',
|
||||||
|
disconnected: 'Disconnected',
|
||||||
|
receiveFilesFailed: 'Failed to receive files',
|
||||||
|
kuraaFooter: 'kuraa',
|
||||||
|
shareUrl: 'Share Link',
|
||||||
|
},
|
||||||
|
|
||||||
|
clipboard: {
|
||||||
|
getRemoteClipboard: 'Get Remote Clipboard',
|
||||||
|
noContent: 'No Content',
|
||||||
|
fetchSuccess: 'Fetched successfully',
|
||||||
|
fetchFailed: 'Failed to fetch',
|
||||||
|
unknownError: 'Unknown Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
transfer: {
|
||||||
|
title: 'Transfer List',
|
||||||
|
clear: 'Clear',
|
||||||
|
waiting: 'Waiting',
|
||||||
|
sending: 'Sending',
|
||||||
|
receiving: 'Receiving',
|
||||||
|
completed: 'Completed',
|
||||||
|
error: 'Error',
|
||||||
|
paused: 'Paused',
|
||||||
|
},
|
||||||
|
|
||||||
|
permission: {
|
||||||
|
title: 'Permission Settings',
|
||||||
|
edit: 'Edit Permission',
|
||||||
|
view: 'View Permission',
|
||||||
|
download: 'Download Permission',
|
||||||
|
desktop: 'Desktop Preview Permission',
|
||||||
|
call: 'Voice Call Permission',
|
||||||
|
camera: 'Camera Permission',
|
||||||
|
editDesc: 'Allow creating, modifying and deleting files and folders',
|
||||||
|
viewDesc: 'Allow viewing file contents and directory structure',
|
||||||
|
downloadDesc: 'Allow transferring files to local',
|
||||||
|
desktopDesc: 'Allow peer to request desktop preview',
|
||||||
|
callDesc: 'Allow peer to request voice call',
|
||||||
|
cameraDesc: 'Allow peer to request camera access',
|
||||||
|
},
|
||||||
|
|
||||||
|
desktop: {
|
||||||
|
remoteDesktop: 'Remote Desktop',
|
||||||
|
waitingForDesktop: 'Waiting for desktop sharing...',
|
||||||
|
endSharing: 'End Sharing',
|
||||||
|
collapseDesktop: 'Collapse Desktop',
|
||||||
|
expandDesktop: 'Expand Desktop',
|
||||||
|
soundOn: 'Sound On',
|
||||||
|
soundOff: 'Mute',
|
||||||
|
fullscreen: 'Fullscreen',
|
||||||
|
exitFullscreen: 'Exit',
|
||||||
|
enableSound: 'Enable Sound',
|
||||||
|
disableSound: 'Mute',
|
||||||
|
desktopBeingViewed: 'Desktop is being viewed',
|
||||||
|
peerViewingDesktop: 'Peer is viewing your desktop',
|
||||||
|
expandDesktopHint: 'Expand desktop preview hint',
|
||||||
|
remoteCamera: 'Remote Camera',
|
||||||
|
waitingForCamera: 'Waiting for camera feed...',
|
||||||
|
collapseCamera: 'Collapse Camera',
|
||||||
|
endCamera: 'End Camera',
|
||||||
|
expandCamera: 'Expand Camera',
|
||||||
|
cameraBeingViewed: 'Camera is being viewed',
|
||||||
|
peerViewingCamera: 'Peer is viewing your camera',
|
||||||
|
expandCameraHint: 'Expand camera preview hint',
|
||||||
|
cameraRequest: 'Camera Request',
|
||||||
|
voiceCallRequest: 'Voice Call Request',
|
||||||
|
peerRequestCamera: 'Peer requests to view your camera',
|
||||||
|
peerRequestCall: 'Peer requests a voice call',
|
||||||
|
reject: 'Reject',
|
||||||
|
accept: 'Accept',
|
||||||
|
voiceCall: 'Voice Call',
|
||||||
|
collapseCall: 'Collapse Call',
|
||||||
|
endCall: 'End Call',
|
||||||
|
inCall: 'In Call',
|
||||||
|
connecting: 'Connecting...',
|
||||||
|
unmute: 'Unmute',
|
||||||
|
mute: 'Mute',
|
||||||
|
expandCall: 'Expand Call',
|
||||||
|
},
|
||||||
|
|
||||||
|
voice: {
|
||||||
|
myId: 'My ID',
|
||||||
|
preparing: 'Preparing connection...',
|
||||||
|
ready: 'Ready',
|
||||||
|
inCall: 'In Call',
|
||||||
|
enterPeerId: 'Enter peer ID',
|
||||||
|
call: 'Call',
|
||||||
|
endCall: 'End Call',
|
||||||
|
unmute: 'Unmute',
|
||||||
|
mute: 'Mute',
|
||||||
|
delay: 'Delay',
|
||||||
|
copy: 'Copy',
|
||||||
|
cannotAccessMic: 'Cannot access microphone',
|
||||||
|
connectionError: 'Connection Error',
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: {
|
||||||
|
uploadImageVideo: 'Upload Image/Video',
|
||||||
|
},
|
||||||
|
}
|
||||||
219
src/i18n/locales/ja-JP.ts
Normal file
219
src/i18n/locales/ja-JP.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
export default {
|
||||||
|
common: {
|
||||||
|
confirm: '確認',
|
||||||
|
cancel: 'キャンセル',
|
||||||
|
copy: 'コピー',
|
||||||
|
delete: '削除',
|
||||||
|
create: '作成',
|
||||||
|
save: '保存',
|
||||||
|
edit: '編集',
|
||||||
|
close: '閉じる',
|
||||||
|
rename: '名前変更',
|
||||||
|
refresh: '更新',
|
||||||
|
download: 'ダウンロード',
|
||||||
|
send: '送信',
|
||||||
|
receive: '受信',
|
||||||
|
share: '共有',
|
||||||
|
connect: '接続',
|
||||||
|
connected: '接続済み',
|
||||||
|
connecting: '接続中...',
|
||||||
|
disconnect: '切断',
|
||||||
|
loading: '読み込み中...',
|
||||||
|
noContent: 'コンテンツなし',
|
||||||
|
success: '成功',
|
||||||
|
failed: '失敗',
|
||||||
|
error: 'エラー',
|
||||||
|
unknownError: '不明なエラー',
|
||||||
|
pleaseWait: 'しばらくお待ちください...',
|
||||||
|
inputPlaceholder: '入力してください',
|
||||||
|
search: '検索',
|
||||||
|
okText: '確認',
|
||||||
|
cancelText: 'キャンセル',
|
||||||
|
warning: '注意',
|
||||||
|
createSuccess: '作成しました',
|
||||||
|
deleteSuccess: '削除しました',
|
||||||
|
renameSuccess: '名前を変更しました',
|
||||||
|
copySuccess: 'コピーしました',
|
||||||
|
operationFailed: '操作に失敗しました',
|
||||||
|
},
|
||||||
|
|
||||||
|
fileView: {
|
||||||
|
parentDir: '親ディレクトリ',
|
||||||
|
sortByName: '名前順',
|
||||||
|
sortBySize: 'サイズ順',
|
||||||
|
sortByType: '種類順',
|
||||||
|
sortByDate: '日付順',
|
||||||
|
permissionSettings: '権限設定',
|
||||||
|
initialize: '初期化',
|
||||||
|
selectDirHint: 'ディレクトリを選択してください',
|
||||||
|
selectDirectory: 'ディレクトリを選択',
|
||||||
|
refresh: '更新',
|
||||||
|
newFolder: '新しいフォルダ',
|
||||||
|
newFile: '新しいファイル',
|
||||||
|
delete: '削除',
|
||||||
|
rename: '名前変更',
|
||||||
|
download: 'ダウンロード',
|
||||||
|
selectLocalDir: 'ローカルディレクトリを選択',
|
||||||
|
confirmDelete: '削除の確認',
|
||||||
|
deleteConfirmContent: '以下のファイルを削除してもよろしいですか?<br>{files}<br>合計 {count} 個のファイル',
|
||||||
|
renameTitle: '名前変更',
|
||||||
|
inputNewName: '新しい名前を入力してください',
|
||||||
|
inputFolderName: 'フォルダ名を入力してください',
|
||||||
|
inputFileName: 'ファイル名を入力してください',
|
||||||
|
createFolderTitle: '新しいフォルダ',
|
||||||
|
createFileTitle: '新しいファイル',
|
||||||
|
warningInputFolderName: 'フォルダ名を入力してください',
|
||||||
|
warningInputFileName: 'ファイル名を入力してください',
|
||||||
|
deleteBtn: '削除',
|
||||||
|
cancelBtn: 'キャンセル',
|
||||||
|
createBtn: '作成',
|
||||||
|
},
|
||||||
|
|
||||||
|
fileReader: {
|
||||||
|
remoteTransfer: 'リモート転送',
|
||||||
|
browserDownload: 'ブラウザダウンロード',
|
||||||
|
save: '保存',
|
||||||
|
edit: '編集',
|
||||||
|
close: '閉じる',
|
||||||
|
autoDetect: '自動検出',
|
||||||
|
other: 'その他',
|
||||||
|
videoNotSupported: 'お使いのブラウザは動画再生に対応していません',
|
||||||
|
audioNotSupported: 'お使いのブラウザは音声再生に対応していません',
|
||||||
|
fileTooLarge: 'ファイルが大きすぎるためプレビューできません。転送してご確認ください',
|
||||||
|
unsupportedType: 'このファイル形式はプレビューに対応していません',
|
||||||
|
openAsText: 'テキストとして開く',
|
||||||
|
downloadTitle: 'ファイルのダウンロード',
|
||||||
|
downloadConfirm: '{name}をダウンロードしてもよろしいですか?',
|
||||||
|
saveFailed: 'ファイルの保存に失敗しました',
|
||||||
|
editor: 'エディタ',
|
||||||
|
preview: 'プレビュー',
|
||||||
|
},
|
||||||
|
|
||||||
|
index: {
|
||||||
|
myId: 'マイID',
|
||||||
|
waitingForConnection: '接続を待っています...',
|
||||||
|
copy: 'コピー',
|
||||||
|
share: '共有',
|
||||||
|
connectedBy: '接続されました',
|
||||||
|
connected: '接続済み',
|
||||||
|
traffic: 'トラフィック',
|
||||||
|
packets: 'パケット数',
|
||||||
|
enterPeerId: '相手のIDを入力',
|
||||||
|
connect: '接続',
|
||||||
|
connectedBtn: '接続済み',
|
||||||
|
send: '送信',
|
||||||
|
receive: '受信',
|
||||||
|
desktopPreview: 'デスクトッププレビュー',
|
||||||
|
voiceCall: '音声通話',
|
||||||
|
camera: 'カメラ',
|
||||||
|
linkCopied: 'リンクをコピーしました',
|
||||||
|
linkCopiedDesc: 'クリップボードにコピーしました',
|
||||||
|
idCopied: 'IDをコピーしました',
|
||||||
|
idCopiedDesc: 'クリップボードにコピーしました',
|
||||||
|
idCopiedConnectDesc: 'クリップボードにコピーしました。接続に使用できます',
|
||||||
|
copyFailed: 'コピーに失敗しました',
|
||||||
|
notCompleted: '未完了',
|
||||||
|
inDevelopment: '開発中...',
|
||||||
|
connectSuccess: '接続しました',
|
||||||
|
newConnection: '新しい接続がありました',
|
||||||
|
errorOccurred: 'エラーが発生しました',
|
||||||
|
disconnected: '切断されました',
|
||||||
|
receiveFilesFailed: 'ファイルの受信に失敗しました',
|
||||||
|
kuraaFooter: 'kuraa',
|
||||||
|
shareUrl: 'リンクを共有',
|
||||||
|
},
|
||||||
|
|
||||||
|
clipboard: {
|
||||||
|
getRemoteClipboard: '相手のクリップボードを取得',
|
||||||
|
noContent: 'コンテンツなし',
|
||||||
|
fetchSuccess: '取得成功',
|
||||||
|
fetchFailed: '取得に失敗しました',
|
||||||
|
unknownError: '不明なエラー',
|
||||||
|
},
|
||||||
|
|
||||||
|
transfer: {
|
||||||
|
title: '転送リスト',
|
||||||
|
clear: 'クリア',
|
||||||
|
waiting: '待機中',
|
||||||
|
sending: '送信中',
|
||||||
|
receiving: '受信中',
|
||||||
|
completed: '完了',
|
||||||
|
error: 'エラー',
|
||||||
|
paused: '一時停止',
|
||||||
|
},
|
||||||
|
|
||||||
|
permission: {
|
||||||
|
title: '権限設定',
|
||||||
|
edit: '編集権限',
|
||||||
|
view: '閲覧権限',
|
||||||
|
download: 'ダウンロード権限',
|
||||||
|
desktop: 'デスクトッププレビュー権限',
|
||||||
|
call: '音声通話権限',
|
||||||
|
camera: 'カメラ権限',
|
||||||
|
editDesc: 'ファイルとフォルダの作成、変更、削除を許可',
|
||||||
|
viewDesc: 'ファイルの内容とディレクトリ構成の表示を許可',
|
||||||
|
downloadDesc: 'ローカルへのファイル転送を許可',
|
||||||
|
desktopDesc: '相手からのデスクトッププレビュー要求を許可',
|
||||||
|
callDesc: '相手からの音声通話要求を許可',
|
||||||
|
cameraDesc: '相手からのカメラアクセス要求を許可',
|
||||||
|
},
|
||||||
|
|
||||||
|
desktop: {
|
||||||
|
remoteDesktop: 'リモートデスクトップ',
|
||||||
|
waitingForDesktop: 'デスクトップ共有を待っています...',
|
||||||
|
endSharing: '共有を終了',
|
||||||
|
collapseDesktop: 'デスクトップを折りたたむ',
|
||||||
|
expandDesktop: 'デスクトップを展開',
|
||||||
|
soundOn: '音声ON',
|
||||||
|
soundOff: 'ミュート',
|
||||||
|
fullscreen: '全画面',
|
||||||
|
exitFullscreen: '終了',
|
||||||
|
enableSound: '音声を有効にする',
|
||||||
|
disableSound: 'ミュート',
|
||||||
|
desktopBeingViewed: 'デスクトップが表示されています',
|
||||||
|
peerViewingDesktop: '相手があなたのデスクトップを表示しています',
|
||||||
|
expandDesktopHint: 'デスクトッププレビューヒントを展開',
|
||||||
|
remoteCamera: 'リモートカメラ',
|
||||||
|
waitingForCamera: 'カメラ映像を待っています...',
|
||||||
|
collapseCamera: 'カメラを折りたたむ',
|
||||||
|
endCamera: 'カメラを終了',
|
||||||
|
expandCamera: 'カメラを展開',
|
||||||
|
cameraBeingViewed: 'カメラが表示されています',
|
||||||
|
peerViewingCamera: '相手があなたのカメラを表示しています',
|
||||||
|
expandCameraHint: 'カメラプレビューヒントを展開',
|
||||||
|
cameraRequest: 'カメラリクエスト',
|
||||||
|
voiceCallRequest: '音声通話リクエスト',
|
||||||
|
peerRequestCamera: '相手がカメラの表示を要求しています',
|
||||||
|
peerRequestCall: '相手が音声通話を要求しています',
|
||||||
|
reject: '拒否',
|
||||||
|
accept: '応答',
|
||||||
|
voiceCall: '音声通話',
|
||||||
|
collapseCall: '通話を折りたたむ',
|
||||||
|
endCall: '通話を終了',
|
||||||
|
inCall: '通話中',
|
||||||
|
connecting: '接続を確立しています...',
|
||||||
|
unmute: 'ミュート解除',
|
||||||
|
mute: 'ミュート',
|
||||||
|
expandCall: '通話を展開',
|
||||||
|
},
|
||||||
|
|
||||||
|
voice: {
|
||||||
|
myId: 'マイID',
|
||||||
|
preparing: '接続準備中...',
|
||||||
|
ready: '準備完了',
|
||||||
|
inCall: '通話中',
|
||||||
|
enterPeerId: '相手のIDを入力',
|
||||||
|
call: '発信',
|
||||||
|
endCall: '通話を終了',
|
||||||
|
unmute: 'ミュート解除',
|
||||||
|
mute: 'ミュート',
|
||||||
|
delay: '遅延時間',
|
||||||
|
copy: 'コピー',
|
||||||
|
cannotAccessMic: 'マイクにアクセスできません',
|
||||||
|
connectionError: '接続エラー',
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: {
|
||||||
|
uploadImageVideo: '画像/動画をアップロード',
|
||||||
|
},
|
||||||
|
}
|
||||||
219
src/i18n/locales/zh-CN.ts
Normal file
219
src/i18n/locales/zh-CN.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
export default {
|
||||||
|
common: {
|
||||||
|
confirm: '确定',
|
||||||
|
cancel: '取消',
|
||||||
|
copy: '复制',
|
||||||
|
delete: '删除',
|
||||||
|
create: '创建',
|
||||||
|
save: '保存',
|
||||||
|
edit: '编辑',
|
||||||
|
close: '关闭',
|
||||||
|
rename: '重命名',
|
||||||
|
refresh: '刷新',
|
||||||
|
download: '下载',
|
||||||
|
send: '发送',
|
||||||
|
receive: '接收',
|
||||||
|
share: '分享',
|
||||||
|
connect: '连接',
|
||||||
|
connected: '已连接',
|
||||||
|
connecting: '连接中...',
|
||||||
|
disconnect: '断开',
|
||||||
|
loading: '加载中...',
|
||||||
|
noContent: '暂无内容',
|
||||||
|
success: '成功',
|
||||||
|
failed: '失败',
|
||||||
|
error: '错误',
|
||||||
|
unknownError: '未知错误',
|
||||||
|
pleaseWait: '请稍后...',
|
||||||
|
inputPlaceholder: '请输入',
|
||||||
|
search: '搜索',
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
warning: '提示',
|
||||||
|
createSuccess: '创建成功',
|
||||||
|
deleteSuccess: '删除成功',
|
||||||
|
renameSuccess: '重命名成功',
|
||||||
|
copySuccess: '复制成功',
|
||||||
|
operationFailed: '操作失败',
|
||||||
|
},
|
||||||
|
|
||||||
|
fileView: {
|
||||||
|
parentDir: '上级目录',
|
||||||
|
sortByName: '按名称',
|
||||||
|
sortBySize: '按大小',
|
||||||
|
sortByType: '按类型',
|
||||||
|
sortByDate: '按日期',
|
||||||
|
permissionSettings: '权限设置',
|
||||||
|
initialize: '初始化',
|
||||||
|
selectDirHint: '请选择一个目录',
|
||||||
|
selectDirectory: '选择目录',
|
||||||
|
refresh: '刷新',
|
||||||
|
newFolder: '新建文件夹',
|
||||||
|
newFile: '新建文件',
|
||||||
|
delete: '删除',
|
||||||
|
rename: '重命名',
|
||||||
|
download: '下载',
|
||||||
|
selectLocalDir: '选择本地目录',
|
||||||
|
confirmDelete: '确认删除',
|
||||||
|
deleteConfirmContent: '确定要删除以下文件吗?<br>{files}<br>共 {count} 个文件',
|
||||||
|
renameTitle: '重命名',
|
||||||
|
inputNewName: '请输入新的名称',
|
||||||
|
inputFolderName: '请输入文件夹名称',
|
||||||
|
inputFileName: '请输入文件名称',
|
||||||
|
createFolderTitle: '新建文件夹',
|
||||||
|
createFileTitle: '新建文件',
|
||||||
|
warningInputFolderName: '请输入文件夹名称',
|
||||||
|
warningInputFileName: '请输入文件名称',
|
||||||
|
deleteBtn: '删除',
|
||||||
|
cancelBtn: '取消',
|
||||||
|
createBtn: '创建',
|
||||||
|
},
|
||||||
|
|
||||||
|
fileReader: {
|
||||||
|
remoteTransfer: '远程传输',
|
||||||
|
browserDownload: '浏览器下载',
|
||||||
|
save: '保存',
|
||||||
|
edit: '编辑',
|
||||||
|
close: '关闭',
|
||||||
|
autoDetect: '自动检测',
|
||||||
|
other: '其他',
|
||||||
|
videoNotSupported: '您的浏览器不支持视频播放',
|
||||||
|
audioNotSupported: '您的浏览器不支持音频播放',
|
||||||
|
fileTooLarge: '文件过大,无法预览,请传输后查看',
|
||||||
|
unsupportedType: '不支持预览该类型的文件',
|
||||||
|
openAsText: '以文本打开',
|
||||||
|
downloadTitle: '下载文件',
|
||||||
|
downloadConfirm: '确定要下载{name}吗?',
|
||||||
|
saveFailed: '保存文件失败',
|
||||||
|
editor: '编辑器',
|
||||||
|
preview: '预览',
|
||||||
|
},
|
||||||
|
|
||||||
|
index: {
|
||||||
|
myId: '我的ID',
|
||||||
|
waitingForConnection: '等待连接...',
|
||||||
|
copy: '复制',
|
||||||
|
share: '分享',
|
||||||
|
connectedBy: '被连接',
|
||||||
|
connected: '已连接',
|
||||||
|
traffic: '流量',
|
||||||
|
packets: '包数',
|
||||||
|
enterPeerId: '输入对方ID',
|
||||||
|
connect: '连接',
|
||||||
|
connectedBtn: '已连接',
|
||||||
|
send: '发送',
|
||||||
|
receive: '接收',
|
||||||
|
desktopPreview: '桌面预览',
|
||||||
|
voiceCall: '语音通话',
|
||||||
|
camera: '摄像头',
|
||||||
|
linkCopied: '链接已复制',
|
||||||
|
linkCopiedDesc: '已成功复制到剪贴板',
|
||||||
|
idCopied: 'ID已复制',
|
||||||
|
idCopiedDesc: '已成功复制到剪贴板',
|
||||||
|
idCopiedConnectDesc: '已成功复制到剪贴板,可用于发起连接',
|
||||||
|
copyFailed: '复制失败',
|
||||||
|
notCompleted: '未完成',
|
||||||
|
inDevelopment: '开发中...',
|
||||||
|
connectSuccess: '连接成功',
|
||||||
|
newConnection: '出现新的连接',
|
||||||
|
errorOccurred: '发生错误',
|
||||||
|
disconnected: '连接已断开',
|
||||||
|
receiveFilesFailed: '接收文件失败',
|
||||||
|
kuraaFooter: 'kuraa',
|
||||||
|
shareUrl: '分享链接',
|
||||||
|
},
|
||||||
|
|
||||||
|
clipboard: {
|
||||||
|
getRemoteClipboard: '获取对方粘贴板',
|
||||||
|
noContent: '暂无内容',
|
||||||
|
fetchSuccess: '获取成功',
|
||||||
|
fetchFailed: '获取失败',
|
||||||
|
unknownError: '未知错误',
|
||||||
|
},
|
||||||
|
|
||||||
|
transfer: {
|
||||||
|
title: '传输列表',
|
||||||
|
clear: '清理',
|
||||||
|
waiting: '等待中',
|
||||||
|
sending: '发送中',
|
||||||
|
receiving: '接收中',
|
||||||
|
completed: '已完成',
|
||||||
|
error: '失败',
|
||||||
|
paused: '已暂停',
|
||||||
|
},
|
||||||
|
|
||||||
|
permission: {
|
||||||
|
title: '权限设置',
|
||||||
|
edit: '编辑权限',
|
||||||
|
view: '查看权限',
|
||||||
|
download: '下载权限',
|
||||||
|
desktop: '桌面预览权限',
|
||||||
|
call: '语音通话权限',
|
||||||
|
camera: '摄像头权限',
|
||||||
|
editDesc: '允许创建、修改、删除文件和文件夹',
|
||||||
|
viewDesc: '允许查看文件内容和目录结构',
|
||||||
|
downloadDesc: '允许传输文件到本地',
|
||||||
|
desktopDesc: '允许对方请求预览当前桌面',
|
||||||
|
callDesc: '允许对方请求语音通话',
|
||||||
|
cameraDesc: '允许对方请求查看当前摄像头',
|
||||||
|
},
|
||||||
|
|
||||||
|
desktop: {
|
||||||
|
remoteDesktop: '远程桌面',
|
||||||
|
waitingForDesktop: '等待桌面共享...',
|
||||||
|
endSharing: '结束共享',
|
||||||
|
collapseDesktop: '收起桌面',
|
||||||
|
expandDesktop: '展开桌面',
|
||||||
|
soundOn: '出声',
|
||||||
|
soundOff: '静音',
|
||||||
|
fullscreen: '全屏',
|
||||||
|
exitFullscreen: '退出',
|
||||||
|
enableSound: '开启声音',
|
||||||
|
disableSound: '静音',
|
||||||
|
desktopBeingViewed: '桌面正在被预览',
|
||||||
|
peerViewingDesktop: '对方正在查看你的桌面',
|
||||||
|
expandDesktopHint: '展开桌面预览提示',
|
||||||
|
remoteCamera: '远程摄像头',
|
||||||
|
waitingForCamera: '等待摄像头画面...',
|
||||||
|
collapseCamera: '收起摄像头',
|
||||||
|
endCamera: '结束摄像头',
|
||||||
|
expandCamera: '展开摄像头',
|
||||||
|
cameraBeingViewed: '摄像头正在被预览',
|
||||||
|
peerViewingCamera: '对方正在查看你的摄像头',
|
||||||
|
expandCameraHint: '展开摄像头预览提示',
|
||||||
|
cameraRequest: '摄像头请求',
|
||||||
|
voiceCallRequest: '语音通话请求',
|
||||||
|
peerRequestCamera: '对方请求查看摄像头',
|
||||||
|
peerRequestCall: '对方请求语音通话',
|
||||||
|
reject: '拒绝',
|
||||||
|
accept: '接听',
|
||||||
|
voiceCall: '语音通话',
|
||||||
|
collapseCall: '收起通话',
|
||||||
|
endCall: '结束通话',
|
||||||
|
inCall: '通话中',
|
||||||
|
connecting: '正在建立连接...',
|
||||||
|
unmute: '取消静音',
|
||||||
|
mute: '静音',
|
||||||
|
expandCall: '展开通话',
|
||||||
|
},
|
||||||
|
|
||||||
|
voice: {
|
||||||
|
myId: '我的ID',
|
||||||
|
preparing: '准备连接...',
|
||||||
|
ready: '准备就绪',
|
||||||
|
inCall: '通话中',
|
||||||
|
enterPeerId: '输入对方的ID',
|
||||||
|
call: '呼叫',
|
||||||
|
endCall: '结束通话',
|
||||||
|
unmute: '取消静音',
|
||||||
|
mute: '静音',
|
||||||
|
delay: '延迟时间',
|
||||||
|
copy: '复制',
|
||||||
|
cannotAccessMic: '无法访问麦克风',
|
||||||
|
connectionError: '连接错误',
|
||||||
|
},
|
||||||
|
|
||||||
|
shader: {
|
||||||
|
uploadImageVideo: '上传图片/视频',
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import App from './App.vue'
|
|||||||
import 'ant-design-vue/dist/reset.css';
|
import 'ant-design-vue/dist/reset.css';
|
||||||
import Antd from 'ant-design-vue';
|
import Antd from 'ant-design-vue';
|
||||||
import router from './router/router';
|
import router from './router/router';
|
||||||
|
import i18n from './i18n';
|
||||||
import { registerSW } from 'virtual:pwa-register'
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
import { isPwa, showPwaInstallPrompt } from './utils/pwa';
|
import { isPwa, showPwaInstallPrompt } from './utils/pwa';
|
||||||
|
|
||||||
@ -25,4 +26,5 @@ const updateSW = registerSW({
|
|||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
app.use(Antd);
|
app.use(Antd);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
app.use(i18n);
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@ -4,59 +4,74 @@
|
|||||||
<!-- 连接状态 -->
|
<!-- 连接状态 -->
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
<div class="peer-info">
|
<div class="peer-info">
|
||||||
我的ID: <span class="id-text">{{ myId || "等待连接..." }}</span>
|
{{ $t("index.myId") }}:
|
||||||
<Button type="primary" @click="copyId" v-if="myId">复制</Button>
|
<span class="id-text">{{
|
||||||
<Button type="link" @click="shareUrl" v-if="myId">分享</Button>
|
myId || $t("index.waitingForConnection")
|
||||||
|
}}</span>
|
||||||
|
<Button type="primary" @click="copyId" v-if="myId">{{
|
||||||
|
$t("index.copy")
|
||||||
|
}}</Button>
|
||||||
|
<Button type="link" @click="shareUrl" v-if="myId">{{
|
||||||
|
$t("index.share")
|
||||||
|
}}</Button>
|
||||||
<span
|
<span
|
||||||
v-if="connectedPeerId"
|
v-if="connectedPeerId"
|
||||||
class="connection-badge"
|
class="connection-badge"
|
||||||
:class="{ inbound: isInboundConnected }"
|
:class="{ inbound: isInboundConnected }"
|
||||||
>
|
>
|
||||||
{{ isInboundConnected ? "被连接" : "已连接" }}:
|
{{
|
||||||
|
isInboundConnected
|
||||||
|
? $t("index.connectedBy")
|
||||||
|
: $t("index.connected")
|
||||||
|
}}:
|
||||||
<span class="connected-peer">{{ connectedPeerLabel }}</span>
|
<span class="connected-peer">{{ connectedPeerLabel }}</span>
|
||||||
<Button
|
<Button
|
||||||
v-if="isInboundConnected && connectedPeerSign"
|
v-if="isInboundConnected && connectedPeerSign"
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
@click="copyPeerSign"
|
@click="copyPeerSign"
|
||||||
>复制</Button
|
>{{ $t("index.copy") }}</Button
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 显示流量 丢包率-->
|
<!-- 显示流量 丢包率-->
|
||||||
<div class="status-info">
|
<div class="status-info">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span>流量:</span>
|
<span>{{ $t("index.traffic") }}:</span>
|
||||||
<span>{{ formatSize(transInfo.bytes) }}/s</span>
|
<span>{{ formatSize(transInfo.bytes) }}/s</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span>包数:</span>
|
<span>{{ $t("index.packets") }}:</span>
|
||||||
<span>{{ transInfo.packets }}/s</span>
|
<span>{{ transInfo.packets }}/s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="status-actions">
|
||||||
<div class="connect-section">
|
<div class="connect-section">
|
||||||
<div class="connect-actions">
|
<div class="connect-actions">
|
||||||
<div
|
<div
|
||||||
class="connect-item"
|
class="connect-item"
|
||||||
:class="{ disabled: !isConnected, active: isDesktopActive }"
|
:class="{ disabled: !isConnected, active: isDesktopActive }"
|
||||||
title="桌面预览"
|
:title="$t('index.desktopPreview')"
|
||||||
@click="requestDesktop"
|
@click="requestDesktop"
|
||||||
>
|
>
|
||||||
<img src="/static/desktop.png" alt="桌面" />
|
<img
|
||||||
|
src="/static/desktop.png"
|
||||||
|
:alt="$t('index.desktopPreview')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="connect-item"
|
class="connect-item"
|
||||||
:class="{ disabled: !isConnected, active: isCallActive }"
|
:class="{ disabled: !isConnected, active: isCallActive }"
|
||||||
title="语音通话"
|
:title="$t('index.voiceCall')"
|
||||||
@click="requestCall"
|
@click="requestCall"
|
||||||
>
|
>
|
||||||
<img src="/static/phone.png" alt="通话" />
|
<img src="/static/phone.png" :alt="$t('index.voiceCall')" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="connect-item"
|
class="connect-item"
|
||||||
:class="{ disabled: !isConnected, active: isCameraActive }"
|
:class="{ disabled: !isConnected, active: isCameraActive }"
|
||||||
title="摄像头"
|
:title="$t('index.camera')"
|
||||||
@click="requestCamera"
|
@click="requestCamera"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@ -72,7 +87,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="targetId"
|
v-model="targetId"
|
||||||
:placeholder="!myId ? '请稍后...' : '输入对方ID'"
|
:placeholder="
|
||||||
|
!myId ? $t('common.pleaseWait') : $t('index.enterPeerId')
|
||||||
|
"
|
||||||
:disabled="isConnected || !myId"
|
:disabled="isConnected || !myId"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@ -80,9 +97,35 @@
|
|||||||
:disabled="!targetId || isConnected"
|
:disabled="!targetId || isConnected"
|
||||||
:loading="isConnecting"
|
:loading="isConnecting"
|
||||||
>
|
>
|
||||||
{{ isConnected ? "已连接" : "连接" }}
|
{{ isConnected ? $t("index.connectedBtn") : $t("index.connect") }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<a-dropdown>
|
||||||
|
<div class="lang-switcher">
|
||||||
|
<svg
|
||||||
|
t="1778484837023"
|
||||||
|
class="icon lang-switcher-icon"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
p-id="1747"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M846.04 866.77c-17.08 2.03-32.57-10.18-34.59-27.26-0.22-1.9-0.27-3.81-0.15-5.71v-123c0-33.73-22.17-33.73-30.53-33.73-21.28-0.46-38.91 16.43-39.36 37.72-0.01 0.46-0.01 0.92 0 1.37v117.66c-0.76 18.9-16.71 33.61-35.61 32.84-17.83-0.72-32.12-15.01-32.84-32.84V647.68c-1.23-17.23 11.74-32.19 28.97-33.41 1.69-0.12 3.39-0.1 5.08 0.05a31.953 31.953 0 0 1 31.33 17.76 89.435 89.435 0 0 1 54.99-17.76c54.11 0 86.45 33.59 86.45 90.03V833.8a32.25 32.25 0 0 1-8.88 23.72 34.026 34.026 0 0 1-24.82 9.33l-0.04-0.08z m-233.12-7.46h-134.7c-42.77 0-61.85-18.96-61.85-61.57V608.07c0-42.52 19.09-61.57 61.85-61.57h128.74c17.92 0 32.45 14.53 32.45 32.45s-14.53 32.45-32.45 32.45H490.73c-1.22-0.08-2.45 0.09-3.6 0.5 0.13 0-0.15 0.8-0.15 2.89v52.58h106c16.33-1.66 30.91 10.24 32.57 26.57 0.17 1.68 0.2 3.37 0.08 5.06 0.98 16.66-11.73 30.97-28.4 31.95-1.41 0.08-2.83 0.07-4.24-0.05H486.9V791c-0.04 1.06 0.08 2.13 0.35 3.15 1.12 0.15 2.25 0.23 3.38 0.24h122.31c16.96-1.07 31.58 11.81 32.65 28.76 0.07 1.16 0.08 2.33 0.02 3.5 1.35 16.68-11.07 31.3-27.75 32.65-1.64 0.13-3.28 0.13-4.92 0h-0.02zM327.54 482.85c-17.36 2.36-33.34-9.8-35.7-27.16-0.3-2.21-0.37-4.44-0.2-6.67V370.5h-85.27c-45.86 0-66.31-20.52-66.31-66.31v-93.87c0-45.58 20.52-65.9 66.31-65.9h85.27v-31.53c-1.38-17.11 11.37-32.11 28.48-33.49 1.92-0.15 3.84-0.13 5.76 0.07 30.26 0 36.63 18.17 36.63 33.42v31.59h86.09c45.86 0 66.33 20.34 66.33 65.88v93.89c0 45.86-20.52 66.29-66.33 66.29h-86.05v78.52c1.25 17.47-11.9 32.65-29.37 33.91-1.88 0.13-3.76 0.1-5.63-0.1v-0.02zM217.21 211.27c-6.47 0-7.07 0.6-7.07 7.07v78.2c0 6.53 0.6 7.15 7.07 7.15h74.43v-92.42h-74.43z m145.35 92.38h75.29c6.29 0 7.09-0.8 7.09-7.07v-78.25c0-6.29-0.8-7.09-7.09-7.09h-75.31v92.42h0.02z m151.42 655.91C266.43 958.82 66.36 757.55 67.1 510c0.1-35 4.31-69.86 12.52-103.88 4.81-19 23.92-30.68 43.03-26.29 19.1 4.61 30.86 23.81 26.29 42.91-48.93 202.33 75.42 406.01 277.75 454.94a376.924 376.924 0 0 0 87.29 10.56c19.69 0.02 35.64 15.99 35.63 35.69-0.02 19.67-15.96 35.61-35.63 35.63z m398.49-310.05c-19.69 0-35.66-15.96-35.66-35.65 0-2.95 0.37-5.9 1.09-8.76 51.31-201.82-70.7-407.02-272.52-458.33-29.89-7.6-60.59-11.5-91.43-11.62-19.68 0-35.64-15.95-35.64-35.63 0-19.68 15.95-35.64 35.63-35.64h0.01c247.57 0.76 447.65 202.08 446.89 449.65-0.11 36.8-4.76 73.44-13.83 109.1-4 15.8-18.23 26.88-34.54 26.88z"
|
||||||
|
fill="currentColor"
|
||||||
|
p-id="1748"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="handleLangMenuClick">
|
||||||
|
<a-menu-item key="zh-CN">中文</a-menu-item>
|
||||||
|
<a-menu-item key="en-US">English</a-menu-item>
|
||||||
|
<a-menu-item key="ja-JP">日本語</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件传输区域 -->
|
<!-- 文件传输区域 -->
|
||||||
@ -106,7 +149,7 @@
|
|||||||
:disabled="!isConnected || selectedLocalFiles.length === 0"
|
:disabled="!isConnected || selectedLocalFiles.length === 0"
|
||||||
@click="handleSend"
|
@click="handleSend"
|
||||||
>
|
>
|
||||||
发送
|
{{ $t("index.send") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -114,7 +157,7 @@
|
|||||||
@click="handleReceive"
|
@click="handleReceive"
|
||||||
:loading="receiveLoading"
|
:loading="receiveLoading"
|
||||||
>
|
>
|
||||||
接收
|
{{ $t("index.receive") }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -123,7 +166,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="author-footer">
|
<div class="author-footer">
|
||||||
<a href="https://kuraa.cc" target="_blank" rel="noopener">kuraa</a>
|
<a href="https://kuraa.cc" target="_blank" rel="noopener">{{
|
||||||
|
$t("index.kuraaFooter")
|
||||||
|
}}</a>
|
||||||
</div>
|
</div>
|
||||||
<FileTransferView />
|
<FileTransferView />
|
||||||
</template>
|
</template>
|
||||||
@ -131,6 +176,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Clipboard from "./item/clipboard.vue";
|
import Clipboard from "./item/clipboard.vue";
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import { peer } from "./utils/peer";
|
import { peer } from "./utils/peer";
|
||||||
import FileView from "./item/fileView.vue";
|
import FileView from "./item/fileView.vue";
|
||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
@ -146,6 +192,12 @@ import {
|
|||||||
} from "./utils/common";
|
} from "./utils/common";
|
||||||
import FileTransferView from "./item/fileTranserView.vue";
|
import FileTransferView from "./item/fileTranserView.vue";
|
||||||
|
|
||||||
|
const { locale, t } = useI18n();
|
||||||
|
|
||||||
|
const handleLangMenuClick = ({ key }: { key: string }) => {
|
||||||
|
locale.value = key;
|
||||||
|
};
|
||||||
|
|
||||||
const isPhone = ref(false);
|
const isPhone = ref(false);
|
||||||
const receiveLoading = ref(false);
|
const receiveLoading = ref(false);
|
||||||
const myId = ref("");
|
const myId = ref("");
|
||||||
@ -198,8 +250,8 @@ const shareUrl = async () => {
|
|||||||
window.location.origin + window.location.pathname + "?sign=" + myId.value;
|
window.location.origin + window.location.pathname + "?sign=" + myId.value;
|
||||||
await copyToClipboard(url);
|
await copyToClipboard(url);
|
||||||
notification.success({
|
notification.success({
|
||||||
message: "链接已复制",
|
message: t("index.linkCopied"),
|
||||||
description: "已成功复制到剪贴板",
|
description: t("index.linkCopiedDesc"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -210,12 +262,12 @@ const copyId = async () => {
|
|||||||
try {
|
try {
|
||||||
await copyToClipboard(myId.value);
|
await copyToClipboard(myId.value);
|
||||||
notification.success({
|
notification.success({
|
||||||
message: "ID已复制",
|
message: t("index.idCopied"),
|
||||||
description: "已成功复制到剪贴板",
|
description: t("index.idCopiedDesc"),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "复制失败",
|
message: t("index.copyFailed"),
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -227,11 +279,11 @@ const copyPeerSign = async () => {
|
|||||||
try {
|
try {
|
||||||
await copyToClipboard(connectedPeerSign.value);
|
await copyToClipboard(connectedPeerSign.value);
|
||||||
notification.success({
|
notification.success({
|
||||||
message: "ID已复制",
|
message: t("index.idCopied"),
|
||||||
description: "已成功复制到剪贴板,可用于发起连接",
|
description: t("index.idCopiedConnectDesc"),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
notification.error({ message: "复制失败" });
|
notification.error({ message: t("index.copyFailed") });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -289,8 +341,8 @@ const checkBytes = () => {
|
|||||||
};
|
};
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
notification.info({
|
notification.info({
|
||||||
message: "未完成",
|
message: t("index.notCompleted"),
|
||||||
description: "开发中...",
|
description: t("index.inDevelopment"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const handleReceive = async () => {
|
const handleReceive = async () => {
|
||||||
@ -313,7 +365,7 @@ const handleReceive = async () => {
|
|||||||
await Promise.all(getFileHandles);
|
await Promise.all(getFileHandles);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "接收文件失败",
|
message: t("index.receiveFilesFailed"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
receiveLoading.value = false;
|
receiveLoading.value = false;
|
||||||
@ -338,12 +390,12 @@ onMounted(async () => {
|
|||||||
updateConnectedPeer(event.detail.peer, false);
|
updateConnectedPeer(event.detail.peer, false);
|
||||||
fileMgrInstance.remoteRootFile.loadLocalDirectory();
|
fileMgrInstance.remoteRootFile.loadLocalDirectory();
|
||||||
notification.success({
|
notification.success({
|
||||||
message: "连接成功",
|
message: t("index.connectSuccess"),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
updateConnectedPeer(event.detail.peer, true);
|
updateConnectedPeer(event.detail.peer, true);
|
||||||
notification.success({
|
notification.success({
|
||||||
message: "出现新的连接",
|
message: t("index.newConnection"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}) as EventListener);
|
}) as EventListener);
|
||||||
@ -354,7 +406,7 @@ onMounted(async () => {
|
|||||||
isCallActive.value = false;
|
isCallActive.value = false;
|
||||||
isCameraActive.value = false;
|
isCameraActive.value = false;
|
||||||
notification.error({
|
notification.error({
|
||||||
message: event.detail.peer + "连接已断开",
|
message: event.detail.peer + t("index.disconnected"),
|
||||||
});
|
});
|
||||||
}) as EventListener);
|
}) as EventListener);
|
||||||
|
|
||||||
@ -370,7 +422,7 @@ onMounted(async () => {
|
|||||||
peer.on("error", ((event: CustomEvent) => {
|
peer.on("error", ((event: CustomEvent) => {
|
||||||
isConnecting.value = false;
|
isConnecting.value = false;
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "发生错误",
|
message: t("index.errorOccurred"),
|
||||||
});
|
});
|
||||||
console.error("连接错误:", event.detail);
|
console.error("连接错误:", event.detail);
|
||||||
}) as EventListener);
|
}) as EventListener);
|
||||||
@ -428,8 +480,9 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -439,6 +492,7 @@ onMounted(async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-badge {
|
.connection-badge {
|
||||||
@ -497,24 +551,74 @@ onMounted(async () => {
|
|||||||
.connect-item.disabled img {
|
.connect-item.disabled img {
|
||||||
filter: grayscale(1);
|
filter: grayscale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-item img {
|
.connect-item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-item svg {
|
.connect-item svg {
|
||||||
width: 22px;
|
width: 35px;
|
||||||
height: 22px;
|
height: 35px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
color: #1677ff;
|
color: #1677ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-actions,
|
||||||
.connect-actions,
|
.connect-actions,
|
||||||
.connect-section {
|
.connect-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-section {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-section input {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-switcher {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid #d9e6f7;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
color: #1677ff;
|
||||||
|
transition:
|
||||||
|
background 0.2s,
|
||||||
|
border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-switcher:hover {
|
||||||
|
background: #f0f6ff;
|
||||||
|
border-color: #91caff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-switcher-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
.file-transfer {
|
.file-transfer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -666,21 +770,33 @@ input:disabled {
|
|||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.status-bar {
|
.status-bar {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.peer-info,
|
||||||
|
.status-info,
|
||||||
|
.status-actions,
|
||||||
.connect-section {
|
.connect-section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-actions {
|
||||||
|
margin-left: 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.connect-section input {
|
.connect-section input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-info {
|
.status-info {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lang-switcher {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -14,14 +14,14 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="clipboard-header">
|
<div class="clipboard-header">
|
||||||
<div class="clipboard-actions">
|
<div class="clipboard-actions">
|
||||||
<Button @click="getRemoteClipboard" :loading="loading"
|
<Button @click="getRemoteClipboard" :loading="loading">{{
|
||||||
>获取对方粘贴板</Button
|
$t("clipboard.getRemoteClipboard")
|
||||||
>
|
}}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="clipboard-content">
|
<div class="clipboard-content">
|
||||||
<div class="clipboard-text">
|
<div class="clipboard-text">
|
||||||
{{ remoteClipboard || "暂无内容" }}
|
{{ remoteClipboard || $t("clipboard.noContent") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -31,10 +31,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import { Button } from "ant-design-vue";
|
import { Button } from "ant-design-vue";
|
||||||
import { notification } from "ant-design-vue";
|
import { notification } from "ant-design-vue";
|
||||||
import { peer, MessageType } from "../utils/peer";
|
import { peer, MessageType } from "../utils/peer";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const remoteClipboard = ref("");
|
const remoteClipboard = ref("");
|
||||||
const mouseEntering = ref(false);
|
const mouseEntering = ref(false);
|
||||||
@ -49,12 +52,12 @@ const getRemoteClipboard = async () => {
|
|||||||
});
|
});
|
||||||
remoteClipboard.value = result;
|
remoteClipboard.value = result;
|
||||||
notification.success({
|
notification.success({
|
||||||
message: "获取成功",
|
message: t("clipboard.fetchSuccess"),
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "获取失败",
|
message: t("clipboard.fetchFailed"),
|
||||||
description: error?.message || "未知错误",
|
description: error?.message || t("clipboard.unknownError"),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -65,7 +68,9 @@ const getRemoteClipboard = async () => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.5s ease, max-height 0.5s ease;
|
transition:
|
||||||
|
opacity 0.5s ease,
|
||||||
|
max-height 0.5s ease;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
<line x1="8" y1="21" x2="16" y2="21" />
|
<line x1="8" y1="21" x2="16" y2="21" />
|
||||||
<line x1="12" y1="17" x2="12" y2="21" />
|
<line x1="12" y1="17" x2="12" y2="21" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>远程桌面</span>
|
<span>{{ $t("desktop.remoteDesktop") }}</span>
|
||||||
<span class="duration-text" v-if="desktopStream">{{
|
<span class="duration-text" v-if="desktopStream">{{
|
||||||
callDurationStr
|
callDurationStr
|
||||||
}}</span>
|
}}</span>
|
||||||
@ -40,7 +40,11 @@
|
|||||||
class="header-btn"
|
class="header-btn"
|
||||||
:class="{ 'sound-enabled': isDesktopSoundEnabled }"
|
:class="{ 'sound-enabled': isDesktopSoundEnabled }"
|
||||||
@click="toggleDesktopSound"
|
@click="toggleDesktopSound"
|
||||||
:title="isDesktopSoundEnabled ? '静音' : '开启声音'"
|
:title="
|
||||||
|
isDesktopSoundEnabled
|
||||||
|
? $t('desktop.disableSound')
|
||||||
|
: $t('desktop.enableSound')
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
v-if="isDesktopSoundEnabled"
|
v-if="isDesktopSoundEnabled"
|
||||||
@ -68,13 +72,21 @@
|
|||||||
<line x1="23" y1="9" x2="17" y2="15" />
|
<line x1="23" y1="9" x2="17" y2="15" />
|
||||||
<line x1="17" y1="9" x2="23" y2="15" />
|
<line x1="17" y1="9" x2="23" y2="15" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ isDesktopSoundEnabled ? "出声" : "静音" }}</span>
|
<span>{{
|
||||||
|
isDesktopSoundEnabled
|
||||||
|
? $t("desktop.soundOn")
|
||||||
|
: $t("desktop.soundOff")
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="desktopStream"
|
v-if="desktopStream"
|
||||||
class="header-btn"
|
class="header-btn"
|
||||||
@click="toggleFullscreen"
|
@click="toggleFullscreen"
|
||||||
:title="isFullscreen ? '退出全屏' : '全屏'"
|
:title="
|
||||||
|
isFullscreen
|
||||||
|
? $t('desktop.exitFullscreen')
|
||||||
|
: $t('desktop.fullscreen')
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
v-if="!isFullscreen"
|
v-if="!isFullscreen"
|
||||||
@ -104,7 +116,11 @@
|
|||||||
<line x1="14" y1="10" x2="21" y2="3" />
|
<line x1="14" y1="10" x2="21" y2="3" />
|
||||||
<line x1="3" y1="21" x2="10" y2="14" />
|
<line x1="3" y1="21" x2="10" y2="14" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ isFullscreen ? "退出" : "全屏" }}</span>
|
<span>{{
|
||||||
|
isFullscreen
|
||||||
|
? $t("desktop.exitFullscreen")
|
||||||
|
: $t("desktop.fullscreen")
|
||||||
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="header-btn danger" @click="endDesktop">
|
<button class="header-btn danger" @click="endDesktop">
|
||||||
<svg
|
<svg
|
||||||
@ -118,7 +134,7 @@
|
|||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>结束共享</span>
|
<span>{{ $t("desktop.endSharing") }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -144,14 +160,14 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>等待桌面共享...</span>
|
<span>{{ $t("desktop.waitingForDesktop") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="desktop-side-toggle"
|
class="desktop-side-toggle"
|
||||||
@click="toggleDesktopCollapse"
|
@click="toggleDesktopCollapse"
|
||||||
title="收起桌面"
|
:title="$t('desktop.collapseDesktop')"
|
||||||
aria-label="收起桌面"
|
:aria-label="$t('desktop.collapseDesktop')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="18"
|
width="18"
|
||||||
@ -174,8 +190,8 @@
|
|||||||
class="desktop-collapsed-tab"
|
class="desktop-collapsed-tab"
|
||||||
:class="desktopCollapsedClasses"
|
:class="desktopCollapsedClasses"
|
||||||
@click="toggleDesktopCollapse"
|
@click="toggleDesktopCollapse"
|
||||||
title="展开桌面"
|
:title="$t('desktop.expandDesktop')"
|
||||||
aria-label="展开桌面"
|
:aria-label="$t('desktop.expandDesktop')"
|
||||||
>
|
>
|
||||||
<span class="sharing-status-dot"></span>
|
<span class="sharing-status-dot"></span>
|
||||||
<svg
|
<svg
|
||||||
@ -214,13 +230,13 @@
|
|||||||
<path d="M8 20h8" />
|
<path d="M8 20h8" />
|
||||||
<path d="M12 16v4" />
|
<path d="M12 16v4" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>桌面正在被预览</span>
|
<span>{{ $t("desktop.desktopBeingViewed") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-header-actions">
|
<div class="panel-header-actions">
|
||||||
<button
|
<button
|
||||||
class="panel-icon-btn"
|
class="panel-icon-btn"
|
||||||
@click="toggleDesktopShareCollapse"
|
@click="toggleDesktopShareCollapse"
|
||||||
title="收起提示"
|
:title="$t('desktop.collapseDesktop')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
@ -233,7 +249,11 @@
|
|||||||
<polyline points="18 15 12 9 6 15" />
|
<polyline points="18 15 12 9 6 15" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="panel-icon-btn" @click="endDesktop" title="结束预览">
|
<button
|
||||||
|
class="panel-icon-btn"
|
||||||
|
@click="endDesktop"
|
||||||
|
:title="$t('desktop.endSharing')"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
@ -250,7 +270,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sharing-panel-body">
|
<div class="sharing-panel-body">
|
||||||
<div class="sharing-status-dot"></div>
|
<div class="sharing-status-dot"></div>
|
||||||
<span>对方正在查看你的桌面</span>
|
<span>{{ $t("desktop.peerViewingDesktop") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
@ -261,8 +281,8 @@
|
|||||||
class="desktop-sharing-tab"
|
class="desktop-sharing-tab"
|
||||||
:class="desktopCollapsedClasses"
|
:class="desktopCollapsedClasses"
|
||||||
@click="toggleDesktopShareCollapse"
|
@click="toggleDesktopShareCollapse"
|
||||||
title="展开桌面预览提示"
|
:title="$t('desktop.expandDesktopHint')"
|
||||||
aria-label="展开桌面预览提示"
|
:aria-label="$t('desktop.expandDesktopHint')"
|
||||||
>
|
>
|
||||||
<span class="sharing-status-dot"></span>
|
<span class="sharing-status-dot"></span>
|
||||||
<svg
|
<svg
|
||||||
@ -307,13 +327,13 @@
|
|||||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
<rect x="1" y="5" width="15" height="14" rx="2" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span>远程摄像头</span>
|
<span>{{ $t("desktop.remoteCamera") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-header-actions">
|
<div class="panel-header-actions">
|
||||||
<button
|
<button
|
||||||
class="panel-icon-btn"
|
class="panel-icon-btn"
|
||||||
@click="toggleCameraCollapse"
|
@click="toggleCameraCollapse"
|
||||||
title="收起摄像头"
|
:title="$t('desktop.collapseCamera')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
@ -329,7 +349,7 @@
|
|||||||
<button
|
<button
|
||||||
class="panel-icon-btn"
|
class="panel-icon-btn"
|
||||||
@click="endCamera"
|
@click="endCamera"
|
||||||
title="结束摄像头"
|
:title="$t('desktop.endCamera')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
@ -353,7 +373,7 @@
|
|||||||
playsinline
|
playsinline
|
||||||
></video>
|
></video>
|
||||||
<div v-else class="loading-state">
|
<div v-else class="loading-state">
|
||||||
<span>等待摄像头画面...</span>
|
<span>{{ $t("desktop.waitingForCamera") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -365,8 +385,8 @@
|
|||||||
class="camera-collapsed-tab"
|
class="camera-collapsed-tab"
|
||||||
:class="cameraCollapsedClasses"
|
:class="cameraCollapsedClasses"
|
||||||
@click="toggleCameraCollapse"
|
@click="toggleCameraCollapse"
|
||||||
title="展开摄像头"
|
:title="$t('desktop.expandCamera')"
|
||||||
aria-label="展开摄像头"
|
:aria-label="$t('desktop.expandCamera')"
|
||||||
>
|
>
|
||||||
<span class="sharing-status-dot"></span>
|
<span class="sharing-status-dot"></span>
|
||||||
<svg
|
<svg
|
||||||
@ -402,13 +422,13 @@
|
|||||||
<path d="M23 7l-7 5 7 5V7z" />
|
<path d="M23 7l-7 5 7 5V7z" />
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
<rect x="1" y="5" width="15" height="14" rx="2" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>摄像头正在被预览</span>
|
<span>{{ $t("desktop.cameraBeingViewed") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-header-actions">
|
<div class="panel-header-actions">
|
||||||
<button
|
<button
|
||||||
class="panel-icon-btn"
|
class="panel-icon-btn"
|
||||||
@click="toggleCameraShareCollapse"
|
@click="toggleCameraShareCollapse"
|
||||||
title="收起提示"
|
:title="$t('desktop.collapseCamera')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
@ -424,7 +444,7 @@
|
|||||||
<button
|
<button
|
||||||
class="panel-icon-btn"
|
class="panel-icon-btn"
|
||||||
@click="endCamera"
|
@click="endCamera"
|
||||||
title="结束摄像头"
|
:title="$t('desktop.endCamera')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
@ -442,7 +462,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sharing-panel-body">
|
<div class="sharing-panel-body">
|
||||||
<div class="sharing-status-dot"></div>
|
<div class="sharing-status-dot"></div>
|
||||||
<span>对方正在查看你的摄像头</span>
|
<span>{{ $t("desktop.peerViewingCamera") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
@ -453,8 +473,8 @@
|
|||||||
class="camera-sharing-tab"
|
class="camera-sharing-tab"
|
||||||
:class="cameraCollapsedClasses"
|
:class="cameraCollapsedClasses"
|
||||||
@click="toggleCameraShareCollapse"
|
@click="toggleCameraShareCollapse"
|
||||||
title="展开摄像头预览提示"
|
:title="$t('desktop.expandCameraHint')"
|
||||||
aria-label="展开摄像头预览提示"
|
:aria-label="$t('desktop.expandCameraHint')"
|
||||||
>
|
>
|
||||||
<span class="sharing-status-dot"></span>
|
<span class="sharing-status-dot"></span>
|
||||||
<svg
|
<svg
|
||||||
@ -515,8 +535,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>{{
|
<span>{{
|
||||||
incomingCallRequest.type === "camera"
|
incomingCallRequest.type === "camera"
|
||||||
? "摄像头请求"
|
? $t("desktop.cameraRequest")
|
||||||
: "语音通话请求"
|
: $t("desktop.voiceCallRequest")
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -554,8 +574,8 @@
|
|||||||
<div class="call-status-text">
|
<div class="call-status-text">
|
||||||
{{
|
{{
|
||||||
incomingCallRequest.type === "camera"
|
incomingCallRequest.type === "camera"
|
||||||
? "对方请求查看摄像头"
|
? $t("desktop.peerRequestCamera")
|
||||||
: "对方请求语音通话"
|
: $t("desktop.peerRequestCall")
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -565,14 +585,14 @@
|
|||||||
:disabled="isIncomingCallHandling"
|
:disabled="isIncomingCallHandling"
|
||||||
@click="rejectIncomingCall"
|
@click="rejectIncomingCall"
|
||||||
>
|
>
|
||||||
拒绝
|
{{ $t("desktop.reject") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="incoming-action-btn accept"
|
class="incoming-action-btn accept"
|
||||||
:disabled="isIncomingCallHandling"
|
:disabled="isIncomingCallHandling"
|
||||||
@click="acceptIncomingCall"
|
@click="acceptIncomingCall"
|
||||||
>
|
>
|
||||||
接听
|
{{ $t("desktop.accept") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -601,13 +621,13 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span>语音通话</span>
|
<span>{{ $t("desktop.voiceCall") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-header-actions">
|
<div class="panel-header-actions">
|
||||||
<button
|
<button
|
||||||
class="panel-icon-btn"
|
class="panel-icon-btn"
|
||||||
@click="toggleCallCollapse"
|
@click="toggleCallCollapse"
|
||||||
title="收起通话"
|
:title="$t('desktop.collapseCall')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
@ -620,7 +640,11 @@
|
|||||||
<polyline points="18 15 12 9 6 15" />
|
<polyline points="18 15 12 9 6 15" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="panel-icon-btn" @click="endCall" title="结束通话">
|
<button
|
||||||
|
class="panel-icon-btn"
|
||||||
|
@click="endCall"
|
||||||
|
:title="$t('desktop.endCall')"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
@ -651,7 +675,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="call-status-text">
|
<div class="call-status-text">
|
||||||
{{ callStream ? "通话中" : "正在建立连接..." }}
|
{{ callStream ? $t("desktop.inCall") : $t("desktop.connecting") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="call-duration">{{ callDurationStr }}</div>
|
<div class="call-duration">{{ callDurationStr }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -660,7 +684,7 @@
|
|||||||
class="control-btn mute-btn"
|
class="control-btn mute-btn"
|
||||||
:class="{ 'is-muted': isCallMuted }"
|
:class="{ 'is-muted': isCallMuted }"
|
||||||
@click="toggleCallMuted"
|
@click="toggleCallMuted"
|
||||||
:title="isCallMuted ? '取消静音' : '静音'"
|
:title="isCallMuted ? $t('desktop.unmute') : $t('desktop.mute')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
v-if="!isCallMuted"
|
v-if="!isCallMuted"
|
||||||
@ -696,7 +720,7 @@
|
|||||||
<button
|
<button
|
||||||
class="control-btn end-call-btn"
|
class="control-btn end-call-btn"
|
||||||
@click="endCall"
|
@click="endCall"
|
||||||
title="结束通话"
|
:title="$t('desktop.endCall')"
|
||||||
>
|
>
|
||||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
@ -719,7 +743,7 @@
|
|||||||
(isCameraLocalSharing && !isCameraShareCollapsed),
|
(isCameraLocalSharing && !isCameraShareCollapsed),
|
||||||
}"
|
}"
|
||||||
@click="toggleCallCollapse"
|
@click="toggleCallCollapse"
|
||||||
title="展开通话"
|
:title="$t('desktop.expandCall')"
|
||||||
>
|
>
|
||||||
<div class="call-collapsed-pulse"></div>
|
<div class="call-collapsed-pulse"></div>
|
||||||
<svg
|
<svg
|
||||||
@ -741,9 +765,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, onMounted, onUnmounted } from "vue";
|
import { computed, nextTick, ref, onMounted, onUnmounted } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import { peer } from "../utils/peer";
|
import { peer } from "../utils/peer";
|
||||||
import { message } from "ant-design-vue";
|
import { message } from "ant-design-vue";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
peerId: string;
|
peerId: string;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
:open="visible"
|
:open="visible"
|
||||||
title="权限设置"
|
:title="$t('permission.title')"
|
||||||
@ok="handleOk"
|
@ok="handleOk"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
:maskClosable="false"
|
:maskClosable="false"
|
||||||
@ -13,9 +13,11 @@
|
|||||||
:key="permission"
|
:key="permission"
|
||||||
>
|
>
|
||||||
<Checkbox v-model:checked="localPermissionSet[permission]">
|
<Checkbox v-model:checked="localPermissionSet[permission]">
|
||||||
{{ permissionLabels[permission] }}
|
{{ $t(`permission.${permission}`) }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<div class="permission-desc">{{ permissionDescs[permission] }}</div>
|
<div class="permission-desc">
|
||||||
|
{{ $t(`permission.${permission}Desc`) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -36,28 +38,11 @@ const permissionsToShow = computed(() =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const permissionLabels: Record<Permission, string> = {
|
|
||||||
[Permission.edit]: "编辑权限",
|
|
||||||
[Permission.view]: "查看权限",
|
|
||||||
[Permission.download]: "下载权限",
|
|
||||||
[Permission.desktop]: "桌面预览权限",
|
|
||||||
[Permission.call]: "语音通话权限",
|
|
||||||
[Permission.camera]: "摄像头权限",
|
|
||||||
};
|
|
||||||
|
|
||||||
const permissionDescs: Record<Permission, string> = {
|
|
||||||
[Permission.edit]: "允许创建、修改、删除文件和文件夹",
|
|
||||||
[Permission.view]: "允许查看文件内容和目录结构",
|
|
||||||
[Permission.download]: "允许传输文件到本地",
|
|
||||||
[Permission.desktop]: "允许对方请求预览当前桌面",
|
|
||||||
[Permission.call]: "允许对方请求语音通话",
|
|
||||||
[Permission.camera]: "允许对方请求查看当前摄像头",
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkCamera = async () => {
|
const checkCamera = async () => {
|
||||||
try {
|
try {
|
||||||
const devices = await navigator.mediaDevices?.enumerateDevices?.();
|
const devices = await navigator.mediaDevices?.enumerateDevices?.();
|
||||||
hasCamera.value = devices?.some((device) => device.kind === "videoinput") ?? false;
|
hasCamera.value =
|
||||||
|
devices?.some((device) => device.kind === "videoinput") ?? false;
|
||||||
} catch {
|
} catch {
|
||||||
hasCamera.value = false;
|
hasCamera.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,12 +8,16 @@
|
|||||||
:disabled="!loadedFile.name"
|
:disabled="!loadedFile.name"
|
||||||
@click="downloadFile"
|
@click="downloadFile"
|
||||||
>
|
>
|
||||||
{{ fileTypeCheck == "unsupported-size" ? "远程传输" : "浏览器下载" }}
|
{{
|
||||||
|
fileTypeCheck == "unsupported-size"
|
||||||
|
? $t("fileReader.remoteTransfer")
|
||||||
|
: $t("fileReader.browserDownload")
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="canEdit" @click="toggleEdit">
|
<button v-if="canEdit" @click="toggleEdit">
|
||||||
{{ isEditing ? "保存" : "编辑" }}
|
{{ isEditing ? $t("fileReader.save") : $t("fileReader.edit") }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="$emit('close')">关闭</button>
|
<button @click="$emit('close')">{{ $t("fileReader.close") }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -59,7 +63,7 @@
|
|||||||
<div v-else-if="fileTypeCheck === 'video'" class="video-viewer">
|
<div v-else-if="fileTypeCheck === 'video'" class="video-viewer">
|
||||||
<video controls>
|
<video controls>
|
||||||
<source :src="fileUrl" :type="mimeType" />
|
<source :src="fileUrl" :type="mimeType" />
|
||||||
您的浏览器不支持视频播放
|
{{ $t("fileReader.videoNotSupported") }}
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -67,7 +71,7 @@
|
|||||||
<div v-else-if="fileTypeCheck === 'audio'" class="audio-viewer">
|
<div v-else-if="fileTypeCheck === 'audio'" class="audio-viewer">
|
||||||
<audio controls>
|
<audio controls>
|
||||||
<source :src="fileUrl" :type="mimeType" />
|
<source :src="fileUrl" :type="mimeType" />
|
||||||
您的浏览器不支持音频播放
|
{{ $t("fileReader.audioNotSupported") }}
|
||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -75,7 +79,7 @@
|
|||||||
<div v-else-if="fileTypeCheck === 'code'" class="code-viewer">
|
<div v-else-if="fileTypeCheck === 'code'" class="code-viewer">
|
||||||
<div class="code-header">
|
<div class="code-header">
|
||||||
<select v-model="selectedLanguage">
|
<select v-model="selectedLanguage">
|
||||||
<option value="auto">自动检测</option>
|
<option value="auto">{{ $t("fileReader.autoDetect") }}</option>
|
||||||
<option value="javascript">JavaScript</option>
|
<option value="javascript">JavaScript</option>
|
||||||
<option value="typescript">TypeScript</option>
|
<option value="typescript">TypeScript</option>
|
||||||
<option value="html">HTML</option>
|
<option value="html">HTML</option>
|
||||||
@ -89,7 +93,7 @@
|
|||||||
<option value="sql">SQL</option>
|
<option value="sql">SQL</option>
|
||||||
<option value="shell">Shell</option>
|
<option value="shell">Shell</option>
|
||||||
<option value="json">JSON</option>
|
<option value="json">JSON</option>
|
||||||
<option value="other">其他</option>
|
<option value="other">{{ $t("fileReader.other") }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isEditing" class="editor">
|
<div v-if="isEditing" class="editor">
|
||||||
@ -107,15 +111,15 @@
|
|||||||
<div>
|
<div>
|
||||||
{{
|
{{
|
||||||
fileTypeCheck === "unsupported-size"
|
fileTypeCheck === "unsupported-size"
|
||||||
? "文件过大,无法预览,请传输后查看"
|
? $t("fileReader.fileTooLarge")
|
||||||
: "不支持预览该类型的文件"
|
: $t("fileReader.unsupportedType")
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
v-if="fileTypeCheck != 'unsupported-size'"
|
v-if="fileTypeCheck != 'unsupported-size'"
|
||||||
@click="openWithText = true"
|
@click="openWithText = true"
|
||||||
>以文本打开</Button
|
>{{ $t("fileReader.openAsText") }}</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -125,6 +129,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed } from "vue";
|
import { ref, onMounted, onUnmounted, computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import {
|
import {
|
||||||
fileMgrInstance,
|
fileMgrInstance,
|
||||||
type FileData,
|
type FileData,
|
||||||
@ -136,6 +141,8 @@ import { Modal } from "ant-design-vue";
|
|||||||
import { localCurrentFile, remoteCurrentFile } from "../utils/common";
|
import { localCurrentFile, remoteCurrentFile } from "../utils/common";
|
||||||
import { NEED_CHUNK_FILE_SIZE_PREVIEW } from "../utils/fileTransfer";
|
import { NEED_CHUNK_FILE_SIZE_PREVIEW } from "../utils/fileTransfer";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
file: FileInfo;
|
file: FileInfo;
|
||||||
}>();
|
}>();
|
||||||
@ -167,19 +174,17 @@ const downloadFile = async () => {
|
|||||||
await props.file.getFile(
|
await props.file.getFile(
|
||||||
false,
|
false,
|
||||||
localCurrentFile.value.path.replace(
|
localCurrentFile.value.path.replace(
|
||||||
(
|
(await fileMgrInstance.getRootFile()).path,
|
||||||
await fileMgrInstance.getRootFile()
|
"",
|
||||||
).path,
|
|
||||||
""
|
|
||||||
),
|
),
|
||||||
remoteCurrentFile.value.path
|
remoteCurrentFile.value.path,
|
||||||
);
|
);
|
||||||
localCurrentFile.value.loadLocalDirectory();
|
localCurrentFile.value.loadLocalDirectory();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "下载文件",
|
title: t("fileReader.downloadTitle"),
|
||||||
content: `确定要下载${props.file.name}吗?`,
|
content: t("fileReader.downloadConfirm", { name: props.file.name }),
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
const blob = new Blob([loadedFile.value.buffer], {
|
const blob = new Blob([loadedFile.value.buffer], {
|
||||||
type: loadedFile.value.type || "application/octet-stream",
|
type: loadedFile.value.type || "application/octet-stream",
|
||||||
@ -313,7 +318,7 @@ const toggleEdit = async () => {
|
|||||||
isEditing.value = false;
|
isEditing.value = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("保存文件失败:", err);
|
console.error("保存文件失败:", err);
|
||||||
alert("保存文件失败: " + (err as Error).message);
|
alert(t("fileReader.saveFailed") + ": " + (err as Error).message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 进入编辑模式
|
// 进入编辑模式
|
||||||
@ -326,7 +331,7 @@ const toggleEdit = async () => {
|
|||||||
// 自动调整编辑器高度
|
// 自动调整编辑器高度
|
||||||
const adjustEditorHeight = () => {
|
const adjustEditorHeight = () => {
|
||||||
const textarea = document.querySelector(
|
const textarea = document.querySelector(
|
||||||
".editor textarea"
|
".editor textarea",
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
textarea.style.height = "auto";
|
textarea.style.height = "auto";
|
||||||
@ -388,7 +393,7 @@ const handleTouchStart = (e: TouchEvent) => {
|
|||||||
const touch2 = e.touches[1];
|
const touch2 = e.touches[1];
|
||||||
lastTouchDistance.value = Math.hypot(
|
lastTouchDistance.value = Math.hypot(
|
||||||
touch2.clientX - touch1.clientX,
|
touch2.clientX - touch1.clientX,
|
||||||
touch2.clientY - touch1.clientY
|
touch2.clientY - touch1.clientY,
|
||||||
);
|
);
|
||||||
} else if (e.touches.length === 1) {
|
} else if (e.touches.length === 1) {
|
||||||
isDragging.value = true;
|
isDragging.value = true;
|
||||||
@ -404,7 +409,7 @@ const handleTouchMove = (e: TouchEvent) => {
|
|||||||
const touch2 = e.touches[1];
|
const touch2 = e.touches[1];
|
||||||
const distance = Math.hypot(
|
const distance = Math.hypot(
|
||||||
touch2.clientX - touch1.clientX,
|
touch2.clientX - touch1.clientX,
|
||||||
touch2.clientY - touch1.clientY
|
touch2.clientY - touch1.clientY,
|
||||||
);
|
);
|
||||||
|
|
||||||
const delta = distance - lastTouchDistance.value;
|
const delta = distance - lastTouchDistance.value;
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="transfer-container">
|
<div class="transfer-container">
|
||||||
<div class="transfer-header">
|
<div class="transfer-header">
|
||||||
传输列表 ({{ tasks.length }})
|
{{ $t("transfer.title") }} ({{ tasks.length }})
|
||||||
<div class="transfer-header-btns">
|
<div class="transfer-header-btns">
|
||||||
<Button type="ghost" class="clear-btn" @click="clearTasks">清理</Button>
|
<Button type="ghost" class="clear-btn" @click="clearTasks">{{
|
||||||
|
$t("transfer.clear")
|
||||||
|
}}</Button>
|
||||||
<Button
|
<Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
class="toggle-btn"
|
class="toggle-btn"
|
||||||
@ -49,8 +51,11 @@ import {
|
|||||||
TransferStatus,
|
TransferStatus,
|
||||||
} from "../utils/fileTransfer";
|
} from "../utils/fileTransfer";
|
||||||
import { type Ref, ref } from "vue";
|
import { type Ref, ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import { formatSize } from "../utils/common";
|
import { formatSize } from "../utils/common";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const isExpanded = ref(true);
|
const isExpanded = ref(true);
|
||||||
const tasks: Ref<TransferTask[]> = ref([]);
|
const tasks: Ref<TransferTask[]> = ref([]);
|
||||||
|
|
||||||
@ -63,12 +68,12 @@ const clearTasks = () => {
|
|||||||
|
|
||||||
const getStatusText = (status: TransferStatus) => {
|
const getStatusText = (status: TransferStatus) => {
|
||||||
const statusMap: Record<TransferStatus, string> = {
|
const statusMap: Record<TransferStatus, string> = {
|
||||||
[TransferStatus.WAITING]: "等待中",
|
[TransferStatus.WAITING]: t("transfer.waiting"),
|
||||||
[TransferStatus.SENDING]: "发送中",
|
[TransferStatus.SENDING]: t("transfer.sending"),
|
||||||
[TransferStatus.RECEIVING]: "接收中",
|
[TransferStatus.RECEIVING]: t("transfer.receiving"),
|
||||||
[TransferStatus.COMPLETED]: "已完成",
|
[TransferStatus.COMPLETED]: t("transfer.completed"),
|
||||||
[TransferStatus.ERROR]: "失败",
|
[TransferStatus.ERROR]: t("transfer.error"),
|
||||||
[TransferStatus.PAUSED]: "已暂停",
|
[TransferStatus.PAUSED]: t("transfer.paused"),
|
||||||
};
|
};
|
||||||
return statusMap[status] || status;
|
return statusMap[status] || status;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,17 +3,25 @@
|
|||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="path-nav">
|
<div class="path-nav">
|
||||||
<Button @click="navigateLocal('..')" :disabled="!canNavigateUpLocal">
|
<Button @click="navigateLocal('..')" :disabled="!canNavigateUpLocal">
|
||||||
上级目录
|
{{ $t("fileView.parentDir") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Select class="sort-select" :value="sortType" @change="refreshLocal">
|
<Select class="sort-select" :value="sortType" @change="refreshLocal">
|
||||||
<SelectOption value="name">按名称</SelectOption>
|
<SelectOption value="name">{{
|
||||||
<SelectOption value="size">按大小</SelectOption>
|
$t("fileView.sortByName")
|
||||||
<SelectOption value="type">按类型</SelectOption>
|
}}</SelectOption>
|
||||||
<SelectOption value="date">按日期</SelectOption>
|
<SelectOption value="size">{{
|
||||||
|
$t("fileView.sortBySize")
|
||||||
|
}}</SelectOption>
|
||||||
|
<SelectOption value="type">{{
|
||||||
|
$t("fileView.sortByType")
|
||||||
|
}}</SelectOption>
|
||||||
|
<SelectOption value="date">{{
|
||||||
|
$t("fileView.sortByDate")
|
||||||
|
}}</SelectOption>
|
||||||
</Select>
|
</Select>
|
||||||
<Button danger v-if="!isRemote" @click="showPermissionSetting"
|
<Button danger v-if="!isRemote" @click="showPermissionSetting">{{
|
||||||
>权限设置</Button
|
$t("fileView.permissionSettings")
|
||||||
>
|
}}</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
@ -28,13 +36,13 @@
|
|||||||
style="text-align: center"
|
style="text-align: center"
|
||||||
v-if="isRemote && !currentDirInfo?.isInit"
|
v-if="isRemote && !currentDirInfo?.isInit"
|
||||||
>
|
>
|
||||||
<Button @click="refreshLocal">初始化</Button>
|
<Button @click="refreshLocal">{{ $t("fileView.initialize") }}</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!currentDirInfo?.isInit">
|
<div v-else-if="!currentDirInfo?.isInit">
|
||||||
<div @click="selectLocalDirectory" class="file-item">
|
<div @click="selectLocalDirectory" class="file-item">
|
||||||
<div class="file-icon">📁</div>
|
<div class="file-icon">📁</div>
|
||||||
<div class="file-info">
|
<div class="file-info">
|
||||||
<div class="file-name">请选择一个目录</div>
|
<div class="file-name">{{ $t("fileView.selectDirHint") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -49,37 +57,51 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentDirInfo?.isInit" class="panel-footer">
|
<div v-if="currentDirInfo?.isInit" class="panel-footer">
|
||||||
<Button v-if="!isRemote" @click="selectLocalDirectory">选择目录</Button>
|
<Button v-if="!isRemote" @click="selectLocalDirectory">{{
|
||||||
<Button @click="refreshLocal">刷新</Button>
|
$t("fileView.selectDirectory")
|
||||||
<Button @click="createNewFolder">新建文件夹</Button>
|
}}</Button>
|
||||||
<Button @click="createNewFile">新建文件</Button>
|
<Button @click="refreshLocal">{{ $t("fileView.refresh") }}</Button>
|
||||||
|
<Button @click="createNewFolder">{{ $t("fileView.newFolder") }}</Button>
|
||||||
|
<Button @click="createNewFile">{{ $t("fileView.newFile") }}</Button>
|
||||||
<Button @click="deleteSelected" :disabled="!hasSelectedFiles">
|
<Button @click="deleteSelected" :disabled="!hasSelectedFiles">
|
||||||
删除
|
{{ $t("fileView.delete") }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右键菜单 -->
|
<!-- 右键菜单 -->
|
||||||
<div v-if="showMenu" class="context-menu" :style="menuPosition">
|
<div v-if="showMenu" class="context-menu" :style="menuPosition">
|
||||||
<div v-if="activeFile">
|
<div v-if="activeFile">
|
||||||
<div class="menu-item" @click="handleRename">重命名</div>
|
<div class="menu-item" @click="handleRename">
|
||||||
<div class="menu-item" @click="handleDelete">删除</div>
|
{{ $t("fileView.rename") }}
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="handleDelete">
|
||||||
|
{{ $t("fileView.delete") }}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="menu-item"
|
class="menu-item"
|
||||||
v-if="props.isRemote && remoteCurrentFile?.isInit"
|
v-if="props.isRemote && remoteCurrentFile?.isInit"
|
||||||
@click="handleDownload"
|
@click="handleDownload"
|
||||||
>
|
>
|
||||||
下载
|
{{ $t("fileView.download") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!currentDirInfo?.isInit">
|
<div v-else-if="!currentDirInfo?.isInit">
|
||||||
<div class="menu-item" @click="selectLocalDirectory">选择本地目录</div>
|
<div class="menu-item" @click="selectLocalDirectory">
|
||||||
|
{{ $t("fileView.selectLocalDir") }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="menu-item" @click="refreshLocal">刷新</div>
|
<div class="menu-item" @click="refreshLocal">
|
||||||
<div class="menu-item" @click="createNewFolder">新建文件夹</div>
|
{{ $t("fileView.refresh") }}
|
||||||
<div class="menu-item" @click="createNewFile">新建文件</div>
|
</div>
|
||||||
|
<div class="menu-item" @click="createNewFolder">
|
||||||
|
{{ $t("fileView.newFolder") }}
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="createNewFile">
|
||||||
|
{{ $t("fileView.newFile") }}
|
||||||
|
</div>
|
||||||
<div class="menu-item" @click="deleteSelected" v-if="hasSelectedFiles">
|
<div class="menu-item" @click="deleteSelected" v-if="hasSelectedFiles">
|
||||||
删除
|
{{ $t("fileView.delete") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -106,6 +128,7 @@ import {
|
|||||||
h,
|
h,
|
||||||
reactive,
|
reactive,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import { Button, Select, SelectOption, Modal } from "ant-design-vue";
|
import { Button, Select, SelectOption, Modal } from "ant-design-vue";
|
||||||
import { FileInfo, fileMgrInstance } from "../utils/fileMgr";
|
import { FileInfo, fileMgrInstance } from "../utils/fileMgr";
|
||||||
import FileItem from "./fileItem.vue";
|
import FileItem from "./fileItem.vue";
|
||||||
@ -114,6 +137,8 @@ import { Breadcrumb, BreadcrumbItem } from "ant-design-vue";
|
|||||||
import emitter, { EmitterEvent } from "../utils/emitter";
|
import emitter, { EmitterEvent } from "../utils/emitter";
|
||||||
import { localCurrentFile, remoteCurrentFile } from "../utils/common";
|
import { localCurrentFile, remoteCurrentFile } from "../utils/common";
|
||||||
import FilePermissionSetDialog from "./filePermissionSetDialog.vue";
|
import FilePermissionSetDialog from "./filePermissionSetDialog.vue";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isRemote: boolean;
|
isRemote: boolean;
|
||||||
selectedFiles: string[];
|
selectedFiles: string[];
|
||||||
@ -173,12 +198,10 @@ const handleDownload = async () => {
|
|||||||
await file.getFile(
|
await file.getFile(
|
||||||
false,
|
false,
|
||||||
localCurrentFile.value.path.replace(
|
localCurrentFile.value.path.replace(
|
||||||
(
|
(await fileMgrInstance.getRootFile()).path,
|
||||||
await fileMgrInstance.getRootFile()
|
"",
|
||||||
).path,
|
|
||||||
""
|
|
||||||
),
|
),
|
||||||
remoteCurrentFile.value.path
|
remoteCurrentFile.value.path,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -274,12 +297,12 @@ const handleRename = async () => {
|
|||||||
newFileName.value = activeFile.value.name;
|
newFileName.value = activeFile.value.name;
|
||||||
const acf = activeFile.value;
|
const acf = activeFile.value;
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "重命名",
|
title: t("fileView.renameTitle"),
|
||||||
class: "custom-modal",
|
class: "custom-modal",
|
||||||
content: h("div", [
|
content: h("div", [
|
||||||
h("input", {
|
h("input", {
|
||||||
class: "ant-input",
|
class: "ant-input",
|
||||||
placeholder: "请输入新的名称",
|
placeholder: t("fileView.inputNewName"),
|
||||||
value: newFileName.value,
|
value: newFileName.value,
|
||||||
onInput: (e: Event) => {
|
onInput: (e: Event) => {
|
||||||
newFileName.value = (e.target as HTMLInputElement).value;
|
newFileName.value = (e.target as HTMLInputElement).value;
|
||||||
@ -290,8 +313,8 @@ const handleRename = async () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
okText: "确定",
|
okText: t("common.okText"),
|
||||||
cancelText: "取消",
|
cancelText: t("common.cancelText"),
|
||||||
async onOk() {
|
async onOk() {
|
||||||
if (!newFileName.value || newFileName.value === acf.name) {
|
if (!newFileName.value || newFileName.value === acf.name) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
@ -302,7 +325,7 @@ const handleRename = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("重命名失败:", err);
|
console.error("重命名失败:", err);
|
||||||
Modal.error({
|
Modal.error({
|
||||||
title: "重命名失败",
|
title: t("fileView.renameTitle") + t("common.failed"),
|
||||||
content: (err as Error).message || (err as string),
|
content: (err as Error).message || (err as string),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -327,13 +350,14 @@ const deleteSelected = async () => {
|
|||||||
if (!selectedLocalFiles.value.length) return;
|
if (!selectedLocalFiles.value.length) return;
|
||||||
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "确认删除",
|
title: t("fileView.confirmDelete"),
|
||||||
content: `确定要删除以下文件吗?<br>${selectedLocalFiles.value.join(
|
content: t("fileView.deleteConfirmContent", {
|
||||||
"<br>"
|
files: selectedLocalFiles.value.join("<br>"),
|
||||||
)}<br>共 ${selectedLocalFiles.value.length} 个文件`,
|
count: selectedLocalFiles.value.length,
|
||||||
okText: "删除",
|
}),
|
||||||
|
okText: t("fileView.deleteBtn"),
|
||||||
okType: "danger",
|
okType: "danger",
|
||||||
cancelText: "取消",
|
cancelText: t("fileView.cancelBtn"),
|
||||||
async onOk() {
|
async onOk() {
|
||||||
try {
|
try {
|
||||||
for (const fileName of selectedLocalFiles.value) {
|
for (const fileName of selectedLocalFiles.value) {
|
||||||
@ -347,7 +371,7 @@ const deleteSelected = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("批量删除失败:", err);
|
console.error("批量删除失败:", err);
|
||||||
Modal.error({
|
Modal.error({
|
||||||
title: "删除失败",
|
title: t("common.delete") + t("common.failed"),
|
||||||
content: (err as Error).message,
|
content: (err as Error).message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -361,12 +385,12 @@ const createNewFolder = async () => {
|
|||||||
|
|
||||||
folderName.value = "";
|
folderName.value = "";
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "新建文件夹",
|
title: t("fileView.createFolderTitle"),
|
||||||
class: "custom-modal",
|
class: "custom-modal",
|
||||||
content: h("div", [
|
content: h("div", [
|
||||||
h("input", {
|
h("input", {
|
||||||
class: "ant-input",
|
class: "ant-input",
|
||||||
placeholder: "请输入文件夹名称",
|
placeholder: t("fileView.inputFolderName"),
|
||||||
value: folderName.value,
|
value: folderName.value,
|
||||||
onInput: (e: Event) => {
|
onInput: (e: Event) => {
|
||||||
folderName.value = (e.target as HTMLInputElement).value;
|
folderName.value = (e.target as HTMLInputElement).value;
|
||||||
@ -377,13 +401,13 @@ const createNewFolder = async () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
okText: "创建",
|
okText: t("fileView.createBtn"),
|
||||||
cancelText: "取消",
|
cancelText: t("fileView.cancelBtn"),
|
||||||
async onOk() {
|
async onOk() {
|
||||||
if (!folderName.value) {
|
if (!folderName.value) {
|
||||||
Modal.warning({
|
Modal.warning({
|
||||||
title: "提示",
|
title: t("common.warning"),
|
||||||
content: "请输入文件夹名称",
|
content: t("fileView.warningInputFolderName"),
|
||||||
});
|
});
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
@ -393,7 +417,7 @@ const createNewFolder = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("创建文件夹失败:", err);
|
console.error("创建文件夹失败:", err);
|
||||||
Modal.error({
|
Modal.error({
|
||||||
title: "创建失败",
|
title: t("fileView.createFolderTitle") + t("common.failed"),
|
||||||
content: (err as Error).message,
|
content: (err as Error).message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -407,12 +431,12 @@ const createNewFile = async () => {
|
|||||||
|
|
||||||
fileName.value = "";
|
fileName.value = "";
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "新建文件",
|
title: t("fileView.createFileTitle"),
|
||||||
class: "custom-modal",
|
class: "custom-modal",
|
||||||
content: h("div", [
|
content: h("div", [
|
||||||
h("input", {
|
h("input", {
|
||||||
class: "ant-input",
|
class: "ant-input",
|
||||||
placeholder: "请输入文件名称",
|
placeholder: t("fileView.inputFileName"),
|
||||||
value: fileName.value,
|
value: fileName.value,
|
||||||
onInput: (e: Event) => {
|
onInput: (e: Event) => {
|
||||||
fileName.value = (e.target as HTMLInputElement).value;
|
fileName.value = (e.target as HTMLInputElement).value;
|
||||||
@ -423,13 +447,13 @@ const createNewFile = async () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
okText: "创建",
|
okText: t("fileView.createBtn"),
|
||||||
cancelText: "取消",
|
cancelText: t("fileView.cancelBtn"),
|
||||||
async onOk() {
|
async onOk() {
|
||||||
if (!fileName.value) {
|
if (!fileName.value) {
|
||||||
Modal.warning({
|
Modal.warning({
|
||||||
title: "提示",
|
title: t("common.warning"),
|
||||||
content: "请输入文件名称",
|
content: t("fileView.warningInputFileName"),
|
||||||
});
|
});
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
@ -440,7 +464,7 @@ const createNewFile = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("创建文件失败:", err);
|
console.error("创建文件失败:", err);
|
||||||
Modal.error({
|
Modal.error({
|
||||||
title: "创建失败",
|
title: t("fileView.createFileTitle") + t("common.failed"),
|
||||||
content: (err as Error).message,
|
content: (err as Error).message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
style="display: none"
|
style="display: none"
|
||||||
/>
|
/>
|
||||||
<button class="upload-btn" @click="triggerFileUpload">
|
<button class="upload-btn" @click="triggerFileUpload">
|
||||||
上传图片/视频
|
{{ $t('shader.uploadImageVideo') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,185 +3,197 @@
|
|||||||
<div class="call-status">
|
<div class="call-status">
|
||||||
<h2>{{ callStatus }}</h2>
|
<h2>{{ callStatus }}</h2>
|
||||||
<div v-if="myPeerId" class="peer-id">
|
<div v-if="myPeerId" class="peer-id">
|
||||||
我的ID: {{ myPeerId }}
|
{{ $t("voice.myId") }}: {{ myPeerId }}
|
||||||
</div>
|
</div>
|
||||||
<button @click="copyPeerId">复制</button>
|
<button @click="copyPeerId">{{ $t("voice.copy") }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="delay-time">
|
<div class="delay-time">
|
||||||
<h2>延迟时间: {{ delayTime + 'ms' }}</h2>
|
<h2>{{ $t("voice.delay") }}: {{ delayTime + "ms" }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="connection-controls" v-if="!isInCall">
|
<div class="connection-controls" v-if="!isInCall">
|
||||||
<input type="text" v-model="remotePeerId" placeholder="输入对方的ID" :disabled="isInCall" />
|
<input
|
||||||
<button type="reset" class="call-btn start-call" @click="startCall" :disabled="!remotePeerId || isInCall">
|
type="text"
|
||||||
呼叫
|
v-model="remotePeerId"
|
||||||
|
:placeholder="$t('voice.enterPeerId')"
|
||||||
|
:disabled="isInCall"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
class="call-btn start-call"
|
||||||
|
@click="startCall"
|
||||||
|
:disabled="!remotePeerId || isInCall"
|
||||||
|
>
|
||||||
|
{{ $t("voice.call") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="call-controls" v-if="isInCall">
|
<div class="call-controls" v-if="isInCall">
|
||||||
<button class="call-btn end-call" @click="endCall">
|
<button class="call-btn end-call" @click="endCall">
|
||||||
结束通话
|
{{ $t("voice.endCall") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="mute-btn" :class="{ muted: isMuted }" @click="toggleMute">
|
<button class="mute-btn" :class="{ muted: isMuted }" @click="toggleMute">
|
||||||
{{ isMuted ? '取消静音' : '静音' }}
|
{{ isMuted ? $t("voice.unmute") : $t("voice.mute") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
import Peer, { type MediaConnection } from 'peerjs'
|
import Peer, { type MediaConnection } from "peerjs";
|
||||||
|
|
||||||
const myPeerId = ref('')
|
import { useI18n } from "vue-i18n";
|
||||||
const remotePeerId = ref('')
|
|
||||||
const isInCall = ref(false)
|
const { t } = useI18n();
|
||||||
const isMuted = ref(false)
|
|
||||||
const callStatus = ref('准备连接...')
|
const myPeerId = ref("");
|
||||||
const localStream = ref<MediaStream | null>(null)
|
const remotePeerId = ref("");
|
||||||
const peer = ref<Peer | null>(null)
|
const isInCall = ref(false);
|
||||||
const currentCall = ref<MediaConnection | null>(null)
|
const isMuted = ref(false);
|
||||||
const delayTime = ref(0)
|
const callStatus = ref(t("voice.preparing"));
|
||||||
let statsInterval: number | null = null
|
const localStream = ref<MediaStream | null>(null);
|
||||||
|
const peer = ref<Peer | null>(null);
|
||||||
|
const currentCall = ref<MediaConnection | null>(null);
|
||||||
|
const delayTime = ref(0);
|
||||||
|
let statsInterval: number | null = null;
|
||||||
|
|
||||||
const initPeer = () => {
|
const initPeer = () => {
|
||||||
peer.value = new Peer(
|
peer.value = new Peer({
|
||||||
{
|
|
||||||
config: {
|
config: {
|
||||||
iceServers: [
|
iceServers: [{ urls: "stun:8.134.35.244:3478" }],
|
||||||
{ urls: 'stun:8.134.35.244:3478' }
|
},
|
||||||
]
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
peer.value.on('open', (id) => {
|
peer.value.on("open", (id) => {
|
||||||
myPeerId.value = id
|
myPeerId.value = id;
|
||||||
callStatus.value = '准备就绪'
|
callStatus.value = t("voice.ready");
|
||||||
})
|
});
|
||||||
|
|
||||||
peer.value.on('call', async (call) => {
|
peer.value.on("call", async (call) => {
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: true,
|
audio: true,
|
||||||
video: false
|
video: false,
|
||||||
})
|
});
|
||||||
|
|
||||||
localStream.value = stream
|
localStream.value = stream;
|
||||||
call.answer(stream)
|
call.answer(stream);
|
||||||
handleCall(call)
|
handleCall(call);
|
||||||
|
|
||||||
isInCall.value = true
|
isInCall.value = true;
|
||||||
callStatus.value = '通话中'
|
callStatus.value = t("voice.inCall");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取音频设备失败:', error)
|
console.error("获取音频设备失败:", error);
|
||||||
callStatus.value = '无法访问麦克风'
|
callStatus.value = t("voice.cannotAccessMic");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
peer.value.on('error', (error) => {
|
peer.value.on("error", (error) => {
|
||||||
console.error('PeerJS错误:', error)
|
console.error("PeerJS错误:", error);
|
||||||
callStatus.value = '连接错误'
|
callStatus.value = t("voice.connectionError");
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
const copyPeerId = () => {
|
const copyPeerId = () => {
|
||||||
navigator.clipboard.writeText(myPeerId.value)
|
navigator.clipboard.writeText(myPeerId.value);
|
||||||
}
|
};
|
||||||
const updateDelayTime = async () => {
|
const updateDelayTime = async () => {
|
||||||
delayTime.value = 0
|
delayTime.value = 0;
|
||||||
if (currentCall.value && currentCall.value.peerConnection) {
|
if (currentCall.value && currentCall.value.peerConnection) {
|
||||||
const stats = await currentCall.value.peerConnection.getStats()
|
const stats = await currentCall.value.peerConnection.getStats();
|
||||||
stats.forEach(report => {
|
stats.forEach((report) => {
|
||||||
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
|
if (report.type === "candidate-pair" && report.state === "succeeded") {
|
||||||
delayTime.value += report.currentRoundTripTime * 1000 //currentRoundTripTime是指往返时间
|
delayTime.value += report.currentRoundTripTime * 1000; //currentRoundTripTime是指往返时间
|
||||||
}
|
}
|
||||||
if (report.type === 'inbound-rtp' && report.kind === 'audio') {
|
if (report.type === "inbound-rtp" && report.kind === "audio") {
|
||||||
delayTime.value += Math.round(
|
delayTime.value += Math.round(
|
||||||
report.jitter * 1000 || 0 + // 抖动缓冲区变化
|
report.jitter * 1000 ||
|
||||||
report.playoutDelay || 0 // 播放延迟
|
0 + // 抖动缓冲区变化
|
||||||
)
|
report.playoutDelay ||
|
||||||
|
0, // 播放延迟
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
const handleCall = (call: MediaConnection) => {
|
const handleCall = (call: MediaConnection) => {
|
||||||
currentCall.value = call
|
currentCall.value = call;
|
||||||
|
|
||||||
statsInterval = window.setInterval(updateDelayTime, 1000)
|
statsInterval = window.setInterval(updateDelayTime, 1000);
|
||||||
|
|
||||||
call.on('stream', (remoteStream) => {
|
call.on("stream", (remoteStream) => {
|
||||||
const audio = new Audio()
|
const audio = new Audio();
|
||||||
audio.srcObject = remoteStream
|
audio.srcObject = remoteStream;
|
||||||
audio.play()
|
audio.play();
|
||||||
})
|
});
|
||||||
|
|
||||||
call.on('close', () => {
|
call.on("close", () => {
|
||||||
endCall()
|
endCall();
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const startCall = async () => {
|
const startCall = async () => {
|
||||||
if (!peer.value || !remotePeerId.value) return
|
if (!peer.value || !remotePeerId.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: true,
|
audio: true,
|
||||||
video: false
|
video: false,
|
||||||
})
|
});
|
||||||
|
|
||||||
localStream.value = stream
|
localStream.value = stream;
|
||||||
const call = peer.value.call(remotePeerId.value, stream)
|
const call = peer.value.call(remotePeerId.value, stream);
|
||||||
handleCall(call)
|
handleCall(call);
|
||||||
|
|
||||||
isInCall.value = true
|
isInCall.value = true;
|
||||||
callStatus.value = '通话中'
|
callStatus.value = t("voice.inCall");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取音频设备失败:', error)
|
console.error("获取音频设备失败:", error);
|
||||||
callStatus.value = '无法访问麦克风'
|
callStatus.value = t("voice.cannotAccessMic");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const toggleMute = () => {
|
const toggleMute = () => {
|
||||||
if (localStream.value) {
|
if (localStream.value) {
|
||||||
const audioTrack = localStream.value.getAudioTracks()[0]
|
const audioTrack = localStream.value.getAudioTracks()[0];
|
||||||
isMuted.value = !isMuted.value
|
isMuted.value = !isMuted.value;
|
||||||
audioTrack.enabled = !isMuted.value
|
audioTrack.enabled = !isMuted.value;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const endCall = () => {
|
const endCall = () => {
|
||||||
if (statsInterval) {
|
if (statsInterval) {
|
||||||
clearInterval(statsInterval)
|
clearInterval(statsInterval);
|
||||||
statsInterval = null
|
statsInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentCall.value) {
|
if (currentCall.value) {
|
||||||
currentCall.value.close()
|
currentCall.value.close();
|
||||||
currentCall.value = null
|
currentCall.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localStream.value) {
|
if (localStream.value) {
|
||||||
localStream.value.getTracks().forEach(track => track.stop())
|
localStream.value.getTracks().forEach((track) => track.stop());
|
||||||
localStream.value = null
|
localStream.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
isInCall.value = false
|
isInCall.value = false;
|
||||||
isMuted.value = false
|
isMuted.value = false;
|
||||||
callStatus.value = '准备就绪'
|
callStatus.value = t("voice.ready");
|
||||||
delayTime.value = 0 // 重置延迟时间
|
delayTime.value = 0;
|
||||||
}
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initPeer()
|
initPeer();
|
||||||
})
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
endCall()
|
endCall();
|
||||||
if (peer.value) {
|
if (peer.value) {
|
||||||
peer.value.destroy()
|
peer.value.destroy();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -230,7 +242,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connection-controls input:focus {
|
.connection-controls input:focus {
|
||||||
border-color: #2196F3;
|
border-color: #2196f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-controls {
|
.call-controls {
|
||||||
@ -259,7 +271,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.start-call {
|
.start-call {
|
||||||
background-color: #4CAF50;
|
background-color: #4caf50;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,12 +281,12 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mute-btn {
|
.mute-btn {
|
||||||
background-color: #2196F3;
|
background-color: #2196f3;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mute-btn.muted {
|
.mute-btn.muted {
|
||||||
background-color: #9E9E9E;
|
background-color: #9e9e9e;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover:not(:disabled) {
|
button:hover:not(:disabled) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user