新增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 Antd from 'ant-design-vue';
|
||||
import router from './router/router';
|
||||
import i18n from './i18n';
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { isPwa, showPwaInstallPrompt } from './utils/pwa';
|
||||
|
||||
@ -25,4 +26,5 @@ const updateSW = registerSW({
|
||||
const app = createApp(App);
|
||||
app.use(Antd);
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
app.mount('#app')
|
||||
|
||||
@ -4,84 +4,127 @@
|
||||
<!-- 连接状态 -->
|
||||
<div class="status-bar">
|
||||
<div class="peer-info">
|
||||
我的ID: <span class="id-text">{{ myId || "等待连接..." }}</span>
|
||||
<Button type="primary" @click="copyId" v-if="myId">复制</Button>
|
||||
<Button type="link" @click="shareUrl" v-if="myId">分享</Button>
|
||||
{{ $t("index.myId") }}:
|
||||
<span class="id-text">{{
|
||||
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
|
||||
v-if="connectedPeerId"
|
||||
class="connection-badge"
|
||||
:class="{ inbound: isInboundConnected }"
|
||||
>
|
||||
{{ isInboundConnected ? "被连接" : "已连接" }}:
|
||||
{{
|
||||
isInboundConnected
|
||||
? $t("index.connectedBy")
|
||||
: $t("index.connected")
|
||||
}}:
|
||||
<span class="connected-peer">{{ connectedPeerLabel }}</span>
|
||||
<Button
|
||||
v-if="isInboundConnected && connectedPeerSign"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="copyPeerSign"
|
||||
>复制</Button
|
||||
>{{ $t("index.copy") }}</Button
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 显示流量 丢包率-->
|
||||
<div class="status-info">
|
||||
<div class="status-item">
|
||||
<span>流量:</span>
|
||||
<span>{{ $t("index.traffic") }}:</span>
|
||||
<span>{{ formatSize(transInfo.bytes) }}/s</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>包数:</span>
|
||||
<span>{{ $t("index.packets") }}:</span>
|
||||
<span>{{ transInfo.packets }}/s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connect-section">
|
||||
<div class="connect-actions">
|
||||
<div
|
||||
class="connect-item"
|
||||
:class="{ disabled: !isConnected, active: isDesktopActive }"
|
||||
title="桌面预览"
|
||||
@click="requestDesktop"
|
||||
>
|
||||
<img src="/static/desktop.png" alt="桌面" />
|
||||
</div>
|
||||
<div
|
||||
class="connect-item"
|
||||
:class="{ disabled: !isConnected, active: isCallActive }"
|
||||
title="语音通话"
|
||||
@click="requestCall"
|
||||
>
|
||||
<img src="/static/phone.png" alt="通话" />
|
||||
</div>
|
||||
<div
|
||||
class="connect-item"
|
||||
:class="{ disabled: !isConnected, active: isCameraActive }"
|
||||
title="摄像头"
|
||||
@click="requestCamera"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
<div class="status-actions">
|
||||
<div class="connect-section">
|
||||
<div class="connect-actions">
|
||||
<div
|
||||
class="connect-item"
|
||||
:class="{ disabled: !isConnected, active: isDesktopActive }"
|
||||
:title="$t('index.desktopPreview')"
|
||||
@click="requestDesktop"
|
||||
>
|
||||
<path d="M23 7l-7 5 7 5V7z" />
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
||||
<img
|
||||
src="/static/desktop.png"
|
||||
:alt="$t('index.desktopPreview')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="connect-item"
|
||||
:class="{ disabled: !isConnected, active: isCallActive }"
|
||||
:title="$t('index.voiceCall')"
|
||||
@click="requestCall"
|
||||
>
|
||||
<img src="/static/phone.png" :alt="$t('index.voiceCall')" />
|
||||
</div>
|
||||
<div
|
||||
class="connect-item"
|
||||
:class="{ disabled: !isConnected, active: isCameraActive }"
|
||||
:title="$t('index.camera')"
|
||||
@click="requestCamera"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M23 7l-7 5 7 5V7z" />
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="targetId"
|
||||
:placeholder="
|
||||
!myId ? $t('common.pleaseWait') : $t('index.enterPeerId')
|
||||
"
|
||||
:disabled="isConnected || !myId"
|
||||
/>
|
||||
<Button
|
||||
@click="handleConnect"
|
||||
:disabled="!targetId || isConnected"
|
||||
:loading="isConnecting"
|
||||
>
|
||||
{{ isConnected ? $t("index.connectedBtn") : $t("index.connect") }}
|
||||
</Button>
|
||||
</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>
|
||||
</div>
|
||||
<input
|
||||
v-model="targetId"
|
||||
:placeholder="!myId ? '请稍后...' : '输入对方ID'"
|
||||
:disabled="isConnected || !myId"
|
||||
/>
|
||||
<Button
|
||||
@click="handleConnect"
|
||||
:disabled="!targetId || isConnected"
|
||||
:loading="isConnecting"
|
||||
>
|
||||
{{ isConnected ? "已连接" : "连接" }}
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
@ -106,7 +149,7 @@
|
||||
:disabled="!isConnected || selectedLocalFiles.length === 0"
|
||||
@click="handleSend"
|
||||
>
|
||||
发送
|
||||
{{ $t("index.send") }}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@ -114,7 +157,7 @@
|
||||
@click="handleReceive"
|
||||
:loading="receiveLoading"
|
||||
>
|
||||
接收
|
||||
{{ $t("index.receive") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -123,7 +166,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<FileTransferView />
|
||||
</template>
|
||||
@ -131,6 +176,7 @@
|
||||
<script setup lang="ts">
|
||||
import Clipboard from "./item/clipboard.vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { peer } from "./utils/peer";
|
||||
import FileView from "./item/fileView.vue";
|
||||
import { notification } from "ant-design-vue";
|
||||
@ -146,6 +192,12 @@ import {
|
||||
} from "./utils/common";
|
||||
import FileTransferView from "./item/fileTranserView.vue";
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
|
||||
const handleLangMenuClick = ({ key }: { key: string }) => {
|
||||
locale.value = key;
|
||||
};
|
||||
|
||||
const isPhone = ref(false);
|
||||
const receiveLoading = ref(false);
|
||||
const myId = ref("");
|
||||
@ -198,8 +250,8 @@ const shareUrl = async () => {
|
||||
window.location.origin + window.location.pathname + "?sign=" + myId.value;
|
||||
await copyToClipboard(url);
|
||||
notification.success({
|
||||
message: "链接已复制",
|
||||
description: "已成功复制到剪贴板",
|
||||
message: t("index.linkCopied"),
|
||||
description: t("index.linkCopiedDesc"),
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -210,12 +262,12 @@ const copyId = async () => {
|
||||
try {
|
||||
await copyToClipboard(myId.value);
|
||||
notification.success({
|
||||
message: "ID已复制",
|
||||
description: "已成功复制到剪贴板",
|
||||
message: t("index.idCopied"),
|
||||
description: t("index.idCopiedDesc"),
|
||||
});
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: "复制失败",
|
||||
message: t("index.copyFailed"),
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
@ -227,11 +279,11 @@ const copyPeerSign = async () => {
|
||||
try {
|
||||
await copyToClipboard(connectedPeerSign.value);
|
||||
notification.success({
|
||||
message: "ID已复制",
|
||||
description: "已成功复制到剪贴板,可用于发起连接",
|
||||
message: t("index.idCopied"),
|
||||
description: t("index.idCopiedConnectDesc"),
|
||||
});
|
||||
} catch {
|
||||
notification.error({ message: "复制失败" });
|
||||
notification.error({ message: t("index.copyFailed") });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -289,8 +341,8 @@ const checkBytes = () => {
|
||||
};
|
||||
const handleSend = async () => {
|
||||
notification.info({
|
||||
message: "未完成",
|
||||
description: "开发中...",
|
||||
message: t("index.notCompleted"),
|
||||
description: t("index.inDevelopment"),
|
||||
});
|
||||
};
|
||||
const handleReceive = async () => {
|
||||
@ -313,7 +365,7 @@ const handleReceive = async () => {
|
||||
await Promise.all(getFileHandles);
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: "接收文件失败",
|
||||
message: t("index.receiveFilesFailed"),
|
||||
});
|
||||
}
|
||||
receiveLoading.value = false;
|
||||
@ -338,12 +390,12 @@ onMounted(async () => {
|
||||
updateConnectedPeer(event.detail.peer, false);
|
||||
fileMgrInstance.remoteRootFile.loadLocalDirectory();
|
||||
notification.success({
|
||||
message: "连接成功",
|
||||
message: t("index.connectSuccess"),
|
||||
});
|
||||
} else {
|
||||
updateConnectedPeer(event.detail.peer, true);
|
||||
notification.success({
|
||||
message: "出现新的连接",
|
||||
message: t("index.newConnection"),
|
||||
});
|
||||
}
|
||||
}) as EventListener);
|
||||
@ -354,7 +406,7 @@ onMounted(async () => {
|
||||
isCallActive.value = false;
|
||||
isCameraActive.value = false;
|
||||
notification.error({
|
||||
message: event.detail.peer + "连接已断开",
|
||||
message: event.detail.peer + t("index.disconnected"),
|
||||
});
|
||||
}) as EventListener);
|
||||
|
||||
@ -370,7 +422,7 @@ onMounted(async () => {
|
||||
peer.on("error", ((event: CustomEvent) => {
|
||||
isConnecting.value = false;
|
||||
notification.error({
|
||||
message: "发生错误",
|
||||
message: t("index.errorOccurred"),
|
||||
});
|
||||
console.error("连接错误:", event.detail);
|
||||
}) as EventListener);
|
||||
@ -428,8 +480,9 @@ onMounted(async () => {
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
@ -439,6 +492,7 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.connection-badge {
|
||||
@ -497,24 +551,74 @@ onMounted(async () => {
|
||||
.connect-item.disabled img {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.connect-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.connect-item svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin: auto;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-actions,
|
||||
.connect-actions,
|
||||
.connect-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@ -666,21 +770,33 @@ input:disabled {
|
||||
@media screen and (max-width: 768px) {
|
||||
.status-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.peer-info,
|
||||
.status-info,
|
||||
.status-actions,
|
||||
.connect-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-actions {
|
||||
margin-left: 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.connect-section input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.lang-switcher {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -14,14 +14,14 @@
|
||||
<div v-else>
|
||||
<div class="clipboard-header">
|
||||
<div class="clipboard-actions">
|
||||
<Button @click="getRemoteClipboard" :loading="loading"
|
||||
>获取对方粘贴板</Button
|
||||
>
|
||||
<Button @click="getRemoteClipboard" :loading="loading">{{
|
||||
$t("clipboard.getRemoteClipboard")
|
||||
}}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clipboard-content">
|
||||
<div class="clipboard-text">
|
||||
{{ remoteClipboard || "暂无内容" }}
|
||||
{{ remoteClipboard || $t("clipboard.noContent") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -31,10 +31,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "ant-design-vue";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { peer, MessageType } from "../utils/peer";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(false);
|
||||
const remoteClipboard = ref("");
|
||||
const mouseEntering = ref(false);
|
||||
@ -49,12 +52,12 @@ const getRemoteClipboard = async () => {
|
||||
});
|
||||
remoteClipboard.value = result;
|
||||
notification.success({
|
||||
message: "获取成功",
|
||||
message: t("clipboard.fetchSuccess"),
|
||||
});
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: "获取失败",
|
||||
description: error?.message || "未知错误",
|
||||
message: t("clipboard.fetchFailed"),
|
||||
description: error?.message || t("clipboard.unknownError"),
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@ -65,7 +68,9 @@ const getRemoteClipboard = async () => {
|
||||
<style scoped>
|
||||
.fade-enter-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;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
<line x1="8" y1="21" x2="16" y2="21" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
<span>远程桌面</span>
|
||||
<span>{{ $t("desktop.remoteDesktop") }}</span>
|
||||
<span class="duration-text" v-if="desktopStream">{{
|
||||
callDurationStr
|
||||
}}</span>
|
||||
@ -40,7 +40,11 @@
|
||||
class="header-btn"
|
||||
:class="{ 'sound-enabled': isDesktopSoundEnabled }"
|
||||
@click="toggleDesktopSound"
|
||||
:title="isDesktopSoundEnabled ? '静音' : '开启声音'"
|
||||
:title="
|
||||
isDesktopSoundEnabled
|
||||
? $t('desktop.disableSound')
|
||||
: $t('desktop.enableSound')
|
||||
"
|
||||
>
|
||||
<svg
|
||||
v-if="isDesktopSoundEnabled"
|
||||
@ -68,13 +72,21 @@
|
||||
<line x1="23" y1="9" x2="17" y2="15" />
|
||||
<line x1="17" y1="9" x2="23" y2="15" />
|
||||
</svg>
|
||||
<span>{{ isDesktopSoundEnabled ? "出声" : "静音" }}</span>
|
||||
<span>{{
|
||||
isDesktopSoundEnabled
|
||||
? $t("desktop.soundOn")
|
||||
: $t("desktop.soundOff")
|
||||
}}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="desktopStream"
|
||||
class="header-btn"
|
||||
@click="toggleFullscreen"
|
||||
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||
:title="
|
||||
isFullscreen
|
||||
? $t('desktop.exitFullscreen')
|
||||
: $t('desktop.fullscreen')
|
||||
"
|
||||
>
|
||||
<svg
|
||||
v-if="!isFullscreen"
|
||||
@ -104,7 +116,11 @@
|
||||
<line x1="14" y1="10" x2="21" y2="3" />
|
||||
<line x1="3" y1="21" x2="10" y2="14" />
|
||||
</svg>
|
||||
<span>{{ isFullscreen ? "退出" : "全屏" }}</span>
|
||||
<span>{{
|
||||
isFullscreen
|
||||
? $t("desktop.exitFullscreen")
|
||||
: $t("desktop.fullscreen")
|
||||
}}</span>
|
||||
</button>
|
||||
<button class="header-btn danger" @click="endDesktop">
|
||||
<svg
|
||||
@ -118,7 +134,7 @@
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
<span>结束共享</span>
|
||||
<span>{{ $t("desktop.endSharing") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -144,14 +160,14 @@
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>等待桌面共享...</span>
|
||||
<span>{{ $t("desktop.waitingForDesktop") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="desktop-side-toggle"
|
||||
@click="toggleDesktopCollapse"
|
||||
title="收起桌面"
|
||||
aria-label="收起桌面"
|
||||
:title="$t('desktop.collapseDesktop')"
|
||||
:aria-label="$t('desktop.collapseDesktop')"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
@ -174,8 +190,8 @@
|
||||
class="desktop-collapsed-tab"
|
||||
:class="desktopCollapsedClasses"
|
||||
@click="toggleDesktopCollapse"
|
||||
title="展开桌面"
|
||||
aria-label="展开桌面"
|
||||
:title="$t('desktop.expandDesktop')"
|
||||
:aria-label="$t('desktop.expandDesktop')"
|
||||
>
|
||||
<span class="sharing-status-dot"></span>
|
||||
<svg
|
||||
@ -214,13 +230,13 @@
|
||||
<path d="M8 20h8" />
|
||||
<path d="M12 16v4" />
|
||||
</svg>
|
||||
<span>桌面正在被预览</span>
|
||||
<span>{{ $t("desktop.desktopBeingViewed") }}</span>
|
||||
</div>
|
||||
<div class="panel-header-actions">
|
||||
<button
|
||||
class="panel-icon-btn"
|
||||
@click="toggleDesktopShareCollapse"
|
||||
title="收起提示"
|
||||
:title="$t('desktop.collapseDesktop')"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
@ -233,7 +249,11 @@
|
||||
<polyline points="18 15 12 9 6 15" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="panel-icon-btn" @click="endDesktop" title="结束预览">
|
||||
<button
|
||||
class="panel-icon-btn"
|
||||
@click="endDesktop"
|
||||
:title="$t('desktop.endSharing')"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@ -250,7 +270,7 @@
|
||||
</div>
|
||||
<div class="sharing-panel-body">
|
||||
<div class="sharing-status-dot"></div>
|
||||
<span>对方正在查看你的桌面</span>
|
||||
<span>{{ $t("desktop.peerViewingDesktop") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@ -261,8 +281,8 @@
|
||||
class="desktop-sharing-tab"
|
||||
:class="desktopCollapsedClasses"
|
||||
@click="toggleDesktopShareCollapse"
|
||||
title="展开桌面预览提示"
|
||||
aria-label="展开桌面预览提示"
|
||||
:title="$t('desktop.expandDesktopHint')"
|
||||
:aria-label="$t('desktop.expandDesktopHint')"
|
||||
>
|
||||
<span class="sharing-status-dot"></span>
|
||||
<svg
|
||||
@ -307,13 +327,13 @@
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>远程摄像头</span>
|
||||
<span>{{ $t("desktop.remoteCamera") }}</span>
|
||||
</div>
|
||||
<div class="panel-header-actions">
|
||||
<button
|
||||
class="panel-icon-btn"
|
||||
@click="toggleCameraCollapse"
|
||||
title="收起摄像头"
|
||||
:title="$t('desktop.collapseCamera')"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
@ -329,7 +349,7 @@
|
||||
<button
|
||||
class="panel-icon-btn"
|
||||
@click="endCamera"
|
||||
title="结束摄像头"
|
||||
:title="$t('desktop.endCamera')"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
@ -353,7 +373,7 @@
|
||||
playsinline
|
||||
></video>
|
||||
<div v-else class="loading-state">
|
||||
<span>等待摄像头画面...</span>
|
||||
<span>{{ $t("desktop.waitingForCamera") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -365,8 +385,8 @@
|
||||
class="camera-collapsed-tab"
|
||||
:class="cameraCollapsedClasses"
|
||||
@click="toggleCameraCollapse"
|
||||
title="展开摄像头"
|
||||
aria-label="展开摄像头"
|
||||
:title="$t('desktop.expandCamera')"
|
||||
:aria-label="$t('desktop.expandCamera')"
|
||||
>
|
||||
<span class="sharing-status-dot"></span>
|
||||
<svg
|
||||
@ -402,13 +422,13 @@
|
||||
<path d="M23 7l-7 5 7 5V7z" />
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
||||
</svg>
|
||||
<span>摄像头正在被预览</span>
|
||||
<span>{{ $t("desktop.cameraBeingViewed") }}</span>
|
||||
</div>
|
||||
<div class="panel-header-actions">
|
||||
<button
|
||||
class="panel-icon-btn"
|
||||
@click="toggleCameraShareCollapse"
|
||||
title="收起提示"
|
||||
:title="$t('desktop.collapseCamera')"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
@ -424,7 +444,7 @@
|
||||
<button
|
||||
class="panel-icon-btn"
|
||||
@click="endCamera"
|
||||
title="结束摄像头"
|
||||
:title="$t('desktop.endCamera')"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
@ -442,7 +462,7 @@
|
||||
</div>
|
||||
<div class="sharing-panel-body">
|
||||
<div class="sharing-status-dot"></div>
|
||||
<span>对方正在查看你的摄像头</span>
|
||||
<span>{{ $t("desktop.peerViewingCamera") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@ -453,8 +473,8 @@
|
||||
class="camera-sharing-tab"
|
||||
:class="cameraCollapsedClasses"
|
||||
@click="toggleCameraShareCollapse"
|
||||
title="展开摄像头预览提示"
|
||||
aria-label="展开摄像头预览提示"
|
||||
:title="$t('desktop.expandCameraHint')"
|
||||
:aria-label="$t('desktop.expandCameraHint')"
|
||||
>
|
||||
<span class="sharing-status-dot"></span>
|
||||
<svg
|
||||
@ -515,8 +535,8 @@
|
||||
</span>
|
||||
<span>{{
|
||||
incomingCallRequest.type === "camera"
|
||||
? "摄像头请求"
|
||||
: "语音通话请求"
|
||||
? $t("desktop.cameraRequest")
|
||||
: $t("desktop.voiceCallRequest")
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -554,8 +574,8 @@
|
||||
<div class="call-status-text">
|
||||
{{
|
||||
incomingCallRequest.type === "camera"
|
||||
? "对方请求查看摄像头"
|
||||
: "对方请求语音通话"
|
||||
? $t("desktop.peerRequestCamera")
|
||||
: $t("desktop.peerRequestCall")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@ -565,14 +585,14 @@
|
||||
:disabled="isIncomingCallHandling"
|
||||
@click="rejectIncomingCall"
|
||||
>
|
||||
拒绝
|
||||
{{ $t("desktop.reject") }}
|
||||
</button>
|
||||
<button
|
||||
class="incoming-action-btn accept"
|
||||
:disabled="isIncomingCallHandling"
|
||||
@click="acceptIncomingCall"
|
||||
>
|
||||
接听
|
||||
{{ $t("desktop.accept") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -601,13 +621,13 @@
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>语音通话</span>
|
||||
<span>{{ $t("desktop.voiceCall") }}</span>
|
||||
</div>
|
||||
<div class="panel-header-actions">
|
||||
<button
|
||||
class="panel-icon-btn"
|
||||
@click="toggleCallCollapse"
|
||||
title="收起通话"
|
||||
:title="$t('desktop.collapseCall')"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
@ -620,7 +640,11 @@
|
||||
<polyline points="18 15 12 9 6 15" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="panel-icon-btn" @click="endCall" title="结束通话">
|
||||
<button
|
||||
class="panel-icon-btn"
|
||||
@click="endCall"
|
||||
:title="$t('desktop.endCall')"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@ -651,7 +675,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="call-status-text">
|
||||
{{ callStream ? "通话中" : "正在建立连接..." }}
|
||||
{{ callStream ? $t("desktop.inCall") : $t("desktop.connecting") }}
|
||||
</div>
|
||||
<div class="call-duration">{{ callDurationStr }}</div>
|
||||
</div>
|
||||
@ -660,7 +684,7 @@
|
||||
class="control-btn mute-btn"
|
||||
:class="{ 'is-muted': isCallMuted }"
|
||||
@click="toggleCallMuted"
|
||||
:title="isCallMuted ? '取消静音' : '静音'"
|
||||
:title="isCallMuted ? $t('desktop.unmute') : $t('desktop.mute')"
|
||||
>
|
||||
<svg
|
||||
v-if="!isCallMuted"
|
||||
@ -696,7 +720,7 @@
|
||||
<button
|
||||
class="control-btn end-call-btn"
|
||||
@click="endCall"
|
||||
title="结束通话"
|
||||
:title="$t('desktop.endCall')"
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
@ -719,7 +743,7 @@
|
||||
(isCameraLocalSharing && !isCameraShareCollapsed),
|
||||
}"
|
||||
@click="toggleCallCollapse"
|
||||
title="展开通话"
|
||||
:title="$t('desktop.expandCall')"
|
||||
>
|
||||
<div class="call-collapsed-pulse"></div>
|
||||
<svg
|
||||
@ -741,9 +765,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, onMounted, onUnmounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { peer } from "../utils/peer";
|
||||
import { message } from "ant-design-vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
peerId: string;
|
||||
}>();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Modal
|
||||
:open="visible"
|
||||
title="权限设置"
|
||||
:title="$t('permission.title')"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
:maskClosable="false"
|
||||
@ -13,9 +13,11 @@
|
||||
:key="permission"
|
||||
>
|
||||
<Checkbox v-model:checked="localPermissionSet[permission]">
|
||||
{{ permissionLabels[permission] }}
|
||||
{{ $t(`permission.${permission}`) }}
|
||||
</Checkbox>
|
||||
<div class="permission-desc">{{ permissionDescs[permission] }}</div>
|
||||
<div class="permission-desc">
|
||||
{{ $t(`permission.${permission}Desc`) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 () => {
|
||||
try {
|
||||
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 {
|
||||
hasCamera.value = false;
|
||||
}
|
||||
|
||||
@ -8,12 +8,16 @@
|
||||
:disabled="!loadedFile.name"
|
||||
@click="downloadFile"
|
||||
>
|
||||
{{ fileTypeCheck == "unsupported-size" ? "远程传输" : "浏览器下载" }}
|
||||
{{
|
||||
fileTypeCheck == "unsupported-size"
|
||||
? $t("fileReader.remoteTransfer")
|
||||
: $t("fileReader.browserDownload")
|
||||
}}
|
||||
</button>
|
||||
<button v-if="canEdit" @click="toggleEdit">
|
||||
{{ isEditing ? "保存" : "编辑" }}
|
||||
{{ isEditing ? $t("fileReader.save") : $t("fileReader.edit") }}
|
||||
</button>
|
||||
<button @click="$emit('close')">关闭</button>
|
||||
<button @click="$emit('close')">{{ $t("fileReader.close") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -59,7 +63,7 @@
|
||||
<div v-else-if="fileTypeCheck === 'video'" class="video-viewer">
|
||||
<video controls>
|
||||
<source :src="fileUrl" :type="mimeType" />
|
||||
您的浏览器不支持视频播放
|
||||
{{ $t("fileReader.videoNotSupported") }}
|
||||
</video>
|
||||
</div>
|
||||
|
||||
@ -67,7 +71,7 @@
|
||||
<div v-else-if="fileTypeCheck === 'audio'" class="audio-viewer">
|
||||
<audio controls>
|
||||
<source :src="fileUrl" :type="mimeType" />
|
||||
您的浏览器不支持音频播放
|
||||
{{ $t("fileReader.audioNotSupported") }}
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
@ -75,7 +79,7 @@
|
||||
<div v-else-if="fileTypeCheck === 'code'" class="code-viewer">
|
||||
<div class="code-header">
|
||||
<select v-model="selectedLanguage">
|
||||
<option value="auto">自动检测</option>
|
||||
<option value="auto">{{ $t("fileReader.autoDetect") }}</option>
|
||||
<option value="javascript">JavaScript</option>
|
||||
<option value="typescript">TypeScript</option>
|
||||
<option value="html">HTML</option>
|
||||
@ -89,7 +93,7 @@
|
||||
<option value="sql">SQL</option>
|
||||
<option value="shell">Shell</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="other">其他</option>
|
||||
<option value="other">{{ $t("fileReader.other") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="isEditing" class="editor">
|
||||
@ -107,15 +111,15 @@
|
||||
<div>
|
||||
{{
|
||||
fileTypeCheck === "unsupported-size"
|
||||
? "文件过大,无法预览,请传输后查看"
|
||||
: "不支持预览该类型的文件"
|
||||
? $t("fileReader.fileTooLarge")
|
||||
: $t("fileReader.unsupportedType")
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
v-if="fileTypeCheck != 'unsupported-size'"
|
||||
@click="openWithText = true"
|
||||
>以文本打开</Button
|
||||
>{{ $t("fileReader.openAsText") }}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@ -125,6 +129,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
fileMgrInstance,
|
||||
type FileData,
|
||||
@ -136,6 +141,8 @@ import { Modal } from "ant-design-vue";
|
||||
import { localCurrentFile, remoteCurrentFile } from "../utils/common";
|
||||
import { NEED_CHUNK_FILE_SIZE_PREVIEW } from "../utils/fileTransfer";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
file: FileInfo;
|
||||
}>();
|
||||
@ -167,19 +174,17 @@ const downloadFile = async () => {
|
||||
await props.file.getFile(
|
||||
false,
|
||||
localCurrentFile.value.path.replace(
|
||||
(
|
||||
await fileMgrInstance.getRootFile()
|
||||
).path,
|
||||
""
|
||||
(await fileMgrInstance.getRootFile()).path,
|
||||
"",
|
||||
),
|
||||
remoteCurrentFile.value.path
|
||||
remoteCurrentFile.value.path,
|
||||
);
|
||||
localCurrentFile.value.loadLocalDirectory();
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: "下载文件",
|
||||
content: `确定要下载${props.file.name}吗?`,
|
||||
title: t("fileReader.downloadTitle"),
|
||||
content: t("fileReader.downloadConfirm", { name: props.file.name }),
|
||||
onOk: () => {
|
||||
const blob = new Blob([loadedFile.value.buffer], {
|
||||
type: loadedFile.value.type || "application/octet-stream",
|
||||
@ -313,7 +318,7 @@ const toggleEdit = async () => {
|
||||
isEditing.value = false;
|
||||
} catch (err) {
|
||||
console.error("保存文件失败:", err);
|
||||
alert("保存文件失败: " + (err as Error).message);
|
||||
alert(t("fileReader.saveFailed") + ": " + (err as Error).message);
|
||||
}
|
||||
} else {
|
||||
// 进入编辑模式
|
||||
@ -326,7 +331,7 @@ const toggleEdit = async () => {
|
||||
// 自动调整编辑器高度
|
||||
const adjustEditorHeight = () => {
|
||||
const textarea = document.querySelector(
|
||||
".editor textarea"
|
||||
".editor textarea",
|
||||
) as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto";
|
||||
@ -388,7 +393,7 @@ const handleTouchStart = (e: TouchEvent) => {
|
||||
const touch2 = e.touches[1];
|
||||
lastTouchDistance.value = Math.hypot(
|
||||
touch2.clientX - touch1.clientX,
|
||||
touch2.clientY - touch1.clientY
|
||||
touch2.clientY - touch1.clientY,
|
||||
);
|
||||
} else if (e.touches.length === 1) {
|
||||
isDragging.value = true;
|
||||
@ -404,7 +409,7 @@ const handleTouchMove = (e: TouchEvent) => {
|
||||
const touch2 = e.touches[1];
|
||||
const distance = Math.hypot(
|
||||
touch2.clientX - touch1.clientX,
|
||||
touch2.clientY - touch1.clientY
|
||||
touch2.clientY - touch1.clientY,
|
||||
);
|
||||
|
||||
const delta = distance - lastTouchDistance.value;
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div class="transfer-container">
|
||||
<div class="transfer-header">
|
||||
传输列表 ({{ tasks.length }})
|
||||
{{ $t("transfer.title") }} ({{ tasks.length }})
|
||||
<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
|
||||
type="ghost"
|
||||
class="toggle-btn"
|
||||
@ -49,8 +51,11 @@ import {
|
||||
TransferStatus,
|
||||
} from "../utils/fileTransfer";
|
||||
import { type Ref, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { formatSize } from "../utils/common";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isExpanded = ref(true);
|
||||
const tasks: Ref<TransferTask[]> = ref([]);
|
||||
|
||||
@ -63,12 +68,12 @@ const clearTasks = () => {
|
||||
|
||||
const getStatusText = (status: TransferStatus) => {
|
||||
const statusMap: Record<TransferStatus, string> = {
|
||||
[TransferStatus.WAITING]: "等待中",
|
||||
[TransferStatus.SENDING]: "发送中",
|
||||
[TransferStatus.RECEIVING]: "接收中",
|
||||
[TransferStatus.COMPLETED]: "已完成",
|
||||
[TransferStatus.ERROR]: "失败",
|
||||
[TransferStatus.PAUSED]: "已暂停",
|
||||
[TransferStatus.WAITING]: t("transfer.waiting"),
|
||||
[TransferStatus.SENDING]: t("transfer.sending"),
|
||||
[TransferStatus.RECEIVING]: t("transfer.receiving"),
|
||||
[TransferStatus.COMPLETED]: t("transfer.completed"),
|
||||
[TransferStatus.ERROR]: t("transfer.error"),
|
||||
[TransferStatus.PAUSED]: t("transfer.paused"),
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
@ -3,17 +3,25 @@
|
||||
<div class="panel-header">
|
||||
<div class="path-nav">
|
||||
<Button @click="navigateLocal('..')" :disabled="!canNavigateUpLocal">
|
||||
上级目录
|
||||
{{ $t("fileView.parentDir") }}
|
||||
</Button>
|
||||
<Select class="sort-select" :value="sortType" @change="refreshLocal">
|
||||
<SelectOption value="name">按名称</SelectOption>
|
||||
<SelectOption value="size">按大小</SelectOption>
|
||||
<SelectOption value="type">按类型</SelectOption>
|
||||
<SelectOption value="date">按日期</SelectOption>
|
||||
<SelectOption value="name">{{
|
||||
$t("fileView.sortByName")
|
||||
}}</SelectOption>
|
||||
<SelectOption value="size">{{
|
||||
$t("fileView.sortBySize")
|
||||
}}</SelectOption>
|
||||
<SelectOption value="type">{{
|
||||
$t("fileView.sortByType")
|
||||
}}</SelectOption>
|
||||
<SelectOption value="date">{{
|
||||
$t("fileView.sortByDate")
|
||||
}}</SelectOption>
|
||||
</Select>
|
||||
<Button danger v-if="!isRemote" @click="showPermissionSetting"
|
||||
>权限设置</Button
|
||||
>
|
||||
<Button danger v-if="!isRemote" @click="showPermissionSetting">{{
|
||||
$t("fileView.permissionSettings")
|
||||
}}</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Breadcrumb>
|
||||
@ -28,13 +36,13 @@
|
||||
style="text-align: center"
|
||||
v-if="isRemote && !currentDirInfo?.isInit"
|
||||
>
|
||||
<Button @click="refreshLocal">初始化</Button>
|
||||
<Button @click="refreshLocal">{{ $t("fileView.initialize") }}</Button>
|
||||
</div>
|
||||
<div v-else-if="!currentDirInfo?.isInit">
|
||||
<div @click="selectLocalDirectory" class="file-item">
|
||||
<div class="file-icon">📁</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">请选择一个目录</div>
|
||||
<div class="file-name">{{ $t("fileView.selectDirHint") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -49,37 +57,51 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="currentDirInfo?.isInit" class="panel-footer">
|
||||
<Button v-if="!isRemote" @click="selectLocalDirectory">选择目录</Button>
|
||||
<Button @click="refreshLocal">刷新</Button>
|
||||
<Button @click="createNewFolder">新建文件夹</Button>
|
||||
<Button @click="createNewFile">新建文件</Button>
|
||||
<Button v-if="!isRemote" @click="selectLocalDirectory">{{
|
||||
$t("fileView.selectDirectory")
|
||||
}}</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">
|
||||
删除
|
||||
{{ $t("fileView.delete") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<div v-if="showMenu" class="context-menu" :style="menuPosition">
|
||||
<div v-if="activeFile">
|
||||
<div class="menu-item" @click="handleRename">重命名</div>
|
||||
<div class="menu-item" @click="handleDelete">删除</div>
|
||||
<div class="menu-item" @click="handleRename">
|
||||
{{ $t("fileView.rename") }}
|
||||
</div>
|
||||
<div class="menu-item" @click="handleDelete">
|
||||
{{ $t("fileView.delete") }}
|
||||
</div>
|
||||
<div
|
||||
class="menu-item"
|
||||
v-if="props.isRemote && remoteCurrentFile?.isInit"
|
||||
@click="handleDownload"
|
||||
>
|
||||
下载
|
||||
{{ $t("fileView.download") }}
|
||||
</div>
|
||||
</div>
|
||||
<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 v-else>
|
||||
<div class="menu-item" @click="refreshLocal">刷新</div>
|
||||
<div class="menu-item" @click="createNewFolder">新建文件夹</div>
|
||||
<div class="menu-item" @click="createNewFile">新建文件</div>
|
||||
<div class="menu-item" @click="refreshLocal">
|
||||
{{ $t("fileView.refresh") }}
|
||||
</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">
|
||||
删除
|
||||
{{ $t("fileView.delete") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -106,6 +128,7 @@ import {
|
||||
h,
|
||||
reactive,
|
||||
} from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button, Select, SelectOption, Modal } from "ant-design-vue";
|
||||
import { FileInfo, fileMgrInstance } from "../utils/fileMgr";
|
||||
import FileItem from "./fileItem.vue";
|
||||
@ -114,6 +137,8 @@ import { Breadcrumb, BreadcrumbItem } from "ant-design-vue";
|
||||
import emitter, { EmitterEvent } from "../utils/emitter";
|
||||
import { localCurrentFile, remoteCurrentFile } from "../utils/common";
|
||||
import FilePermissionSetDialog from "./filePermissionSetDialog.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
isRemote: boolean;
|
||||
selectedFiles: string[];
|
||||
@ -173,12 +198,10 @@ const handleDownload = async () => {
|
||||
await file.getFile(
|
||||
false,
|
||||
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;
|
||||
const acf = activeFile.value;
|
||||
Modal.confirm({
|
||||
title: "重命名",
|
||||
title: t("fileView.renameTitle"),
|
||||
class: "custom-modal",
|
||||
content: h("div", [
|
||||
h("input", {
|
||||
class: "ant-input",
|
||||
placeholder: "请输入新的名称",
|
||||
placeholder: t("fileView.inputNewName"),
|
||||
value: newFileName.value,
|
||||
onInput: (e: Event) => {
|
||||
newFileName.value = (e.target as HTMLInputElement).value;
|
||||
@ -290,8 +313,8 @@ const handleRename = async () => {
|
||||
},
|
||||
}),
|
||||
]),
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
okText: t("common.okText"),
|
||||
cancelText: t("common.cancelText"),
|
||||
async onOk() {
|
||||
if (!newFileName.value || newFileName.value === acf.name) {
|
||||
return Promise.reject();
|
||||
@ -302,7 +325,7 @@ const handleRename = async () => {
|
||||
} catch (err) {
|
||||
console.error("重命名失败:", err);
|
||||
Modal.error({
|
||||
title: "重命名失败",
|
||||
title: t("fileView.renameTitle") + t("common.failed"),
|
||||
content: (err as Error).message || (err as string),
|
||||
});
|
||||
}
|
||||
@ -327,13 +350,14 @@ const deleteSelected = async () => {
|
||||
if (!selectedLocalFiles.value.length) return;
|
||||
|
||||
Modal.confirm({
|
||||
title: "确认删除",
|
||||
content: `确定要删除以下文件吗?<br>${selectedLocalFiles.value.join(
|
||||
"<br>"
|
||||
)}<br>共 ${selectedLocalFiles.value.length} 个文件`,
|
||||
okText: "删除",
|
||||
title: t("fileView.confirmDelete"),
|
||||
content: t("fileView.deleteConfirmContent", {
|
||||
files: selectedLocalFiles.value.join("<br>"),
|
||||
count: selectedLocalFiles.value.length,
|
||||
}),
|
||||
okText: t("fileView.deleteBtn"),
|
||||
okType: "danger",
|
||||
cancelText: "取消",
|
||||
cancelText: t("fileView.cancelBtn"),
|
||||
async onOk() {
|
||||
try {
|
||||
for (const fileName of selectedLocalFiles.value) {
|
||||
@ -347,7 +371,7 @@ const deleteSelected = async () => {
|
||||
} catch (err) {
|
||||
console.error("批量删除失败:", err);
|
||||
Modal.error({
|
||||
title: "删除失败",
|
||||
title: t("common.delete") + t("common.failed"),
|
||||
content: (err as Error).message,
|
||||
});
|
||||
}
|
||||
@ -361,12 +385,12 @@ const createNewFolder = async () => {
|
||||
|
||||
folderName.value = "";
|
||||
Modal.confirm({
|
||||
title: "新建文件夹",
|
||||
title: t("fileView.createFolderTitle"),
|
||||
class: "custom-modal",
|
||||
content: h("div", [
|
||||
h("input", {
|
||||
class: "ant-input",
|
||||
placeholder: "请输入文件夹名称",
|
||||
placeholder: t("fileView.inputFolderName"),
|
||||
value: folderName.value,
|
||||
onInput: (e: Event) => {
|
||||
folderName.value = (e.target as HTMLInputElement).value;
|
||||
@ -377,13 +401,13 @@ const createNewFolder = async () => {
|
||||
},
|
||||
}),
|
||||
]),
|
||||
okText: "创建",
|
||||
cancelText: "取消",
|
||||
okText: t("fileView.createBtn"),
|
||||
cancelText: t("fileView.cancelBtn"),
|
||||
async onOk() {
|
||||
if (!folderName.value) {
|
||||
Modal.warning({
|
||||
title: "提示",
|
||||
content: "请输入文件夹名称",
|
||||
title: t("common.warning"),
|
||||
content: t("fileView.warningInputFolderName"),
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
@ -393,7 +417,7 @@ const createNewFolder = async () => {
|
||||
} catch (err) {
|
||||
console.error("创建文件夹失败:", err);
|
||||
Modal.error({
|
||||
title: "创建失败",
|
||||
title: t("fileView.createFolderTitle") + t("common.failed"),
|
||||
content: (err as Error).message,
|
||||
});
|
||||
}
|
||||
@ -407,12 +431,12 @@ const createNewFile = async () => {
|
||||
|
||||
fileName.value = "";
|
||||
Modal.confirm({
|
||||
title: "新建文件",
|
||||
title: t("fileView.createFileTitle"),
|
||||
class: "custom-modal",
|
||||
content: h("div", [
|
||||
h("input", {
|
||||
class: "ant-input",
|
||||
placeholder: "请输入文件名称",
|
||||
placeholder: t("fileView.inputFileName"),
|
||||
value: fileName.value,
|
||||
onInput: (e: Event) => {
|
||||
fileName.value = (e.target as HTMLInputElement).value;
|
||||
@ -423,13 +447,13 @@ const createNewFile = async () => {
|
||||
},
|
||||
}),
|
||||
]),
|
||||
okText: "创建",
|
||||
cancelText: "取消",
|
||||
okText: t("fileView.createBtn"),
|
||||
cancelText: t("fileView.cancelBtn"),
|
||||
async onOk() {
|
||||
if (!fileName.value) {
|
||||
Modal.warning({
|
||||
title: "提示",
|
||||
content: "请输入文件名称",
|
||||
title: t("common.warning"),
|
||||
content: t("fileView.warningInputFileName"),
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
@ -440,7 +464,7 @@ const createNewFile = async () => {
|
||||
} catch (err) {
|
||||
console.error("创建文件失败:", err);
|
||||
Modal.error({
|
||||
title: "创建失败",
|
||||
title: t("fileView.createFileTitle") + t("common.failed"),
|
||||
content: (err as Error).message,
|
||||
});
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
style="display: none"
|
||||
/>
|
||||
<button class="upload-btn" @click="triggerFileUpload">
|
||||
上传图片/视频
|
||||
{{ $t('shader.uploadImageVideo') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,185 +3,197 @@
|
||||
<div class="call-status">
|
||||
<h2>{{ callStatus }}</h2>
|
||||
<div v-if="myPeerId" class="peer-id">
|
||||
我的ID: {{ myPeerId }}
|
||||
{{ $t("voice.myId") }}: {{ myPeerId }}
|
||||
</div>
|
||||
<button @click="copyPeerId">复制</button>
|
||||
<button @click="copyPeerId">{{ $t("voice.copy") }}</button>
|
||||
</div>
|
||||
<div class="delay-time">
|
||||
<h2>延迟时间: {{ delayTime + 'ms' }}</h2>
|
||||
<h2>{{ $t("voice.delay") }}: {{ delayTime + "ms" }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="connection-controls" v-if="!isInCall">
|
||||
<input type="text" v-model="remotePeerId" placeholder="输入对方的ID" :disabled="isInCall" />
|
||||
<button type="reset" class="call-btn start-call" @click="startCall" :disabled="!remotePeerId || isInCall">
|
||||
呼叫
|
||||
<input
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div class="call-controls" v-if="isInCall">
|
||||
<button class="call-btn end-call" @click="endCall">
|
||||
结束通话
|
||||
{{ $t("voice.endCall") }}
|
||||
</button>
|
||||
|
||||
<button class="mute-btn" :class="{ muted: isMuted }" @click="toggleMute">
|
||||
{{ isMuted ? '取消静音' : '静音' }}
|
||||
{{ isMuted ? $t("voice.unmute") : $t("voice.mute") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import Peer, { type MediaConnection } from 'peerjs'
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import Peer, { type MediaConnection } from "peerjs";
|
||||
|
||||
const myPeerId = ref('')
|
||||
const remotePeerId = ref('')
|
||||
const isInCall = ref(false)
|
||||
const isMuted = ref(false)
|
||||
const callStatus = ref('准备连接...')
|
||||
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
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const myPeerId = ref("");
|
||||
const remotePeerId = ref("");
|
||||
const isInCall = ref(false);
|
||||
const isMuted = ref(false);
|
||||
const callStatus = ref(t("voice.preparing"));
|
||||
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 = () => {
|
||||
peer.value = new Peer(
|
||||
{
|
||||
config: {
|
||||
iceServers: [
|
||||
{ urls: 'stun:8.134.35.244:3478' }
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
peer.value = new Peer({
|
||||
config: {
|
||||
iceServers: [{ urls: "stun:8.134.35.244:3478" }],
|
||||
},
|
||||
});
|
||||
|
||||
peer.value.on('open', (id) => {
|
||||
myPeerId.value = id
|
||||
callStatus.value = '准备就绪'
|
||||
})
|
||||
peer.value.on("open", (id) => {
|
||||
myPeerId.value = id;
|
||||
callStatus.value = t("voice.ready");
|
||||
});
|
||||
|
||||
peer.value.on('call', async (call) => {
|
||||
peer.value.on("call", async (call) => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: false
|
||||
})
|
||||
video: false,
|
||||
});
|
||||
|
||||
localStream.value = stream
|
||||
call.answer(stream)
|
||||
handleCall(call)
|
||||
localStream.value = stream;
|
||||
call.answer(stream);
|
||||
handleCall(call);
|
||||
|
||||
isInCall.value = true
|
||||
callStatus.value = '通话中'
|
||||
isInCall.value = true;
|
||||
callStatus.value = t("voice.inCall");
|
||||
} catch (error) {
|
||||
console.error('获取音频设备失败:', error)
|
||||
callStatus.value = '无法访问麦克风'
|
||||
console.error("获取音频设备失败:", error);
|
||||
callStatus.value = t("voice.cannotAccessMic");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
peer.value.on('error', (error) => {
|
||||
console.error('PeerJS错误:', error)
|
||||
callStatus.value = '连接错误'
|
||||
})
|
||||
}
|
||||
peer.value.on("error", (error) => {
|
||||
console.error("PeerJS错误:", error);
|
||||
callStatus.value = t("voice.connectionError");
|
||||
});
|
||||
};
|
||||
const copyPeerId = () => {
|
||||
navigator.clipboard.writeText(myPeerId.value)
|
||||
}
|
||||
navigator.clipboard.writeText(myPeerId.value);
|
||||
};
|
||||
const updateDelayTime = async () => {
|
||||
delayTime.value = 0
|
||||
delayTime.value = 0;
|
||||
if (currentCall.value && currentCall.value.peerConnection) {
|
||||
const stats = await currentCall.value.peerConnection.getStats()
|
||||
stats.forEach(report => {
|
||||
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
|
||||
delayTime.value += report.currentRoundTripTime * 1000 //currentRoundTripTime是指往返时间
|
||||
const stats = await currentCall.value.peerConnection.getStats();
|
||||
stats.forEach((report) => {
|
||||
if (report.type === "candidate-pair" && report.state === "succeeded") {
|
||||
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(
|
||||
report.jitter * 1000 || 0 + // 抖动缓冲区变化
|
||||
report.playoutDelay || 0 // 播放延迟
|
||||
)
|
||||
report.jitter * 1000 ||
|
||||
0 + // 抖动缓冲区变化
|
||||
report.playoutDelay ||
|
||||
0, // 播放延迟
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleCall = (call: MediaConnection) => {
|
||||
currentCall.value = call
|
||||
currentCall.value = call;
|
||||
|
||||
statsInterval = window.setInterval(updateDelayTime, 1000)
|
||||
statsInterval = window.setInterval(updateDelayTime, 1000);
|
||||
|
||||
call.on('stream', (remoteStream) => {
|
||||
const audio = new Audio()
|
||||
audio.srcObject = remoteStream
|
||||
audio.play()
|
||||
})
|
||||
call.on("stream", (remoteStream) => {
|
||||
const audio = new Audio();
|
||||
audio.srcObject = remoteStream;
|
||||
audio.play();
|
||||
});
|
||||
|
||||
call.on('close', () => {
|
||||
endCall()
|
||||
})
|
||||
}
|
||||
call.on("close", () => {
|
||||
endCall();
|
||||
});
|
||||
};
|
||||
|
||||
const startCall = async () => {
|
||||
if (!peer.value || !remotePeerId.value) return
|
||||
if (!peer.value || !remotePeerId.value) return;
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: false
|
||||
})
|
||||
video: false,
|
||||
});
|
||||
|
||||
localStream.value = stream
|
||||
const call = peer.value.call(remotePeerId.value, stream)
|
||||
handleCall(call)
|
||||
localStream.value = stream;
|
||||
const call = peer.value.call(remotePeerId.value, stream);
|
||||
handleCall(call);
|
||||
|
||||
isInCall.value = true
|
||||
callStatus.value = '通话中'
|
||||
isInCall.value = true;
|
||||
callStatus.value = t("voice.inCall");
|
||||
} catch (error) {
|
||||
console.error('获取音频设备失败:', error)
|
||||
callStatus.value = '无法访问麦克风'
|
||||
console.error("获取音频设备失败:", error);
|
||||
callStatus.value = t("voice.cannotAccessMic");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
if (localStream.value) {
|
||||
const audioTrack = localStream.value.getAudioTracks()[0]
|
||||
isMuted.value = !isMuted.value
|
||||
audioTrack.enabled = !isMuted.value
|
||||
const audioTrack = localStream.value.getAudioTracks()[0];
|
||||
isMuted.value = !isMuted.value;
|
||||
audioTrack.enabled = !isMuted.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const endCall = () => {
|
||||
if (statsInterval) {
|
||||
clearInterval(statsInterval)
|
||||
statsInterval = null
|
||||
clearInterval(statsInterval);
|
||||
statsInterval = null;
|
||||
}
|
||||
|
||||
if (currentCall.value) {
|
||||
currentCall.value.close()
|
||||
currentCall.value = null
|
||||
currentCall.value.close();
|
||||
currentCall.value = null;
|
||||
}
|
||||
|
||||
if (localStream.value) {
|
||||
localStream.value.getTracks().forEach(track => track.stop())
|
||||
localStream.value = null
|
||||
localStream.value.getTracks().forEach((track) => track.stop());
|
||||
localStream.value = null;
|
||||
}
|
||||
|
||||
isInCall.value = false
|
||||
isMuted.value = false
|
||||
callStatus.value = '准备就绪'
|
||||
delayTime.value = 0 // 重置延迟时间
|
||||
}
|
||||
isInCall.value = false;
|
||||
isMuted.value = false;
|
||||
callStatus.value = t("voice.ready");
|
||||
delayTime.value = 0;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initPeer()
|
||||
})
|
||||
initPeer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
endCall()
|
||||
endCall();
|
||||
if (peer.value) {
|
||||
peer.value.destroy()
|
||||
peer.value.destroy();
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -230,7 +242,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.connection-controls input:focus {
|
||||
border-color: #2196F3;
|
||||
border-color: #2196f3;
|
||||
}
|
||||
|
||||
.call-controls {
|
||||
@ -259,7 +271,7 @@ button:disabled {
|
||||
}
|
||||
|
||||
.start-call {
|
||||
background-color: #4CAF50;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@ -269,12 +281,12 @@ button:disabled {
|
||||
}
|
||||
|
||||
.mute-btn {
|
||||
background-color: #2196F3;
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mute-btn.muted {
|
||||
background-color: #9E9E9E;
|
||||
background-color: #9e9e9e;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user