├── .github └── logo.png ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public ├── bg.png ├── empty.svg ├── file-download.png ├── file-upload.png ├── icon │ ├── chat-empty.png │ ├── chat.png │ ├── notify-empty.png │ ├── notify.png │ ├── set-empty.png │ ├── set.png │ ├── talk-empty.png │ ├── talk.png │ ├── user-empty.png │ └── user.png ├── id.png ├── linyu.png ├── logo.png ├── tauri.svg └── updater.png ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── audio │ ├── remind-short.wav │ └── remind.wav ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── main.rs │ ├── tray.rs │ └── user_cmd.rs └── tauri.conf.json ├── src ├── App.css ├── App.jsx ├── api │ ├── chatGroup.js │ ├── chatGroupMember.js │ ├── chatGroupNotice.js │ ├── chatList.js │ ├── friend.js │ ├── group.js │ ├── login.js │ ├── message.js │ ├── notify.js │ ├── qr.js │ ├── talk.js │ ├── talkComment.js │ ├── talkLike.js │ ├── user.js │ ├── userSet.js │ └── video.js ├── assets │ ├── iconfont.css │ ├── iconfont.ttf │ ├── iconfont.woff │ └── iconfont.woff2 ├── componets │ ├── ChatGroupInvite │ │ ├── index.jsx │ │ └── index.less │ ├── CircularProgressBar │ │ ├── index.jsx │ │ └── index.less │ ├── CommonChatFrame │ │ ├── ChatContent │ │ │ ├── Call │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── File │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── Img │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── MsgContent │ │ │ │ └── index.jsx │ │ │ ├── Retraction │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── SystemMsg │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── Text │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── Time │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ └── Voice │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ ├── index.jsx │ │ └── index.less │ ├── CustomAccordion │ │ ├── index.jsx │ │ └── index.less │ ├── CustomAffirmModal │ │ ├── index.jsx │ │ └── index.less │ ├── CustomAudio │ │ ├── index.jsx │ │ └── index.less │ ├── CustomBox │ │ ├── index.jsx │ │ └── index.less │ ├── CustomButton │ │ ├── index.css │ │ └── index.jsx │ ├── CustomDragDiv │ │ └── index.jsx │ ├── CustomDrawer │ │ ├── index.jsx │ │ └── index.less │ ├── CustomDropdown │ │ ├── index.jsx │ │ └── index.less │ ├── CustomEditableText │ │ ├── index.jsx │ │ └── index.less │ ├── CustomEmpty │ │ └── index.jsx │ ├── CustomImg │ │ ├── index.jsx │ │ └── index.less │ ├── CustomInput │ │ ├── index.jsx │ │ └── index.less │ ├── CustomLine │ │ ├── index.jsx │ │ └── index.less │ ├── CustomModal │ │ ├── index.jsx │ │ └── index.less │ ├── CustomOverlay │ │ ├── index.jsx │ │ └── index.less │ ├── CustomPwdInput │ │ ├── index.jsx │ │ └── index.less │ ├── CustomSearchInput │ │ ├── index.jsx │ │ └── index.less │ ├── CustomSelectionIcon │ │ └── index.jsx │ ├── CustomShortcutInput │ │ ├── index.jsx │ │ └── index.less │ ├── CustomSoundIcon │ │ ├── index.jsx │ │ └── index.less │ ├── CustomSwitch │ │ ├── index.jsx │ │ └── index.less │ ├── CustomTextarea │ │ ├── index.jsx │ │ └── index.less │ ├── CustomToast │ │ ├── index.jsx │ │ └── index.less │ ├── CustomTooltip │ │ ├── index.jsx │ │ └── index.less │ ├── CustomUserNameInput │ │ ├── index.jsx │ │ └── index.less │ ├── DropdownButton │ │ ├── index.jsx │ │ └── index.less │ ├── FriendSearchCard │ │ ├── index.jsx │ │ └── index.less │ ├── IconButton │ │ ├── index.css │ │ └── index.jsx │ ├── IconMinorButton │ │ ├── index.jsx │ │ └── index.less │ ├── MsgContentShow │ │ └── index.jsx │ ├── ProgressBar │ │ ├── index.jsx │ │ └── index.less │ ├── QRCodeGenerator │ │ └── index.jsx │ ├── QuillRichTextEditor │ │ ├── index.css │ │ └── index.jsx │ ├── RichTextEditor │ │ ├── index.jsx │ │ └── index.less │ ├── RightClickContent │ │ ├── index.jsx │ │ └── index.less │ ├── RightClickMenu │ │ ├── index.jsx │ │ └── index.less │ ├── VoiceRecorder │ │ ├── index.jsx │ │ └── index.less │ └── WindowOperation │ │ ├── index.jsx │ │ └── index.less ├── main.jsx ├── pages │ ├── AboutWindow │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── ChatGroupNotice │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── ChatWindow │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── Command │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── ForgetPassword │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── Home │ │ ├── Chat │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── Friend │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── Notify │ │ │ ├── FriendNotify │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── SystemNotify │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── Set │ │ │ ├── General │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── MessageNotify │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── Shortcut │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── Talk │ │ │ ├── AllTalk │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── CreateTalk │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── DetailTalk │ │ │ │ ├── index.jsx │ │ │ │ └── index.less │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── ImageViewer │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── Login │ │ ├── AccountLogin │ │ │ └── index.jsx │ │ ├── LoginSet │ │ │ └── index.jsx │ │ ├── QrCodeLogin │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── MessageBox │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── Register │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── TrayMenu │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ ├── VideoChat │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx │ └── screenshot │ │ ├── index.jsx │ │ ├── index.less │ │ └── window.jsx ├── store │ ├── chat │ │ ├── action.js │ │ ├── reducer.js │ │ └── type.js │ ├── home │ │ ├── action.js │ │ ├── reducer.js │ │ └── type.js │ └── index.jsx ├── styles.css └── utils │ ├── api.js │ ├── date.js │ ├── emoji.js │ ├── img.js │ ├── shortcut.js │ ├── storage.js │ ├── string.js │ └── ws.js └── vite.config.js /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/.github/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | linyu 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linyu-client", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@tauri-apps/api": "^2.0.0", 14 | "@tauri-apps/plugin-autostart": "^2.0.0", 15 | "@tauri-apps/plugin-clipboard-manager": "^2.0.0", 16 | "@tauri-apps/plugin-dialog": "^2.0.0", 17 | "@tauri-apps/plugin-fs": "^2.0.0", 18 | "@tauri-apps/plugin-global-shortcut": "^2.0.0", 19 | "@tauri-apps/plugin-http": "^2.0.0", 20 | "@tauri-apps/plugin-process": "^2.0.0", 21 | "@tauri-apps/plugin-shell": "^2.0.0", 22 | "@tauri-apps/plugin-updater": "^2.0.0", 23 | "@tauri-apps/plugin-upload": "^2.0.0", 24 | "@tauri-apps/plugin-websocket": "^2.0.0", 25 | "jsencrypt": "^3.3.2", 26 | "less": "^4.1.3", 27 | "less-loader": "^11.0.0", 28 | "qrcode.react": "^4.1.0", 29 | "quill": "^2.0.2", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "react-dropzone": "^14.2.3", 33 | "react-focus-lock": "^2.12.1", 34 | "react-quill": "^2.0.0", 35 | "react-redux": "^8.0.5", 36 | "react-router": "^5.3.4", 37 | "react-router-dom": "^5.3.4", 38 | "react-viewer": "^3.2.2", 39 | "redux": "^4.2.0" 40 | }, 41 | "devDependencies": { 42 | "@tauri-apps/cli": "^2.0.0-rc.1", 43 | "@vitejs/plugin-react": "^4.2.1", 44 | "vite": "^5.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/bg.png -------------------------------------------------------------------------------- /public/file-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/file-download.png -------------------------------------------------------------------------------- /public/file-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/file-upload.png -------------------------------------------------------------------------------- /public/icon/chat-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/chat-empty.png -------------------------------------------------------------------------------- /public/icon/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/chat.png -------------------------------------------------------------------------------- /public/icon/notify-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/notify-empty.png -------------------------------------------------------------------------------- /public/icon/notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/notify.png -------------------------------------------------------------------------------- /public/icon/set-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/set-empty.png -------------------------------------------------------------------------------- /public/icon/set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/set.png -------------------------------------------------------------------------------- /public/icon/talk-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/talk-empty.png -------------------------------------------------------------------------------- /public/icon/talk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/talk.png -------------------------------------------------------------------------------- /public/icon/user-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/user-empty.png -------------------------------------------------------------------------------- /public/icon/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/icon/user.png -------------------------------------------------------------------------------- /public/id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/id.png -------------------------------------------------------------------------------- /public/linyu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/linyu.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/logo.png -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/updater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/public/updater.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "linyu" 3 | version = "0.0.1" 4 | description = "linyu" 5 | authors = ["cershy"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [build-dependencies] 11 | tauri-build = { version = "2.0.0-rc.0", features = [] } 12 | 13 | 14 | [dependencies] 15 | tauri = { version = "2.0.0", features = ["tray-icon", "image-png"] } 16 | tauri-plugin-shell = "2.0.0" 17 | serde = { version = "1", features = ["derive"] } 18 | serde_json = "1" 19 | tauri-plugin-websocket = "2.0.0" 20 | tauri-plugin-http = "2.0.0" 21 | tauri-plugin-process = "2.0.0" 22 | tauri-plugin-fs = "2.0.0" 23 | tauri-plugin-dialog = "2.0.0" 24 | tauri-plugin-upload = "2.0.0" 25 | tauri-plugin-global-shortcut = "2.0.0" 26 | tauri-plugin-autostart = "2.0.0" 27 | lazy_static = "1.4" 28 | screenshots = "0.5.4" 29 | base64 = "0.22.1" 30 | tauri-plugin-clipboard-manager = "2.0.0" 31 | [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] 32 | tauri-plugin-updater = "2.0.0" 33 | rodio = "0.17.3" 34 | 35 | -------------------------------------------------------------------------------- /src-tauri/audio/remind-short.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/audio/remind-short.wav -------------------------------------------------------------------------------- /src-tauri/audio/remind.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/audio/remind.wav -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "*" 7 | ], 8 | "permissions": [ 9 | "core:path:default", 10 | "core:event:default", 11 | "core:window:default", 12 | "core:window:allow-start-dragging", 13 | "core:window:allow-close", 14 | "core:window:allow-hide", 15 | "core:window:allow-center", 16 | "core:window:allow-show", 17 | "core:window:allow-maximize", 18 | "core:window:allow-minimize", 19 | "core:window:allow-destroy", 20 | "core:window:allow-is-focused", 21 | "core:window:allow-is-fullscreen", 22 | "core:window:allow-set-focus", 23 | "core:window:allow-set-position", 24 | "core:window:allow-scale-factor", 25 | "core:window:allow-unminimize", 26 | "core:window:allow-set-always-on-top", 27 | "core:window:allow-set-size", 28 | "core:window:allow-unmaximize", 29 | "core:app:default", 30 | "core:image:default", 31 | "core:resources:default", 32 | "core:menu:default", 33 | "core:tray:default", 34 | "core:tray:allow-new", 35 | "core:tray:allow-set-tooltip", 36 | "core:tray:allow-set-icon", 37 | "core:tray:allow-get-by-id", 38 | "shell:allow-open", 39 | "websocket:default", 40 | "http:default", 41 | "process:default", 42 | "process:allow-exit", 43 | "core:webview:default", 44 | "core:webview:allow-create-webview", 45 | "core:webview:allow-create-webview-window", 46 | "clipboard-manager:default", 47 | "clipboard-manager:allow-write-image", 48 | "clipboard-manager:allow-write-text", 49 | "dialog:allow-save", 50 | "upload:allow-download", 51 | "dialog:allow-open", 52 | "upload:allow-upload", 53 | "fs:default", 54 | "fs:write-all", 55 | "fs:allow-stat", 56 | "fs:write-all", 57 | "global-shortcut:default", 58 | "global-shortcut:allow-register", 59 | "global-shortcut:allow-is-registered", 60 | "global-shortcut:allow-unregister", 61 | "fs:default", 62 | "autostart:allow-enable", 63 | "autostart:allow-disable", 64 | "autostart:default", 65 | "updater:allow-check", 66 | "updater:default", 67 | { 68 | "identifier": "http:default", 69 | "allow": [ 70 | { 71 | "url": "http://**" 72 | }, 73 | { 74 | "url": "https://**" 75 | }, 76 | { 77 | "url": "http://*:*" 78 | }, 79 | { 80 | "url": "https://*:*" 81 | } 82 | ] 83 | } 84 | ] 85 | } -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | mod tray; 4 | mod user_cmd; 5 | use user_cmd::{get_user_info, save_user_info,default_window_icon,screenshot,audio}; 6 | use tauri_plugin_autostart::MacosLauncher; 7 | 8 | fn main() { 9 | tauri::Builder::default() 10 | .plugin(tauri_plugin_process::init()) 11 | .plugin(tauri_plugin_http::init()) 12 | .plugin(tauri_plugin_websocket::init()) 13 | .plugin(tauri_plugin_shell::init()) 14 | .plugin(tauri_plugin_fs::init()) 15 | .plugin(tauri_plugin_upload::init()) 16 | .plugin(tauri_plugin_dialog::init()) 17 | .plugin(tauri_plugin_clipboard_manager::init()) 18 | .plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec!["--flag1"]))) 19 | .setup(move |app| { 20 | app.handle().plugin(tauri_plugin_global_shortcut::Builder::new().build())?; 21 | tray::create_tray(app.handle())?; 22 | #[cfg(desktop)] 23 | app.handle().plugin(tauri_plugin_updater::Builder::new().build())?; 24 | Ok(()) 25 | }) 26 | .invoke_handler(tauri::generate_handler![get_user_info, save_user_info,default_window_icon,screenshot,audio]) 27 | .run(tauri::generate_context!()) 28 | .expect("error while running tauri application"); 29 | } 30 | -------------------------------------------------------------------------------- /src-tauri/src/tray.rs: -------------------------------------------------------------------------------- 1 | use tauri::{ 2 | tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, Emitter, Manager, Runtime 3 | }; 4 | 5 | pub fn create_tray(app: &tauri::AppHandle) -> tauri::Result<()> { 6 | let _ = TrayIconBuilder::with_id("tray") 7 | .tooltip("linyu") 8 | .icon(app.default_window_icon().unwrap().clone()) 9 | .on_tray_icon_event(|tray, event| match event { 10 | TrayIconEvent::Click { 11 | id: _, 12 | position, 13 | rect: _, 14 | button, 15 | button_state: _, 16 | } => match button { 17 | MouseButton::Left {} => { 18 | let windows = tray.app_handle().webview_windows(); 19 | for (key, value) in windows { 20 | if key == "login" || key == "home" { 21 | value.show().unwrap(); 22 | value.unminimize().unwrap(); 23 | value.set_focus().unwrap(); 24 | } 25 | } 26 | } 27 | MouseButton::Right {} => { 28 | tray.app_handle().emit("tray_menu", position).unwrap(); 29 | } 30 | _ => {} 31 | }, 32 | TrayIconEvent::Enter { 33 | id: _, 34 | position, 35 | rect: _, 36 | } => { 37 | tray.app_handle().emit("tray_enter", position).unwrap(); 38 | } 39 | TrayIconEvent::Leave { 40 | id: _, 41 | position, 42 | rect: _, 43 | } => { 44 | tray.app_handle().emit("tray_leave", position).unwrap(); 45 | } 46 | _ => {} 47 | }) 48 | .build(app); 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /src-tauri/src/user_cmd.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose, Engine as _}; 2 | use lazy_static::lazy_static; 3 | use screenshots::Screen; 4 | use serde::Serialize; 5 | use std::sync::{Arc, RwLock}; 6 | use std::thread::{sleep, spawn}; 7 | use std::time::Duration; 8 | use tauri::path::BaseDirectory; 9 | use tauri::{AppHandle, Manager, ResourceId, Runtime, Webview}; 10 | 11 | // 定义用户信息结构体 12 | #[derive(Debug, Clone, Serialize)] 13 | pub struct UserInfo { 14 | user_id: String, 15 | username: String, 16 | token: String, 17 | portrait: String, 18 | } 19 | 20 | // 全局变量 21 | lazy_static! { 22 | static ref USER_INFO: Arc> = Arc::new(RwLock::new(UserInfo { 23 | user_id: String::new(), 24 | username: String::new(), 25 | token: String::new(), 26 | portrait: String::new(), 27 | })); 28 | } 29 | 30 | // 保存用户信息的方法 31 | #[tauri::command] 32 | pub fn save_user_info(userid: &str, username: &str, token: &str, portrait: &str) -> i32 { 33 | let mut user_info = USER_INFO.write().unwrap(); 34 | user_info.user_id = userid.to_string(); 35 | user_info.username = username.to_string(); 36 | user_info.token = token.to_string(); 37 | user_info.portrait = portrait.to_string(); 38 | 0 39 | } 40 | 41 | // 获取用户信息的方法 42 | #[tauri::command] 43 | pub fn get_user_info() -> UserInfo { 44 | let user_info = USER_INFO.read().unwrap(); 45 | user_info.clone() 46 | } 47 | 48 | #[tauri::command] 49 | pub fn default_window_icon( 50 | webview: Webview, 51 | app: AppHandle, 52 | ) -> Option { 53 | app.default_window_icon().cloned().map(|icon| { 54 | let mut resources_table = webview.resources_table(); 55 | resources_table.add(icon.to_owned()) 56 | }) 57 | } 58 | 59 | #[tauri::command] 60 | pub fn screenshot(x: &str, y: &str, width: &str, height: &str) -> String { 61 | let screen = Screen::from_point(100, 100).unwrap(); 62 | let image = screen 63 | .capture_area( 64 | x.parse::().unwrap(), 65 | y.parse::().unwrap(), 66 | width.parse::().unwrap(), 67 | height.parse::().unwrap(), 68 | ) 69 | .unwrap(); 70 | let buffer = image.buffer(); 71 | let base64_str = general_purpose::STANDARD_NO_PAD.encode(buffer); 72 | base64_str 73 | } 74 | 75 | #[tauri::command] 76 | pub fn audio(filename: &str, handle: tauri::AppHandle) { 77 | use rodio::{Decoder, Source}; 78 | use std::fs::File; 79 | use std::io::BufReader; 80 | let path = "audio/".to_string() + filename; 81 | spawn(move || { 82 | let audio_path = handle 83 | .path() 84 | .resolve(path, BaseDirectory::Resource) 85 | .unwrap(); 86 | let audio = File::open(audio_path).unwrap(); 87 | let file = BufReader::new(audio); 88 | let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap(); 89 | let source = Decoder::new(file).unwrap(); 90 | stream_handle.play_raw(source.convert_samples()).unwrap(); 91 | sleep(Duration::from_millis(3000)); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "林语", 3 | "version": "0.0.1", 4 | "identifier": "com.cershy", 5 | "build": { 6 | "beforeDevCommand": "npm run dev", 7 | "devUrl": "http://localhost:1420", 8 | "beforeBuildCommand": "npm run build", 9 | "frontendDist": "../dist" 10 | }, 11 | "app": { 12 | "windows": [ 13 | { 14 | "title": "林语", 15 | "label": "login", 16 | "width": 360, 17 | "height": 510, 18 | "center": true, 19 | "transparent": true, 20 | "decorations": false, 21 | "fullscreen": false, 22 | "resizable": false, 23 | "shadow": false 24 | } 25 | ], 26 | "security": { 27 | "csp": null 28 | } 29 | }, 30 | "plugins": { 31 | "updater": { 32 | "active": true, 33 | "dialog": true, 34 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVERDExMEEyQzgxQTk4Q0QKUldUTm1CcklvaERSN1NObE4waFUxeUxEZVZxdjBNZ21YZGNBclNaT2RyRkdkdzRERmtVWlNTT0cK", 35 | "endpoints": [ 36 | "https://127.0.0.1/files/updater.json" 37 | ] 38 | } 39 | }, 40 | "bundle": { 41 | "active": true, 42 | "targets": "all", 43 | "icon": [ 44 | "icons/32x32.png", 45 | "icons/128x128.png", 46 | "icons/128x128@2x.png", 47 | "icons/icon.icns", 48 | "icons/icon.ico" 49 | ], 50 | "createUpdaterArtifacts": true, 51 | "windows": { 52 | "wix": { 53 | "language": "zh-CN" 54 | }, 55 | "webviewInstallMode": { 56 | "type": "embedBootstrapper" 57 | } 58 | }, 59 | "resources": { 60 | "audio/*": "audio/" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 4px; 3 | } 4 | 5 | ::-webkit-scrollbar-track { 6 | border-radius: 10px; 7 | } 8 | 9 | ::-webkit-scrollbar-thumb { 10 | border-radius: 10px; 11 | background: rgba(117, 117, 117, 0.3); 12 | } 13 | 14 | .ellipsis { 15 | white-space: nowrap; 16 | overflow: hidden; 17 | text-overflow: ellipsis; 18 | } 19 | 20 | .flex-shrink { 21 | flex-shrink: 0 22 | } 23 | 24 | .dots::after { 25 | content: ''; 26 | animation: dots 1.5s steps(3, end) infinite; 27 | } 28 | 29 | @keyframes dots { 30 | 0%, 20% { 31 | content: '.'; 32 | } 33 | 40% { 34 | content: '..'; 35 | } 36 | 60% { 37 | content: '...'; 38 | } 39 | 80%, 100% { 40 | content: ''; 41 | } 42 | } -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import {Redirect, Route, Switch} from "react-router-dom"; 3 | import Home from "./pages/Home/index.jsx"; 4 | import Login from "./pages/Login/index.jsx"; 5 | import TrayMenu from "./pages/TrayMenu/index.jsx"; 6 | import ChatWindow from "./pages/ChatWindow/index.jsx"; 7 | import MessageBox from "./pages/MessageBox/index.jsx"; 8 | import {useEffect} from "react"; 9 | import Screenshot from "./pages/screenshot/index.jsx"; 10 | import VideoChat from "./pages/VideoChat/index.jsx"; 11 | import ImageViewer from "./pages/ImageViewer/index.jsx"; 12 | import AboutWindow from "./pages/AboutWindow/index.jsx"; 13 | import Register from "./pages/Register/index.jsx"; 14 | import Command from "./pages/Command/index.jsx"; 15 | import ChatGroupNotice from "./pages/ChatGroupNotice/index.jsx"; 16 | import ForgetPassword from "./pages/ForgetPassword/index.jsx"; 17 | 18 | function App() { 19 | 20 | const ignoreKey = ["v", "a", "c", "z"]; 21 | 22 | const onGlobalKeyDown = (e) => { 23 | if ((e.ctrlKey || e.altKey) && ignoreKey.indexOf(e.key) < 0) { 24 | e.preventDefault(); 25 | } 26 | } 27 | 28 | useEffect(() => { 29 | window.addEventListener("keydown", onGlobalKeyDown); 30 | return () => { 31 | window.removeEventListener("keydown", onGlobalKeyDown); 32 | }; 33 | }, []); 34 | 35 | return ( 36 |
e.preventDefault()}> 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | ); 55 | } 56 | 57 | export default App; 58 | -------------------------------------------------------------------------------- /src/api/chatGroup.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | list() { 5 | return Http.get("/v1/api/chat-group/list"); 6 | }, 7 | details(param) { 8 | return Http.post("/v1/api/chat-group/details", param); 9 | }, 10 | create(param) { 11 | return Http.post("/v1/api/chat-group/create", param); 12 | }, 13 | uploadPortrait(file, param) { 14 | return Http.upload(`/v1/api/chat-group/upload/portrait`, file, param) 15 | }, 16 | updateGroupName(param) { 17 | return Http.post("/v1/api/chat-group/update/name", param); 18 | }, 19 | update(param) { 20 | return Http.post("/v1/api/chat-group/update", param); 21 | }, 22 | invite(param) { 23 | return Http.post("/v1/api/chat-group/invite", param); 24 | }, 25 | quit(param) { 26 | return Http.post("/v1/api/chat-group/quit", param); 27 | }, 28 | kick(param) { 29 | return Http.post("/v1/api/chat-group/kick", param); 30 | }, 31 | dissolve(param) { 32 | return Http.post("/v1/api/chat-group/dissolve", param); 33 | }, 34 | transfer(param) { 35 | return Http.post("/v1/api/chat-group/transfer", param); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/api/chatGroupMember.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | list(param) { 5 | return Http.post("/v1/api/chat-group-member/list", param); 6 | }, 7 | listPage(param) { 8 | return Http.post("/v1/api/chat-group-member/list/page", param); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/api/chatGroupNotice.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | list(param) { 5 | return Http.post("/v1/api/chat-group-notice/list", param); 6 | }, 7 | create(param) { 8 | return Http.post("/v1/api/chat-group-notice/create", param); 9 | }, 10 | delete(param) { 11 | return Http.post("/v1/api/chat-group-notice/delete", param); 12 | }, 13 | update(param) { 14 | return Http.post("/v1/api/chat-group-notice/update", param); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/api/chatList.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | list() { 5 | return Http.get("/v1/api/chat-list/list"); 6 | }, 7 | create(param) { 8 | return Http.post("/v1/api/chat-list/create", param); 9 | }, 10 | read(param) { 11 | return Http.get(`/v1/api/chat-list/read/${param}`) 12 | }, 13 | detail(param) { 14 | return Http.post(`/v1/api/chat-list/detail`, param) 15 | }, 16 | delete(param) { 17 | return Http.post("/v1/api/chat-list/delete", param); 18 | }, 19 | top(param) { 20 | return Http.post("/v1/api/chat-list/top", param); 21 | }, 22 | readAll() { 23 | return Http.get(`/v1/api/chat-list/read/all`) 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/api/friend.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | list() { 5 | return Http.get("/v1/api/friend/list"); 6 | }, 7 | details(param) { 8 | return Http.get(`/v1/api/friend/details/${param}`) 9 | }, 10 | search(param) { 11 | return Http.post(`/v1/api/friend/search`, param) 12 | }, 13 | agree(param) { 14 | return Http.post(`/v1/api/friend/agree`, param) 15 | }, 16 | setRemark(param) { 17 | return Http.post(`/v1/api/friend/set/remark`, param) 18 | }, 19 | setGroup(param) { 20 | return Http.post(`/v1/api/friend/set/group`, param) 21 | }, 22 | delete(param) { 23 | return Http.post(`/v1/api/friend/delete`, param) 24 | }, 25 | careFor(param) { 26 | return Http.post(`/v1/api/friend/carefor`, param) 27 | }, 28 | unCareFor(param) { 29 | return Http.post(`/v1/api/friend/uncarefor`, param) 30 | }, 31 | listFlat(param) { 32 | return Http.get("/v1/api/friend/list/flat", param); 33 | }, 34 | listFlatUnread(param) { 35 | return Http.get("/v1/api/friend/list/flat/unread", param); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/api/group.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | create(param) { 5 | return Http.post("/v1/api/group/create", param); 6 | }, 7 | update(param) { 8 | return Http.post("/v1/api/group/update", param); 9 | }, 10 | delete(param) { 11 | return Http.post("/v1/api/group/delete", param); 12 | }, 13 | list() { 14 | return Http.get("/v1/api/group/list"); 15 | }, 16 | }; -------------------------------------------------------------------------------- /src/api/login.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | login(param) { 5 | return Http.post("/v1/api/login", param); 6 | }, 7 | publicKey() { 8 | return Http.get("/v1/api/login/public-key"); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/api/message.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | sendMsg(param) { 5 | return Http.post("/v1/api/message/send", param); 6 | }, 7 | record(param) { 8 | return Http.post("/v1/api/message/record", param); 9 | }, 10 | recordDesc(param) { 11 | return Http.post("/v1/api/message/record/desc", param); 12 | }, 13 | sendFile(param, progressHandler) { 14 | return Http.uploadFile("/v1/api/message/send/file", param, progressHandler) 15 | }, 16 | getFile(param, progressHandler) { 17 | return Http.downloadFile("/v1/api/message/get/file", param, progressHandler) 18 | }, 19 | getMedia(param) { 20 | return Http.get("/v1/api/message/get/media", param) 21 | }, 22 | sendMedia(file, param, progressHandler) { 23 | return Http.upload("/v1/api/message/send/file", file, param) 24 | }, 25 | retraction(param) { 26 | return Http.post("/v1/api/message/retraction", param); 27 | }, 28 | reedit(param) { 29 | return Http.post("/v1/api/message/reedit", param); 30 | }, 31 | voiceToText(param) { 32 | return Http.get("/v1/api/message/voice/to/text", param); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/api/notify.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | friendApply(param) { 5 | return Http.post(`/v1/api/notify/friend/apply`, param) 6 | }, 7 | friendList() { 8 | return Http.get(`/v1/api/notify/friend/list`) 9 | }, 10 | read(param) { 11 | return Http.post(`/v1/api/notify/read`, param) 12 | }, 13 | systemList() { 14 | return Http.get(`/v1/api/notify/system/list`) 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/api/qr.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | code(param) { 5 | return Http.get(`/qr/code`, param) 6 | }, 7 | result(param) { 8 | return Http.post(`/qr/code/result`, param) 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/api/talk.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | list(param) { 5 | return Http.post(`/v1/api/talk/list`, param) 6 | }, 7 | create(param) { 8 | return Http.post(`/v1/api/talk/create`, param) 9 | }, 10 | uploadImg(file, param) { 11 | return Http.upload(`/v1/api/talk/upload/img`, file, param) 12 | }, 13 | delete(param) { 14 | return Http.post(`/v1/api/talk/delete`, param) 15 | }, 16 | details(param) { 17 | return Http.post(`/v1/api/talk/details`, param) 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/talkComment.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | list(param) { 5 | return Http.post(`/v1/api/talk-comment/list`, param) 6 | }, 7 | create(param) { 8 | return Http.post(`/v1/api/talk-comment/create`, param) 9 | }, 10 | delete(param) { 11 | return Http.post(`/v1/api/talk-comment/delete`, param) 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/talkLike.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | list(param) { 5 | return Http.post(`/v1/api/talk-like/list`, param) 6 | }, 7 | create(param) { 8 | return Http.post(`/v1/api/talk-like/create`, param) 9 | }, 10 | delete(param) { 11 | return Http.post(`/v1/api/talk-like/delete`, param) 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | search(param) { 5 | return Http.post(`/v1/api/user/search`, param) 6 | }, 7 | unread() { 8 | return Http.get(`/v1/api/user/unread`) 9 | }, 10 | info() { 11 | return Http.get(`/v1/api/user/info`) 12 | }, 13 | upload(param) { 14 | return Http.upload(`/v1/api/user/upload/portrait`, param) 15 | }, 16 | update(param) { 17 | return Http.post(`/v1/api/user/update`, param) 18 | }, 19 | updatePassword(param) { 20 | return Http.post(`/v1/api/user/update/password`, param) 21 | }, 22 | getImg(param) { 23 | return Http.get("/v1/api/user/get/img", param) 24 | }, 25 | register(param) { 26 | return Http.post("/v1/api/user/register", param) 27 | }, 28 | forget(param) { 29 | return Http.post("/v1/api/user/forget", param) 30 | }, 31 | emailVerification(param) { 32 | return Http.post("/v1/api/user/email/verify", param) 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/api/userSet.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | getUserSet() { 5 | return Http.get(`/v1/api/user-set`) 6 | }, 7 | update(param) { 8 | return Http.post(`/v1/api/user-set/update`, param) 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/api/video.js: -------------------------------------------------------------------------------- 1 | import Http from "../utils/api"; 2 | 3 | export default { 4 | offer(param) { 5 | return Http.post(`/v1/api/video/offer`, param) 6 | }, 7 | answer(param) { 8 | return Http.post(`/v1/api/video/answer`, param) 9 | }, 10 | candidate(param) { 11 | return Http.post(`/v1/api/video/candidate`, param) 12 | }, 13 | hangup(param) { 14 | return Http.post(`/v1/api/video/hangup`, param) 15 | }, 16 | invite(param) { 17 | return Http.post(`/v1/api/video/invite`, param) 18 | }, 19 | accept(param) { 20 | return Http.post(`/v1/api/video/accept`, param) 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/assets/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src/assets/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src/assets/iconfont.woff -------------------------------------------------------------------------------- /src/assets/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src/assets/iconfont.woff2 -------------------------------------------------------------------------------- /src/componets/ChatGroupInvite/index.less: -------------------------------------------------------------------------------- 1 | .chat-group-invite-container { 2 | width: 600px; 3 | height: 540px; 4 | background-color: #FFFFFF; 5 | border-radius: 10px; 6 | user-select: none; 7 | 8 | .chat-group-invite { 9 | display: flex; 10 | height: 100%; 11 | 12 | .invite-left { 13 | width: 220px; 14 | display: flex; 15 | flex-direction: column; 16 | border-right: #e3e3e3 1px solid; 17 | padding: 10px 5px 10px 10px; 18 | overflow: hidden; 19 | 20 | .friend-list { 21 | overflow-y: scroll; 22 | 23 | .friend-list-item { 24 | display: flex; 25 | align-items: center; 26 | height: 40px; 27 | border-radius: 5px; 28 | padding-left: 5px; 29 | margin-right: 5px; 30 | 31 | &:hover { 32 | background-color: #EDF2F9; 33 | } 34 | } 35 | } 36 | } 37 | 38 | .invite-right { 39 | flex: 1; 40 | padding: 10px 5px 10px 10px; 41 | overflow: hidden; 42 | display: flex; 43 | flex-direction: column; 44 | 45 | .friend-list { 46 | overflow-y: scroll; 47 | flex: 1; 48 | border-bottom: #e3e3e3 1px solid; 49 | 50 | .friend-list-item { 51 | display: flex; 52 | align-items: center; 53 | height: 50px; 54 | border-radius: 5px; 55 | padding: 0 10px; 56 | margin-right: 5px; 57 | justify-content: space-between; 58 | 59 | &:hover { 60 | background-color: #EDF2F9; 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/componets/CircularProgressBar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.less'; 3 | 4 | const CircularProgressBar = ({progress, children, size = 60, strokeWidth = 5}) => { 5 | const circleRadius = (size - strokeWidth) / 2; 6 | const circleCircumference = 2 * Math.PI * circleRadius; 7 | const progressOffset = circleCircumference - (progress / 100 * circleCircumference); 8 | 9 | return ( 10 |
11 |
12 | {progress > 0 && 13 | 22 | 35 | 36 | } 37 |
38 | {children} 39 | {progress > 0 &&
40 | {progress < 100 ? `${progress.toFixed(1)}%` : "100%"} 41 |
42 | } 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default CircularProgressBar; 50 | -------------------------------------------------------------------------------- /src/componets/CircularProgressBar/index.less: -------------------------------------------------------------------------------- 1 | .circular-progress-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | 6 | .outer-circle { 7 | position: relative; 8 | width: 100%; 9 | height: 100%; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | 14 | .progress-ring { 15 | transform: rotate(-90deg); 16 | 17 | .progress-ring__circle-bg, 18 | .progress-ring__circle { 19 | transition: stroke-dashoffset 0.5s ease-in-out; 20 | } 21 | } 22 | 23 | .progress-content { 24 | position: absolute; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-direction: column; 29 | 30 | .progress-text { 31 | position: absolute; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | flex-direction: column; 36 | width: 100%; 37 | height: 100%; 38 | border-radius: 100px; 39 | background-color: rgba(148, 148, 148, 0.6); 40 | backdrop-filter: blur(1px); 41 | } 42 | } 43 | } 44 | } 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Call/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {memo, useEffect, useState} from "react"; 3 | import {formatTimingTime} from "../../../../utils/date.js"; 4 | 5 | const Call = memo(({value, right = false}) => { 6 | let [msgContent, setMsgContent] = useState(null) 7 | 8 | useEffect(() => { 9 | let content = JSON.parse(value.msgContent?.content) 10 | setMsgContent(content) 11 | }, [value]) 12 | 13 | return ( 14 | <> 15 |
16 |
17 | 19 |
20 | { 21 | msgContent?.time > 0 ? `通话时长 ${formatTimingTime(msgContent.time)}` : "通话未接通" 22 | } 23 |
24 |
25 |
26 | 27 | ) 28 | }) 29 | export default Call; -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Call/index.less: -------------------------------------------------------------------------------- 1 | .chat-content-call { 2 | display: flex; 3 | flex-direction: row; 4 | margin-bottom: 10px; 5 | white-space: pre-wrap; 6 | word-break: break-all; 7 | word-wrap: break-word; 8 | height: 34px; 9 | min-height: 34px; 10 | 11 | .content { 12 | background-color: #ffffff; 13 | width: 150px; 14 | height: 32px; 15 | border-radius: 5px; 16 | display: flex; 17 | align-items: center; 18 | font-size: 14px; 19 | user-select: none; 20 | } 21 | 22 | .content.right { 23 | color: #FFFFFF; 24 | background-color: #4C9BFF; 25 | margin-left: auto; 26 | } 27 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/File/index.less: -------------------------------------------------------------------------------- 1 | .chat-content-file { 2 | display: flex; 3 | flex-direction: column; 4 | margin-bottom: 10px; 5 | white-space: pre-wrap; 6 | word-break: break-all; 7 | word-wrap: break-word; 8 | height: 76px; 9 | max-height: 76px; 10 | min-height: 76px; 11 | 12 | .content { 13 | border-radius: 15px; 14 | color: #1F1F1F; 15 | font-size: 12px; 16 | font-weight: 600; 17 | padding: 8px 15px; 18 | background-color: #ffffff; 19 | letter-spacing: 0.5px; 20 | vertical-align: top; 21 | cursor: pointer; 22 | } 23 | 24 | .content.left { 25 | margin-right: auto; 26 | } 27 | 28 | .content.right { 29 | color: #fff; 30 | background-color: #4C9BFF; 31 | margin-left: auto; 32 | } 33 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Img/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {memo, useEffect, useRef, useState} from "react"; 3 | import MessageApi from "../../../../api/message.js"; 4 | import CreateImageViewer from "../../../../pages/ImageViewer/window.jsx"; 5 | 6 | const Img = memo(({value, right = false}) => { 7 | const [imgInfo, setImgInfo] = useState(null) 8 | const imgInfoRef = useRef(null) 9 | const fileInfo = useRef() 10 | const [isLoaded, setIsLoaded] = useState(false) 11 | const interval = useRef(null) 12 | const [retryNum, setRetryNum] = useState(0) 13 | const retryNumRef = useRef(0) 14 | 15 | useEffect(() => { 16 | fileInfo.current = JSON.parse(value.msgContent?.content) 17 | MessageApi.getMedia({ 18 | msgId: value.id, 19 | }).then((res) => { 20 | imgInfoRef.current = res?.data 21 | setImgInfo(res?.data) 22 | }) 23 | if (interval.current) clearInterval(interval.current) 24 | interval.current = setInterval(() => { 25 | if (retryNumRef.current > 60) { 26 | clearInterval(interval.current) 27 | return 28 | } 29 | setImgInfo(imgInfoRef.current) 30 | retryNumRef.current = retryNumRef.current + 1 31 | setRetryNum(retryNumRef.current) 32 | }, 1000) 33 | 34 | }, [value]) 35 | 36 | return ( 37 | <> 38 |
39 |
40 | {!isLoaded ? ( 41 |
42 | ) : null} 43 | {imgInfo && { 53 | clearInterval(interval.current) 54 | setIsLoaded(true) 55 | }} 56 | alt="加载失败" 57 | onClick={() => CreateImageViewer(fileInfo.current.name, imgInfo)} 58 | />} 59 |
60 |
61 | 62 | ) 63 | }) 64 | export default Img; -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Img/index.less: -------------------------------------------------------------------------------- 1 | .chat-content-img { 2 | display: flex; 3 | flex-direction: row; 4 | margin-bottom: 10px; 5 | white-space: pre-wrap; 6 | word-break: break-all; 7 | word-wrap: break-word; 8 | height: 100px; 9 | min-height: 100px; 10 | 11 | .loading-spinner { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | width: 100px; 16 | height: 100px; 17 | } 18 | 19 | .loading-spinner::after { 20 | content: " "; 21 | width: 40px; 22 | height: 40px; 23 | border: 4px solid #f3f3f3; 24 | border-top: 4px solid #4C9BFF; 25 | border-radius: 50%; 26 | animation: spin 2s linear infinite; 27 | } 28 | 29 | @keyframes spin { 30 | 0% { 31 | transform: rotate(0deg); 32 | } 33 | 100% { 34 | transform: rotate(360deg); 35 | } 36 | } 37 | 38 | .content { 39 | border-radius: 15px; 40 | color: #1F1F1F; 41 | font-size: 12px; 42 | font-weight: 600; 43 | padding: 8px 15px; 44 | letter-spacing: 0.5px; 45 | max-width: 300px; 46 | vertical-align: top; 47 | } 48 | 49 | .content.right { 50 | color: #fff; 51 | margin-left: auto; 52 | } 53 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/MsgContent/index.jsx: -------------------------------------------------------------------------------- 1 | import Text from "../Text/index.jsx"; 2 | import FileContent from "../File/index.jsx"; 3 | import Img from "../Img/index.jsx"; 4 | import Retraction from "../Retraction/index.jsx"; 5 | import Voice from "../Voice/index.jsx"; 6 | import Call from "../Call/index.jsx"; 7 | 8 | export const MsgContent = ({msg, userId, onReedit}) => { 9 | let isRight = msg.fromId === userId 10 | switch (msg.msgContent?.type) { 11 | case "text": { 12 | return 16 | } 17 | case "file": { 18 | return 22 | } 23 | case "img": { 24 | return 28 | } 29 | case "retraction": { 30 | return 35 | } 36 | case "voice": { 37 | return 41 | } 42 | case "call": { 43 | return 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Retraction/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {memo} from "react"; 3 | 4 | const Retraction = memo(({value, right = false, onReedit}) => { 5 | 6 | return ( 7 | <> 8 |
9 |
{`${right ? "你" : "对方"}撤回了一条消息`}
10 | {right && value?.msgContent?.ext === "text" && 11 | < div 12 | style={{color: "#4C9BFF", marginLeft: 5, cursor: "pointer"}} 13 | onClick={() => { 14 | if (onReedit) onReedit(value) 15 | }} 16 | > 17 | 重新编辑 18 |
19 | } 20 | 21 | 22 | ) 23 | }) 24 | export default Retraction; -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Retraction/index.less: -------------------------------------------------------------------------------- 1 | .chat-content-retraction { 2 | margin-bottom: 10px; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | font-size: 12px; 7 | color: #969696; 8 | user-select: none; 9 | height: 20px; 10 | min-height: 20px; 11 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/SystemMsg/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {useEffect, useState} from "react"; 3 | 4 | export default function SystemMsg({value}) { 5 | const [systemMsgList, setSystemMsgList] = useState([]) 6 | useEffect(() => { 7 | setSystemMsgList(JSON.parse(value?.content)) 8 | }, [value]) 9 | return ( 10 |
11 |
12 | { 13 | systemMsgList?.map(msg => { 14 | return ( 15 | <> 16 | {msg.isEmphasize ? 17 | {msg.content} : 18 | {msg.content} 19 | } 20 | 21 | ) 22 | }) 23 | } 24 |
25 |
26 | ) 27 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/SystemMsg/index.less: -------------------------------------------------------------------------------- 1 | .chat-content-system-msg{ 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | user-select: none; 6 | margin-bottom: 10px; 7 | height: 20px; 8 | min-height: 20px; 9 | 10 | .content { 11 | border-radius: 2px; 12 | padding: 5px; 13 | background-color: rgba(255, 255, 255, 0.8); 14 | font-size: 10px; 15 | line-height: 10px; 16 | } 17 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Text/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {memo} from "react"; 3 | 4 | const Text = memo(({value, right = false}) => { 5 | const emojiRegex = /([\uD800-\uDBFF][\uDC00-\uDFFF])/; 6 | const parts = value.split(emojiRegex); 7 | return ( 8 | <> 9 |
10 |
11 | { 12 | parts.map((part, index) => 13 | emojiRegex.test(part) ? ( 14 | 16 | {part} 17 | 18 | ) : ( 19 | 20 | {part} 21 | 22 | ) 23 | ) 24 | } 25 |
26 |
27 | 28 | ) 29 | }) 30 | export default Text; -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Text/index.less: -------------------------------------------------------------------------------- 1 | .chat-content-msg { 2 | display: flex; 3 | flex-direction: row; 4 | margin-bottom: 10px; 5 | white-space: pre-wrap; 6 | word-break: break-all; 7 | word-wrap: break-word; 8 | min-height: 40px; 9 | 10 | .content { 11 | border-radius: 15px; 12 | color: #1F1F1F; 13 | font-size: 12px; 14 | font-weight: 600; 15 | padding: 8px 15px; 16 | background-color: #ffffff; 17 | letter-spacing: 0.5px; 18 | max-width: 300px; 19 | vertical-align: top; 20 | } 21 | 22 | .content.right { 23 | color: #fff; 24 | background-color: #4C9BFF; 25 | margin-left: auto; 26 | } 27 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Time/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | 3 | export default function Time({value}) { 4 | return ( 5 |
6 |
7 | {value} 8 |
9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Time/index.less: -------------------------------------------------------------------------------- 1 | .chat-content-time { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | user-select: none; 6 | margin-bottom: 10px; 7 | height: 20px; 8 | min-height: 20px; 9 | 10 | .content { 11 | border-radius: 2px; 12 | padding: 5px; 13 | background-color: rgba(255, 255, 255, 0.8); 14 | font-size: 10px; 15 | line-height: 10px; 16 | } 17 | } -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Voice/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {memo, useEffect, useRef, useState} from "react"; 3 | import MessageApi from "../../../../api/message.js"; 4 | import CustomAudio from "../../../CustomAudio/index.jsx"; 5 | 6 | const Voice = memo(({value, right = false}) => { 7 | const [info, setInfo] = useState(null) 8 | const infoRef = useRef(null) 9 | const fileInfo = useRef() 10 | const [audioTime, setAudioTime] = useState(0) 11 | const interval = useRef(null) 12 | const [retryNum, setRetryNum] = useState(0) 13 | const retryNumRef = useRef(0) 14 | const [text, setText] = useState('') 15 | const [loading, setLoading] = useState(true) 16 | 17 | useEffect(() => { 18 | setLoading(value.loading) 19 | fileInfo.current = JSON.parse(value.msgContent?.content) 20 | setAudioTime(fileInfo.current.time) 21 | setText(fileInfo.current.text) 22 | MessageApi.getMedia({ 23 | msgId: value.id, 24 | }).then((res) => { 25 | infoRef.current = res?.data 26 | setInfo(res?.data) 27 | }) 28 | if (interval.current) clearInterval(interval.current) 29 | interval.current = setInterval(() => { 30 | if (retryNumRef.current > 60) { 31 | clearInterval(interval.current) 32 | return 33 | } 34 | setInfo(infoRef.current) 35 | retryNumRef.current = retryNumRef.current + 1 36 | setRetryNum(retryNumRef.current) 37 | }, 1000) 38 | }, [value]) 39 | 40 | return ( 41 | <> 42 |
43 |
44 | {info ? 45 | clearInterval(interval.current)} 51 | /> : 52 |
53 | } 54 | { 55 | text && 56 |
57 | {text} 58 |
59 | } 60 | { 61 | loading && !text && 62 |
63 | 加载中... 64 |
65 | } 66 |
67 |
68 | 69 | ) 70 | }) 71 | export default Voice; -------------------------------------------------------------------------------- /src/componets/CommonChatFrame/ChatContent/Voice/index.less: -------------------------------------------------------------------------------- 1 | .chat-content-voice { 2 | display: flex; 3 | flex-direction: row; 4 | margin-bottom: 10px; 5 | white-space: pre-wrap; 6 | word-break: break-all; 7 | word-wrap: break-word; 8 | min-height: 34px; 9 | 10 | .content { 11 | display: flex; 12 | flex-direction: column; 13 | align-items: start; 14 | justify-content: right; 15 | 16 | .text { 17 | font-size: 14px; 18 | border-radius: 5px; 19 | background-color: #FFF; 20 | margin-top: 1px; 21 | padding: 5px; 22 | max-width: 240px; 23 | 24 | &.right { 25 | background-color: #4C9BFF; 26 | color: #fff; 27 | } 28 | } 29 | } 30 | 31 | .content.right { 32 | color: #fff; 33 | margin-left: auto; 34 | align-items: end; 35 | } 36 | 37 | .loading-spinner { 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | width: 100px; 42 | height: 100px; 43 | } 44 | 45 | .loading-spinner::after { 46 | content: " "; 47 | width: 40px; 48 | height: 40px; 49 | border: 4px solid #f3f3f3; 50 | border-top: 4px solid #4C9BFF; 51 | border-radius: 50%; 52 | animation: spin 2s linear infinite; 53 | } 54 | 55 | @keyframes spin { 56 | 0% { 57 | transform: rotate(0deg); 58 | } 59 | 100% { 60 | transform: rotate(360deg); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/componets/CustomAccordion/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {useEffect, useRef, useState} from "react"; 3 | 4 | export default function CustomAccordion({title, titleEnd, children, onContextMenu}) { 5 | const [isOpen, setIsOpen] = useState(false); 6 | const [contentHeight, setContentHeight] = useState(0) 7 | const accordionContentRef = useRef(null); 8 | 9 | const toggleAccordion = () => { 10 | setIsOpen(!isOpen); 11 | }; 12 | 13 | useEffect(() => { 14 | if (isOpen) { 15 | setContentHeight(accordionContentRef.current.clientHeight) 16 | } else { 17 | setContentHeight(0) 18 | } 19 | }, [isOpen]) 20 | 21 | return ( 22 |
23 |
{ 27 | e.preventDefault() 28 | if (onContextMenu) onContextMenu(e) 29 | }} 30 | > 31 | 35 |
36 | {title} 37 |
38 |
39 | {titleEnd} 40 |
41 |
42 |
48 |
49 | {children} 50 |
51 |
52 |
53 | ); 54 | } -------------------------------------------------------------------------------- /src/componets/CustomAccordion/index.less: -------------------------------------------------------------------------------- 1 | .accordion { 2 | .accordion-title { 3 | padding: 5px; 4 | display: flex; 5 | align-items: center; 6 | font-size: 14px; 7 | position: relative; 8 | 9 | .ellipsis { 10 | white-space: nowrap; 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | } 14 | 15 | .accordion-title-end { 16 | position: absolute; 17 | right: 5px; 18 | } 19 | } 20 | 21 | 22 | .accordion-title.open .arrow { 23 | transform: rotate(90deg); 24 | } 25 | 26 | .arrow { 27 | transition: transform 0.5s ease; 28 | } 29 | 30 | .accordion-content { 31 | overflow: hidden; 32 | transition: max-height 0.5s ease; 33 | } 34 | 35 | .accordion-content.open { 36 | padding: 10px; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/componets/CustomAffirmModal/index.jsx: -------------------------------------------------------------------------------- 1 | import CustomModal from "../CustomModal/index.jsx"; 2 | import IconButton from "../IconButton/index.jsx"; 3 | import CustomButton from "../CustomButton/index.jsx"; 4 | import {useEffect, useState} from "react"; 5 | 6 | export default function CustomAffirmModal({isOpen, txt, onOk, onCancel}) { 7 | 8 | const [interIsOpen, setInterIsOpen] = useState(isOpen) 9 | 10 | useEffect(() => { 11 | setInterIsOpen(isOpen) 12 | }, [isOpen]) 13 | 14 | return ( 15 | 16 |
17 |
26 |
27 | } 30 | onClick={() => { 31 | if (onCancel) onCancel() 32 | }} 33 | /> 34 |
35 |
36 |
37 | {txt} 38 |
39 |
40 | { 43 | if (onOk) onOk() 44 | }} 45 | > 46 | 确定 47 | 48 | { 52 | if (onCancel) onCancel() 53 | }} 54 | > 55 | 取消 56 | 57 |
58 |
59 |
60 | ) 61 | } -------------------------------------------------------------------------------- /src/componets/CustomAffirmModal/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DWHengr/linyu-client/dbee9d47bf4d9d61d0c47fd9b4434c14f3586e30/src/componets/CustomAffirmModal/index.less -------------------------------------------------------------------------------- /src/componets/CustomAudio/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import CustomSoundIcon from "../CustomSoundIcon/index.jsx"; 3 | import React, {useRef, useState} from "react"; 4 | 5 | export default function CustomAudio({audioUrl, time, onLoadedMetadata, type = ""}) { 6 | const [audioTime, setAudioTime] = useState(time) 7 | const audioRef = useRef(null) 8 | const [isPlay, setIsPlay] = useState(false) 9 | const [isPause, setIsPause] = useState(true) 10 | 11 | const playAudio = () => { 12 | if (audioRef.current && isPause) { 13 | audioRef.current.pause(); 14 | setIsPlay(true) 15 | audioRef.current.currentTime = 0; 16 | audioRef.current.play(); 17 | setIsPause(false) 18 | } else { 19 | audioRef.current.pause(); 20 | setIsPause(true) 21 | setIsPlay(false) 22 | } 23 | }; 24 | 25 | return (
26 |
27 | 29 |
{audioTime}"
30 |
42 |
) 43 | } -------------------------------------------------------------------------------- /src/componets/CustomAudio/index.less: -------------------------------------------------------------------------------- 1 | .custom-audio { 2 | background-color: #4C9BFF; 3 | width: 150px; 4 | height: 32px; 5 | border-radius: 5px; 6 | color: #FFFFFF; 7 | display: flex; 8 | align-items: center; 9 | cursor: pointer; 10 | 11 | &.minor { 12 | background-color: #FFFFFF; 13 | color: #1F1F1F; 14 | } 15 | } -------------------------------------------------------------------------------- /src/componets/CustomBox/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {useEffect, useState} from "react"; 3 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow"; 4 | import CustomDragDiv from "../CustomDragDiv/index.jsx"; 5 | 6 | export default function CustomBox({children, className}) { 7 | let [isFull, setIsFull] = useState(false) 8 | useEffect(() => { 9 | const window = WebviewWindow.getCurrent() 10 | let unResize = window.listen("tauri://resize", async function (e) { 11 | let isFull = await window.isMaximized() 12 | setIsFull(isFull) 13 | }); 14 | return async () => { 15 | (await unResize)(); 16 | } 17 | }) 18 | return ( 19 |
20 | 21 | {children} 22 | 23 |
24 | ) 25 | } -------------------------------------------------------------------------------- /src/componets/CustomBox/index.less: -------------------------------------------------------------------------------- 1 | .custom-box-container { 2 | padding: 5px; 3 | overflow: hidden; 4 | 5 | .custom-box { 6 | overflow: hidden; 7 | border: 1px solid #E1E0E0; 8 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 9 | height: calc(100vh - 10px); 10 | width: calc(100vw - 10px); 11 | border-radius: 10px; 12 | } 13 | 14 | &.full { 15 | padding: 0; 16 | 17 | .custom-box { 18 | height: 100vh; 19 | width: 100vw; 20 | border-radius: 0; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/componets/CustomButton/index.css: -------------------------------------------------------------------------------- 1 | .custom-button { 2 | display: flex; 3 | border-radius: 5px; 4 | padding: 2px 8px; 5 | color: #fff; 6 | background-color: #4C9BFF; 7 | cursor: pointer; 8 | font-size: 14px; 9 | margin: 0 2px; 10 | justify-content: center; 11 | align-items: center; 12 | user-select: none; 13 | 14 | &:hover { 15 | background-color: rgba(76, 155, 255, 0.8); 16 | } 17 | } 18 | 19 | .custom-button.minor { 20 | color: #1F1F1F; 21 | background-color: #EDF2F9; 22 | 23 | &:hover { 24 | background-color: #E3ECFF; 25 | } 26 | } 27 | 28 | .custom-button.error { 29 | background-color: #ff4c4c; 30 | 31 | &:hover { 32 | background-color: rgba(255, 76, 76, 0.6); 33 | } 34 | } 35 | 36 | 37 | .custom-button.disabled { 38 | cursor: not-allowed; 39 | background-color: rgba(76, 155, 255, 0.5); 40 | 41 | &:hover { 42 | background-color: rgba(76, 155, 255, 0.5); 43 | } 44 | } 45 | 46 | .custom-button.line { 47 | color: #1F1F1F; 48 | background-color: transparent; 49 | border: #C4C4C4 1px solid; 50 | 51 | &:hover { 52 | background-color: #E3ECFF; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/componets/CustomButton/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.css" 2 | 3 | export default function CustomButton({children, onClick, width, type = "", style, disabled = false}) { 4 | return ( 5 | <> 6 |
{ 10 | if (onClick && !disabled) onClick() 11 | }} 12 | > 13 | {children} 14 |
15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/componets/CustomDragDiv/index.jsx: -------------------------------------------------------------------------------- 1 | import {emit} from "@tauri-apps/api/event"; 2 | 3 | export default function CustomDragDiv(props) { 4 | return ( 5 |
emit('drag-click', {})}> 6 | {props.children} 7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /src/componets/CustomDrawer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.less'; 3 | 4 | const CustomDrawer = ({isOpen, onClose, children, width = 300}) => { 5 | const closeDrawer = (e) => { 6 | if (e.target.classList.contains('drawer-overlay')) { 7 | onClose(); 8 | } 9 | }; 10 | 11 | return ( 12 |
13 |
14 |
15 | {children} 16 |
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default CustomDrawer; -------------------------------------------------------------------------------- /src/componets/CustomDrawer/index.less: -------------------------------------------------------------------------------- 1 | .drawer-wrapper { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | pointer-events: none; 8 | z-index: 1112; 9 | margin: 5px; 10 | border-radius: 10px; 11 | overflow: hidden; 12 | } 13 | 14 | .drawer-overlay { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | bottom: 0; 20 | display: flex; 21 | justify-content: flex-end; 22 | opacity: 0.5; 23 | transition: opacity 0.3s ease; 24 | } 25 | 26 | .drawer { 27 | width: 340px; 28 | height: 100%; 29 | border-radius: 0 10px 10px 0; 30 | box-shadow: -2px 0 5px rgba(0, 0, 0, 0.2); 31 | background-image: linear-gradient(-130deg, rgba(249, 251, 255, 0.7), rgba(227, 236, 255, 0.7)); 32 | backdrop-filter: blur(15px); 33 | border-left: #fff 2px solid; 34 | overflow-y: auto; 35 | transform: translateX(110%); 36 | transition: transform 0.3s ease-out; 37 | } 38 | 39 | .drawer-wrapper.open { 40 | pointer-events: auto; 41 | 42 | .drawer-overlay { 43 | opacity: 1; 44 | } 45 | 46 | .drawer { 47 | transform: translateX(0); 48 | } 49 | } -------------------------------------------------------------------------------- /src/componets/CustomDropdown/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useRef, useEffect} from 'react'; 2 | import './index.less'; 3 | 4 | const CustomDropdown = ({options, defaultValue, onSelect, width = 80, placeholder = "请选择内容"}) => { 5 | const [isOpen, setIsOpen] = useState(false); 6 | const [selectedValue, setSelectedValue] = useState(defaultValue || ''); 7 | 8 | const dropdownRef = useRef(null); 9 | 10 | useEffect(() => { 11 | setSelectedValue(defaultValue) 12 | }, [defaultValue]) 13 | 14 | const toggleDropdown = () => { 15 | setIsOpen(!isOpen); 16 | }; 17 | 18 | const handleSelect = (option) => { 19 | setSelectedValue(option.label); 20 | if (onSelect) onSelect(option); 21 | setIsOpen(false); 22 | }; 23 | 24 | const handleClickOutside = (event) => { 25 | if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { 26 | setIsOpen(false); 27 | } 28 | }; 29 | 30 | useEffect(() => { 31 | document.addEventListener('mousedown', handleClickOutside); 32 | return () => { 33 | document.removeEventListener('mousedown', handleClickOutside); 34 | }; 35 | }, []); 36 | 37 | return ( 38 |
39 |
40 |
41 | {selectedValue || placeholder} 42 |
43 | setShowRecentInput(!showRecentInput)} 46 | /> 47 |
48 | {isOpen && ( 49 |
50 | {options.map((option, index) => ( 51 |
handleSelect(option)} 55 | > 56 | {option.label} 57 |
58 | ))} 59 |
60 | )} 61 |
62 | ); 63 | }; 64 | 65 | export default CustomDropdown; 66 | -------------------------------------------------------------------------------- /src/componets/CustomDropdown/index.less: -------------------------------------------------------------------------------- 1 | .custom-select { 2 | position: relative; 3 | user-select: none; 4 | 5 | .selected-box { 6 | border-radius: 5px; 7 | padding: 0 5px; 8 | border: 1px solid rgba(204, 204, 204, 0.5); 9 | cursor: pointer; 10 | background-color: #fff; 11 | display: flex; 12 | 13 | .selected-value { 14 | white-space: nowrap; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | } 18 | 19 | .arrow { 20 | font-size: 12px; 21 | } 22 | 23 | } 24 | 25 | .options { 26 | position: absolute; 27 | top: 100%; 28 | left: 0; 29 | width: 100%; 30 | border: 1px solid #ccc; 31 | //background-color: rgba(237, 242, 249, 0.95); 32 | //backdrop-filter: blur(2px); 33 | border-radius: 0 0 5px 5px; 34 | z-index: 999; 35 | user-select: none; 36 | } 37 | 38 | .option { 39 | padding: 2px 4px; 40 | cursor: pointer; 41 | white-space: nowrap; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | } 45 | 46 | .option:hover { 47 | background-color: rgb(220, 234, 255); 48 | } 49 | 50 | .selected { 51 | background-color: rgb(220, 234, 255); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/componets/CustomEditableText/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useRef, useEffect} from 'react'; 2 | import "./index.less" 3 | 4 | const CustomEditableText = ({text, onSave, placeholder, style, inputStyle = {}, readOnly = false}) => { 5 | const [isEditing, setIsEditing] = useState(false); 6 | const [value, setValue] = useState(text); 7 | const inputRef = useRef(null); 8 | 9 | const handleClick = () => { 10 | if (readOnly) return 11 | setIsEditing(true); 12 | }; 13 | 14 | useEffect(() => { 15 | setValue(text) 16 | }, [text]); 17 | 18 | const handleClickOutside = (event) => { 19 | if (inputRef.current && !inputRef.current.contains(event.target)) { 20 | setIsEditing(false); 21 | if (value !== text) { 22 | onSave(value); 23 | } 24 | } 25 | }; 26 | 27 | useEffect(() => { 28 | if (isEditing) { 29 | document.addEventListener('mousedown', handleClickOutside); 30 | inputRef.current.focus(); 31 | } else { 32 | document.removeEventListener('mousedown', handleClickOutside); 33 | } 34 | return () => { 35 | document.removeEventListener('mousedown', handleClickOutside); 36 | }; 37 | }, [isEditing, value, text, onSave]); 38 | 39 | const handleChange = (event) => { 40 | setValue(event.target.value); 41 | }; 42 | 43 | const handleBlur = () => { 44 | setIsEditing(false); 45 | if (value !== text) { 46 | onSave(value); 47 | } 48 | }; 49 | 50 | return ( 51 |
56 | {isEditing ? ( 57 | 66 | ) : ( 67 |
68 | {text || {placeholder ? placeholder : "请设置值"}} 69 |
70 | )} 71 |
72 | ); 73 | }; 74 | 75 | export default CustomEditableText; 76 | -------------------------------------------------------------------------------- /src/componets/CustomEditableText/index.less: -------------------------------------------------------------------------------- 1 | .custom-editable-text { 2 | input { 3 | outline-color: #4C9BFF; 4 | padding: 5px; 5 | box-sizing: border-box; 6 | height: 34px; 7 | } 8 | } -------------------------------------------------------------------------------- /src/componets/CustomEmpty/index.jsx: -------------------------------------------------------------------------------- 1 | import CustomDragDiv from "../CustomDragDiv/index.jsx"; 2 | 3 | export default function CustomEmpty({placeholder = "搜索结果为空~"}) { 4 | return ( 5 | 13 | empty 14 |
{placeholder}
15 |
16 | ) 17 | } -------------------------------------------------------------------------------- /src/componets/CustomImg/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less"; 2 | import {memo, useEffect, useState} from "react"; 3 | import UserAPi from "../../api/user.js"; 4 | import CreateImageViewer from "../../pages/ImageViewer/window.jsx"; 5 | 6 | const CustomImg = memo(({fileName, targetId}) => { 7 | const [imgInfo, setImgInfo] = useState(null); 8 | const [isLoaded, setIsLoaded] = useState(false); 9 | 10 | useEffect(() => { 11 | UserAPi.getImg({fileName, targetId}) 12 | .then((res) => { 13 | setImgInfo(res.data); 14 | }); 15 | }, [fileName, targetId]); 16 | 17 | const handleImageLoad = () => { 18 | setIsLoaded(true); 19 | }; 20 | 21 | return ( 22 |
e.stopPropagation()}> 23 | {imgInfo && !isLoaded ? ( 24 |
25 | ) : null} 26 | {imgInfo ? ( 27 | { 34 | CreateImageViewer(fileName, imgInfo); 35 | }} 36 | /> 37 | ) : null} 38 |
39 | ) 40 | }); 41 | 42 | export default CustomImg; 43 | -------------------------------------------------------------------------------- /src/componets/CustomImg/index.less: -------------------------------------------------------------------------------- 1 | .custom-img { 2 | width: 100px; 3 | height: 100px; 4 | border-radius: 5px; 5 | margin: 2px; 6 | 7 | .img { 8 | width: 100px; 9 | height: 100px; 10 | border-radius: 5px; 11 | object-fit: cover; 12 | } 13 | 14 | } 15 | 16 | .loading-spinner { 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | width: 100px; 21 | height: 100px; 22 | } 23 | 24 | .loading-spinner::after { 25 | content: " "; 26 | width: 40px; 27 | height: 40px; 28 | border: 4px solid #f3f3f3; 29 | border-top: 4px solid #4C9BFF; 30 | border-radius: 50%; 31 | animation: spin 2s linear infinite; 32 | } 33 | 34 | @keyframes spin { 35 | 0% { 36 | transform: rotate(0deg); 37 | } 38 | 100% { 39 | transform: rotate(360deg); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/componets/CustomInput/index.less: -------------------------------------------------------------------------------- 1 | .custom-input { 2 | height: 30px; 3 | min-height: 30px; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | background-color: #FFF; 8 | border-radius: 4px; 9 | border: #d9d9d9 1px solid; 10 | padding: 0 10px; 11 | font-size: 14px; 12 | 13 | &.required { 14 | border: #ff4c4c 1px solid; 15 | } 16 | 17 | .pre { 18 | flex-shrink: 0; 19 | min-width: 50px; 20 | } 21 | 22 | .operation { 23 | display: flex; 24 | justify-items: center; 25 | align-items: center; 26 | 27 | .operation-icon { 28 | font-size: 16px; 29 | color: #7F7F7F; 30 | } 31 | } 32 | 33 | 34 | input { 35 | font-size: 14px; 36 | padding: 0; 37 | margin: 0; 38 | border: none; 39 | outline: none; 40 | width: 100%; 41 | background-color: transparent; 42 | } 43 | 44 | input::placeholder { 45 | color: #7F7F7F 46 | } 47 | 48 | input[type="password"]::-ms-reveal { 49 | display: none 50 | } 51 | 52 | .character-count { 53 | flex-shrink: 0; 54 | margin-left: 5px; 55 | min-width: 36px; 56 | color: #7F7F7F 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/componets/CustomLine/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | 3 | export default function CustomLine({width, direction = "horizontal", size}) { 4 | return ( 5 |
14 |
15 | ) 16 | } -------------------------------------------------------------------------------- /src/componets/CustomLine/index.less: -------------------------------------------------------------------------------- 1 | .custom-line { 2 | &.horizontal { 3 | height: 1px; 4 | width: 100%; 5 | border-top: solid #C9C9C9 2px; 6 | } 7 | 8 | &.vertical { 9 | display: inline-block; 10 | width: 1px; 11 | height: 100%; 12 | border-right: solid #C9C9C9 2px; 13 | margin: 0 5px; 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/componets/CustomModal/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import './index.less'; 3 | 4 | const CustomModal = ({isOpen, onClose, children, overlayColor = "rgba(0,0,0,0.1)"}) => { 5 | // 处理点击空白处关闭 6 | useEffect(() => { 7 | if (!onClose) return 8 | const handleClickOutside = (event) => { 9 | if (event.target.classList.contains('modal-overlay')) { 10 | onClose() 11 | } 12 | }; 13 | 14 | if (isOpen) { 15 | window.addEventListener('click', handleClickOutside); 16 | } 17 | 18 | return () => { 19 | window.removeEventListener('click', handleClickOutside); 20 | }; 21 | }, [isOpen, onClose]); 22 | 23 | if (!isOpen) return null; 24 | 25 | return ( 26 |
27 |
32 |
33 | {children} 34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default CustomModal; 41 | -------------------------------------------------------------------------------- /src/componets/CustomModal/index.less: -------------------------------------------------------------------------------- 1 | .modal-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: calc(100vw - 9px); 6 | height: calc(100vh - 9px); 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | z-index: 1113; 11 | border-radius: 10px; 12 | margin: 5px; 13 | } -------------------------------------------------------------------------------- /src/componets/CustomOverlay/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {useEffect, useRef, useState} from "react"; 3 | import {listen} from "@tauri-apps/api/event"; 4 | 5 | export default function CustomOverlay({visible, position, width, children, onClose}) { 6 | const [inVisible, setInVisible] = useState(visible) 7 | const contentRef = useRef(null); 8 | 9 | useEffect(() => { 10 | setInVisible(visible) 11 | }, [visible]) 12 | 13 | useEffect(() => { 14 | if (contentRef.current) { 15 | contentRef.current.focus() 16 | } 17 | }, [inVisible]) 18 | 19 | useEffect(() => { 20 | const unListen = listen('drag-click', (event) => { 21 | setInVisible(false) 22 | if (onClose) onClose() 23 | }); 24 | return async () => { 25 | (await unListen)() 26 | } 27 | }, []) 28 | 29 | return ( 30 |
{ 31 | e.stopPropagation() 32 | }}> 33 |
34 | {inVisible && 35 |
{ 41 | setInVisible(false); 42 | if (onClose) onClose() 43 | }} 44 | > 45 | {children} 46 |
47 | } 48 |
49 |
50 | ) 51 | } -------------------------------------------------------------------------------- /src/componets/CustomOverlay/index.less: -------------------------------------------------------------------------------- 1 | .custom-overlay { 2 | z-index: 999; 3 | 4 | .content { 5 | position: absolute; 6 | } 7 | } -------------------------------------------------------------------------------- /src/componets/CustomPwdInput/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {useState} from "react"; 3 | 4 | export default function CustomPwdInput({value, onChange, onKeyDown}) { 5 | const [inputValue, setInputValue] = useState(value) 6 | const onCleanValue = () => { 7 | setInputValue("") 8 | onChange("") 9 | } 10 | 11 | return ( 12 |
13 |
14 | { 19 | setInputValue(e.target.value) 20 | if (onChange) onChange(e.target.value) 21 | }} 22 | onKeyDown={(e) => { 23 | if (onKeyDown) onKeyDown(e) 24 | }} 25 | 26 | /> 27 |
28 |
29 | {inputValue ? 30 | : 34 |
35 | } 36 |
37 |
38 | ) 39 | } -------------------------------------------------------------------------------- /src/componets/CustomPwdInput/index.less: -------------------------------------------------------------------------------- 1 | .custom-pwd-input { 2 | height: 40px; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | background-color: #FFF; 7 | border-radius: 10px; 8 | width: 260px; 9 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 10 | 11 | .placeholder { 12 | flex: 1; 13 | width: 40px; 14 | height: 10px; 15 | } 16 | 17 | .operation { 18 | flex: 1; 19 | width: 40px; 20 | display: flex; 21 | justify-items: center; 22 | align-items: center; 23 | 24 | .operation-icon { 25 | width: 20px; 26 | font-size: 16px; 27 | color: #7F7F7F; 28 | } 29 | } 30 | 31 | 32 | input { 33 | font-size: 16px; 34 | padding: 0; 35 | margin: 0; 36 | border: none; 37 | outline: none; 38 | width: 100%; 39 | background-color: transparent; 40 | text-align: center; 41 | flex: 3; 42 | } 43 | 44 | input::placeholder { 45 | color: #7F7F7F 46 | } 47 | 48 | input[type="password"]::-ms-reveal{ 49 | display:none 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/componets/CustomSearchInput/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | 3 | export default function CustomSearchInput({value, onChange, placeholder = "搜索", style = {}}) { 4 | return ( 5 |
6 | { 11 | if (onChange) onChange(e.target.value) 12 | }} 13 | /> 14 |
15 | 16 |
17 |
18 | ) 19 | } -------------------------------------------------------------------------------- /src/componets/CustomSearchInput/index.less: -------------------------------------------------------------------------------- 1 | .custom-search-input { 2 | margin-top: 18px; 3 | margin-bottom: 18px; 4 | height: 38px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | background-color: #E3ECFF; 9 | border-radius: 10px; 10 | 11 | input { 12 | font-size: 14px; 13 | font-weight: 600; 14 | margin: 0 0 0 10px; 15 | border: none; 16 | outline: none; 17 | width: 100%; 18 | background-color: transparent; 19 | } 20 | 21 | input::placeholder { 22 | color: #4C9BFF 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/componets/CustomSelectionIcon/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SelectionIcon = ({status, style}) => { 4 | const getIconColor = () => { 5 | switch (status) { 6 | case 'selected': 7 | return '#4C9BFF'; 8 | case 'unselected': 9 | return '#E0E0E0'; 10 | case 'disabled': 11 | return 'rgba(76,155,255,0.45)'; 12 | default: 13 | return '#E0E0E0'; 14 | } 15 | }; 16 | 17 | return ( 18 | 23 | 26 | 27 | ); 28 | }; 29 | 30 | export default SelectionIcon; -------------------------------------------------------------------------------- /src/componets/CustomShortcutInput/index.less: -------------------------------------------------------------------------------- 1 | .shortcut-input-container { 2 | display: flex; 3 | cursor: pointer; 4 | border: 1px solid rgba(204, 204, 204, 0.5); 5 | align-items: center; 6 | width: 160px; 7 | height: 24px; 8 | font-size: 12px; 9 | padding: 0 4px; 10 | border-radius: 3px; 11 | 12 | &.edit { 13 | border: 1px solid #4C9BFF; 14 | } 15 | 16 | input { 17 | border: none; 18 | outline: none; 19 | width: 100%; 20 | color: #4C9BFF; 21 | padding: 0; 22 | font-size: 12px; 23 | } 24 | 25 | input::placeholder { 26 | color: #4C9BFF; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/componets/CustomSoundIcon/index.jsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | 3 | const CustomSoundIcon = ({isStart, style = {}, barStyle = {}}) => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ); 13 | }; 14 | 15 | export default CustomSoundIcon; 16 | -------------------------------------------------------------------------------- /src/componets/CustomSoundIcon/index.less: -------------------------------------------------------------------------------- 1 | .sound-icon { 2 | display: flex; 3 | justify-content: space-around; 4 | align-items: center; 5 | width: 30px; 6 | height: 18px; 7 | } 8 | 9 | .bar { 10 | width: 2px; 11 | background-color: #fff; 12 | } 13 | 14 | .bar1 { 15 | height: 5px; 16 | } 17 | 18 | .bar2 { 19 | height: 10px; 20 | } 21 | 22 | .bar3 { 23 | height: 15px; 24 | } 25 | 26 | .bar4 { 27 | height: 10px; 28 | } 29 | 30 | .bar5 { 31 | height: 5px; 32 | } 33 | 34 | .start .bar1, 35 | .start .bar5 { 36 | animation: bar-animation 0.6s infinite; 37 | } 38 | 39 | .start .bar2, 40 | .start .bar4 { 41 | animation: bar-animation 0.6s infinite 0.25s; 42 | } 43 | 44 | .start .bar3 { 45 | animation: bar-animation 0.6s infinite 0.5s; 46 | } 47 | 48 | @keyframes bar-animation { 49 | 0%, 100% { 50 | transform: scaleY(1); 51 | } 52 | 50% { 53 | transform: scaleY(1.5); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/componets/CustomSwitch/index.jsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | import {useEffect, useState} from "react"; 3 | 4 | const CustomSwitch = ({id, isOn, handleToggle}) => { 5 | 6 | const [value, setValue] = useState(isOn) 7 | 8 | useEffect(() => { 9 | setValue(isOn) 10 | }, [isOn]) 11 | 12 | return ( 13 |
14 | 21 | 27 |
28 | ); 29 | }; 30 | 31 | export default CustomSwitch; 32 | -------------------------------------------------------------------------------- /src/componets/CustomSwitch/index.less: -------------------------------------------------------------------------------- 1 | .switch-container { 2 | display: flex; 3 | justify-items: center; 4 | align-items: center; 5 | 6 | .switch-checkbox { 7 | height: 0; 8 | width: 0; 9 | visibility: hidden; 10 | } 11 | 12 | .switch-label { 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-between; 16 | cursor: pointer; 17 | width: 40px; 18 | height: 20px; 19 | background: grey; 20 | border-radius: 40px; 21 | position: relative; 22 | transition: background-color 0.2s; 23 | } 24 | 25 | .switch-label .switch-button { 26 | content: ''; 27 | position: absolute; 28 | top: 2px; 29 | left: 2px; 30 | width: 16px; 31 | height: 16px; 32 | border-radius: 40px; 33 | transition: 0.2s; 34 | background: #fff; 35 | box-shadow: 0 0 2px 0 rgba(10, 10, 10, 0.29); 36 | } 37 | 38 | .switch-checkbox:checked + .switch-label { 39 | background: #4C9BFF; 40 | } 41 | 42 | .switch-checkbox:checked + .switch-label .switch-button { 43 | left: calc(100% - 2px); 44 | transform: translateX(-100%); 45 | } 46 | } -------------------------------------------------------------------------------- /src/componets/CustomTextarea/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | 3 | export default function CustomTextarea({children, height, placeholder, value, onChange}) { 4 | return ( 5 |
6 | 15 |
{children}
16 |
17 | ) 18 | } -------------------------------------------------------------------------------- /src/componets/CustomTextarea/index.less: -------------------------------------------------------------------------------- 1 | .custom-textarea { 2 | border-radius: 5px; 3 | width: 100%; 4 | background-color: #ffffff; 5 | border: #C9C9C9 1px solid; 6 | display: flex; 7 | flex-direction: column; 8 | 9 | &:hover { 10 | outline: #4C9BFF 1.5px solid; 11 | } 12 | 13 | textarea { 14 | margin: 2px 5px; 15 | width: calc(100% - 10px); 16 | background-color: #FFFFFF; 17 | height: 50px; 18 | resize: none; 19 | border: none; 20 | outline: none; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/componets/CustomToast/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, createContext, useContext} from 'react'; 2 | import "./index.less" 3 | 4 | const Toast = ({message, duration, onClose, error}) => { 5 | useEffect(() => { 6 | const timer = setTimeout(() => { 7 | onClose(); 8 | }, duration); 9 | 10 | return () => clearTimeout(timer); 11 | }, [duration, onClose]); 12 | 13 | return ( 14 |
15 | {message} 16 |
17 | ); 18 | }; 19 | 20 | 21 | const ToastContext = createContext(); 22 | 23 | export const useToast = () => useContext(ToastContext); 24 | 25 | const ToastProvider = ({children}) => { 26 | const [toasts, setToasts] = useState([]); 27 | 28 | const showToast = (message, error = false, duration = 2000,) => { 29 | const id = Math.random().toString(36).substr(2, 9); 30 | setToasts([...toasts, {id, message, duration, error}]); 31 | }; 32 | 33 | const removeToast = (id) => { 34 | setToasts(toasts.filter(toast => toast.id !== id)); 35 | }; 36 | 37 | return ( 38 | 39 | {children} 40 |
41 | {toasts.map(toast => ( 42 | removeToast(toast.id)} 48 | /> 49 | ))} 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default ToastProvider; 56 | -------------------------------------------------------------------------------- /src/componets/CustomToast/index.less: -------------------------------------------------------------------------------- 1 | .toast { 2 | position: fixed; 3 | top: 30px; 4 | padding: 6px 15px; 5 | background-color: #4C9BFF; 6 | color: white; 7 | border-radius: 5px; 8 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 9 | opacity: 0; 10 | animation: fadeInOut 3s; 11 | } 12 | 13 | .toast.error { 14 | background-color: #ff4c4c; 15 | } 16 | 17 | @keyframes fadeInOut { 18 | 0% { 19 | opacity: 0; 20 | transform: translateY(-30px); 21 | } 22 | 10% { 23 | opacity: 1; 24 | transform: translateY(0); 25 | } 26 | 90% { 27 | opacity: 1; 28 | transform: translateY(0); 29 | } 30 | 100% { 31 | opacity: 0; 32 | transform: translateY(-30px); 33 | } 34 | } 35 | 36 | .toast-container { 37 | position: fixed; 38 | top: 0; 39 | left: 0; 40 | width: 100%; 41 | height: 100%; 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | pointer-events: none; 46 | z-index: 1111; 47 | } 48 | -------------------------------------------------------------------------------- /src/componets/CustomTooltip/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useRef, useEffect} from 'react'; 2 | import './index.less'; 3 | 4 | const CustomTooltip = ({placement, title, children}) => { 5 | const [visible, setVisible] = useState(false); 6 | const [position, setPosition] = useState({}); 7 | const tooltipRef = useRef(null); 8 | const childRef = useRef(null); 9 | 10 | useEffect(() => { 11 | if (visible && childRef.current && tooltipRef.current) { 12 | const childRect = childRef.current.getBoundingClientRect(); 13 | const tooltipRect = tooltipRef.current.getBoundingClientRect(); 14 | const newPosition = calculatePosition(placement, childRect, tooltipRect); 15 | setPosition(newPosition); 16 | } 17 | }, [visible, placement]); 18 | 19 | const showTooltip = () => setVisible(true); 20 | const hideTooltip = () => setVisible(false); 21 | 22 | const calculatePosition = (placement, childRect, tooltipRect) => { 23 | switch (placement) { 24 | case 'top': 25 | return { 26 | top: childRect.top - tooltipRect.height - 4, 27 | left: childRect.left + (childRect.width - tooltipRect.width) / 2 28 | }; 29 | case 'bottom': 30 | return {top: childRect.bottom, left: childRect.left + (childRect.width - tooltipRect.width) / 2}; 31 | case 'topLeft': 32 | return {top: childRect.top - tooltipRect.height, left: childRect.left}; 33 | case 'topRight': 34 | return {top: childRect.top - tooltipRect.height, left: childRect.right - tooltipRect.width}; 35 | case 'bottomLeft': 36 | return {top: childRect.bottom, left: childRect.left}; 37 | case 'bottomRight': 38 | return {top: childRect.bottom, left: childRect.right - tooltipRect.width}; 39 | default: 40 | return {top: childRect.bottom, left: childRect.left}; 41 | } 42 | }; 43 | 44 | return ( 45 |
46 | {visible && title && ( 47 |
52 | {title} 53 |
54 | )} 55 |
56 | {children} 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default CustomTooltip; 63 | -------------------------------------------------------------------------------- /src/componets/CustomTooltip/index.less: -------------------------------------------------------------------------------- 1 | .tooltip-container { 2 | position: relative; 3 | display: inline-block; 4 | } 5 | 6 | .tooltip-box { 7 | margin-top: 2px; 8 | position: fixed; 9 | background-color: #555; 10 | color: #fff; 11 | text-align: center; 12 | padding: 0 6px; 13 | border-radius: 4px; 14 | z-index: 1000; 15 | white-space: nowrap; 16 | font-weight: 500; 17 | font-size: 12px; 18 | margin-bottom: 100px; 19 | } -------------------------------------------------------------------------------- /src/componets/CustomUserNameInput/index.less: -------------------------------------------------------------------------------- 1 | 2 | .custom-username-input { 3 | display: flex; 4 | flex-direction: column; 5 | z-index: 999; 6 | 7 | .custom-username-input-content { 8 | z-index: 999; 9 | height: 40px; 10 | max-height: 40px; 11 | min-height: 40px; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | background-color: #FFF; 16 | border-radius: 10px; 17 | width: 260px; 18 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 19 | 20 | .placeholder { 21 | flex: 1; 22 | width: 40px; 23 | height: 10px; 24 | } 25 | 26 | .operation { 27 | flex: 1; 28 | width: 40px; 29 | display: flex; 30 | justify-items: center; 31 | align-items: center; 32 | 33 | .operation-icon { 34 | width: 20px; 35 | font-size: 16px; 36 | color: #7F7F7F; 37 | } 38 | } 39 | 40 | 41 | input { 42 | font-size: 16px; 43 | padding: 0; 44 | margin: 0; 45 | border: none; 46 | outline: none; 47 | width: 100%; 48 | background-color: transparent; 49 | text-align: center; 50 | flex: 3; 51 | } 52 | 53 | input::placeholder { 54 | color: #7F7F7F 55 | } 56 | } 57 | 58 | .custom-user-input-down { 59 | display: flex; 60 | flex-direction: column; 61 | background-color: rgba(237, 242, 249, 0.8); 62 | backdrop-filter: blur(10px); 63 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5); 64 | border-radius: 5px; 65 | padding: 5px 3px; 66 | margin-top: 5px; 67 | max-height: 150px; 68 | overflow-y: scroll; 69 | 70 | .custom-user-input-down-item { 71 | padding: 6px 4px; 72 | user-select: none; 73 | cursor: pointer; 74 | border-radius: 4px; 75 | display: flex; 76 | justify-content: space-between; 77 | align-items: center; 78 | 79 | &:hover { 80 | background-color: rgb(220, 234, 255); 81 | } 82 | } 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /src/componets/DropdownButton/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useRef} from 'react'; 2 | import './index.less'; 3 | import CustomButton from "../CustomButton/index.jsx"; 4 | 5 | const DropdownButton = ({options, defaultOption, onOptionSelect}) => { 6 | const [selectedOption, setSelectedOption] = useState(defaultOption); 7 | const [dropdownVisible, setDropdownVisible] = useState(false); 8 | const dropdownRef = useRef(null); 9 | 10 | useEffect(() => { 11 | setSelectedOption(defaultOption); 12 | }, [defaultOption]); 13 | 14 | const handleOptionSelect = (option) => { 15 | setSelectedOption(option); 16 | setDropdownVisible(false); 17 | onOptionSelect(option); 18 | }; 19 | 20 | const handleBlur = (e) => { 21 | if (!dropdownRef.current.contains(e.relatedTarget)) { 22 | setDropdownVisible(false); 23 | } 24 | }; 25 | 26 | 27 | return ( 28 |
34 |
35 | handleOptionSelect(selectedOption)}> 39 | {selectedOption.label} 40 | 41 |
setDropdownVisible(!dropdownVisible)} 49 | > 50 | 54 |
55 |
56 | {dropdownVisible && ( 57 |
    58 | {options.map((option) => { 59 | return ( 60 | option.key !== selectedOption.key ?
  • handleOptionSelect(option)}> 63 | {option.label} 64 |
  • : <> 65 | ) 66 | } 67 | )} 68 |
69 | )} 70 |
71 | ); 72 | }; 73 | 74 | export default DropdownButton; 75 | -------------------------------------------------------------------------------- /src/componets/DropdownButton/index.less: -------------------------------------------------------------------------------- 1 | .dropdown-button { 2 | position: relative; 3 | display: inline-block; 4 | } 5 | .dropdown-menu { 6 | position: absolute; 7 | top: 100%; 8 | left: 0; 9 | background-color: rgba(237, 242, 249, 0.8); 10 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); 11 | backdrop-filter: blur(5px); 12 | z-index: 1; 13 | list-style-type: none; 14 | padding: 0; 15 | margin-top: 2px; 16 | width: 100%; 17 | } 18 | 19 | .dropdown-menu li { 20 | padding: 2px 10px; 21 | cursor: pointer; 22 | } 23 | 24 | .dropdown-menu li:hover { 25 | background-color: rgb(220, 234, 255); 26 | } 27 | -------------------------------------------------------------------------------- /src/componets/FriendSearchCard/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | const FriendSearchCard = ({info, onClick}) => { 3 | return ( 4 |
{ 8 | e.preventDefault() 9 | if (onContextMenu) onContextMenu(e) 10 | }} 11 | > 12 | {info.portrait}/ 14 |
15 |
16 |
23 | {info.remark ? info.remark + "(" + info.name + ")" : info.name} 24 |
25 |
26 |
27 |
31 | 账号:{info.account} 32 |
33 |
34 |
35 |
36 | ) 37 | } 38 | export default FriendSearchCard -------------------------------------------------------------------------------- /src/componets/FriendSearchCard/index.less: -------------------------------------------------------------------------------- 1 | .friend-search-card { 2 | border-radius: 10px; 3 | background-color: #FFFFFF; 4 | padding: 10px 10px; 5 | display: flex; 6 | align-items: center; 7 | 8 | .friend-search-card-portrait { 9 | width: 45px; 10 | height: 45px; 11 | border-radius: 45px; 12 | margin-right: 5px; 13 | } 14 | 15 | .friend-search-card-content { 16 | height: 45px; 17 | width: calc(100% - 50px); 18 | display: flex; 19 | flex-direction: column; 20 | 21 | .friend-search-card-content-item { 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | width: 100%; 26 | 27 | .ellipsis { 28 | white-space: nowrap; 29 | overflow: hidden; 30 | text-overflow: ellipsis; 31 | } 32 | } 33 | } 34 | } 35 | 36 | .friend-search-card:hover { 37 | background-color: #EDF2F9; 38 | } 39 | 40 | .friend-search-card.selected { 41 | background-color: #4C9BFF; 42 | } 43 | -------------------------------------------------------------------------------- /src/componets/IconButton/index.css: -------------------------------------------------------------------------------- 1 | .button-icon-container { 2 | width: 30px; 3 | height: 30px; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | border-radius: 2px; 8 | cursor: pointer; 9 | } 10 | 11 | .button-icon-container:hover { 12 | background-color: #4C9BFF; 13 | color: #FFFFFF; 14 | } 15 | 16 | .button-icon-container:hover.danger { 17 | background-color: #ff4c4c; 18 | color: #FFFFFF; 19 | } -------------------------------------------------------------------------------- /src/componets/IconButton/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.css" 3 | 4 | export default function IconButton({icon, onClick, danger = false}) { 5 | return ( 6 |
{ 9 | if (onClick) onClick(); 10 | }} 11 | > 12 | {icon} 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /src/componets/IconMinorButton/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import React, {useRef} from "react"; 3 | import CustomTooltip from "../CustomTooltip/index.jsx"; 4 | 5 | export default function IconMinorButton({icon, onClick, danger = false, title = ""}) { 6 | const ref = useRef(); 7 | return ( 8 | 9 |
{ 13 | if (onClick) onClick(e, ref) 14 | }} 15 | > 16 | {icon} 17 |
18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /src/componets/IconMinorButton/index.less: -------------------------------------------------------------------------------- 1 | .button-icon-minor { 2 | width: 30px; 3 | height: 30px; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | cursor: pointer; 8 | color: #646464; 9 | 10 | &:hover { 11 | background-color: #EDF2F9; 12 | border-radius: 5px; 13 | } 14 | 15 | &:hover.danger { 16 | background-color: #ff4c4c; 17 | color: #FFFFFF; 18 | } 19 | } -------------------------------------------------------------------------------- /src/componets/MsgContentShow/index.jsx: -------------------------------------------------------------------------------- 1 | import {formatTimingTime} from "../../utils/date.js"; 2 | 3 | export default function MsgContentShow({msgContent}) { 4 | if (!msgContent) return 5 | switch (msgContent.type) { 6 | case "text": { 7 | return <>{msgContent.content} 8 | } 9 | case "file": { 10 | let content = JSON.parse(msgContent.content) 11 | return <>[文件] {content.name} 12 | } 13 | case "img": { 14 | return <>[图片] 15 | } 16 | case "retraction": { 17 | return <>[消息被撤回] 18 | } 19 | case "voice": { 20 | let content = JSON.parse(msgContent.content) 21 | return
[语音] {content.time}"
22 | } 23 | case "call": { 24 | let content = JSON.parse(msgContent.content) 25 | return
[通话] {content?.time > 0 ? formatTimingTime(content?.time) : "未接通"}
26 | } 27 | case "system": { 28 | return <>[系统消息] 29 | } 30 | case "quit": { 31 | return <>[系统消息] 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/componets/ProgressBar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.less'; 3 | 4 | const ProgressBar = ({progress}) => { 5 | return ( 6 |
7 | {progress > 0 && 8 | <> 9 |
10 |
11 |
12 |
13 | 14 |
15 |
23 | {progress < 100 ? `${progress.toFixed(1)}/100` : "100%"} 24 |
25 | 26 | } 27 |
28 | 29 | ); 30 | }; 31 | 32 | export default ProgressBar; 33 | -------------------------------------------------------------------------------- /src/componets/ProgressBar/index.less: -------------------------------------------------------------------------------- 1 | .progress-bar-container { 2 | display: flex; 3 | justify-content: center; 4 | align-content: center; 5 | align-items: center; 6 | margin-top: 5px; 7 | height: 5px; 8 | } 9 | 10 | .progress-bar-bg { 11 | width: 100%; 12 | background-color: #e0e0e0; 13 | border-radius: 25px; 14 | overflow: hidden; 15 | height: 5px; 16 | } 17 | 18 | .progress-bar { 19 | height: 5px; 20 | background-color: #A0D9F6; 21 | width: 0; 22 | border-radius: 25px; 23 | position: relative; 24 | transition: width 0.5s ease-in-out; 25 | } 26 | 27 | .progress-bar-flash { 28 | content: ''; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | height: 100%; 33 | width: 5px; 34 | border-radius: 10px; 35 | background: rgba(255, 255, 255, 0.5); 36 | animation: flash 1s infinite linear; 37 | } 38 | 39 | @keyframes flash { 40 | 0% { 41 | left: 0; 42 | } 43 | 50% { 44 | left: calc(100% - 5px); 45 | } 46 | 100% { 47 | left: 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/componets/QRCodeGenerator/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {QRCodeSVG} from 'qrcode.react'; 3 | 4 | const CustomQRCode = ({value, size = 128, fgColor = "#000000", bgColor = "#ffffff"}) => { 5 | const [text, setText] = useState(value) 6 | useEffect(() => { 7 | setText(value) 8 | }, [value]); 9 | return ( 10 | 23 | ); 24 | }; 25 | 26 | export default CustomQRCode; 27 | -------------------------------------------------------------------------------- /src/componets/QuillRichTextEditor/index.css: -------------------------------------------------------------------------------- 1 | .editor-container { 2 | height: 100%; 3 | } 4 | 5 | .editor-container .ql-container { 6 | border: none; 7 | } 8 | 9 | .editor-container .ql-editor { 10 | padding: 0; 11 | border: none; 12 | } 13 | 14 | .editor-container .ql-editor:focus { 15 | outline: none; 16 | } -------------------------------------------------------------------------------- /src/componets/RichTextEditor/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef} from 'react'; 2 | import "./index.less" 3 | import {listen} from "@tauri-apps/api/event"; 4 | 5 | const RichTextEditor = React.forwardRef((props, ref) => { 6 | const editorRef = useRef(null); 7 | 8 | useEffect(() => { 9 | //监听后端发送的消息 10 | const unScreenshotListen = listen('screenshot_result', async (event) => { 11 | const img = document.createElement('img'); 12 | img.src = 'data:image/png;base64,' + event.payload; 13 | img.style.width = '100px' 14 | img.style.height = 'auto'; 15 | editorRef.current.appendChild(img); 16 | moveCursorToEnd(); 17 | }) 18 | return async () => { 19 | (await unScreenshotListen)(); 20 | } 21 | }, []) 22 | 23 | const handlePaste = (event) => { 24 | event.preventDefault(); 25 | const clipboardData = event.clipboardData; 26 | const text = clipboardData.getData('text'); 27 | const items = clipboardData.items; 28 | 29 | let hasImage = false; 30 | 31 | for (let i = 0; i < items.length; i++) { 32 | if (items[i].type.indexOf('image') !== -1) { 33 | hasImage = true; 34 | const blob = items[i].getAsFile(); 35 | const reader = new FileReader(); 36 | reader.onload = function (event) { 37 | const img = document.createElement('img'); 38 | img.src = event.target.result; 39 | img.style.width = '100px' 40 | img.style.height = 'auto' 41 | editorRef.current.appendChild(img); 42 | moveCursorToEnd(); 43 | }; 44 | reader.readAsDataURL(blob); 45 | } 46 | } 47 | 48 | if (!hasImage && text) { 49 | document.execCommand('insertText', false, text); 50 | } 51 | }; 52 | 53 | const moveCursorToEnd = () => { 54 | const range = document.createRange(); 55 | const selection = window.getSelection(); 56 | // 将光标移到内容的最后 57 | range.selectNodeContents(editorRef.current); 58 | range.collapse(false); 59 | selection.removeAllRanges(); 60 | selection.addRange(range); 61 | }; 62 | 63 | const handleKeyDown = (event) => { 64 | if (props.onKeyDown) { 65 | props.onKeyDown(event); 66 | } 67 | }; 68 | 69 | React.useImperativeHandle(ref, () => { 70 | editorRef.current.focus = () => { 71 | moveCursorToEnd(); 72 | } 73 | return editorRef.current 74 | }); 75 | 76 | return ( 77 |
84 | ); 85 | }); 86 | 87 | export default RichTextEditor; 88 | -------------------------------------------------------------------------------- /src/componets/RichTextEditor/index.less: -------------------------------------------------------------------------------- 1 | .rich-text-editor { 2 | font-size: 14px; 3 | width: 100%; 4 | height: 100%; 5 | overflow-y: scroll; 6 | background-color: #F9FBFF; 7 | border: none; 8 | resize: none; 9 | outline: none; 10 | line-height: 18px; 11 | } -------------------------------------------------------------------------------- /src/componets/RightClickContent/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {useEffect, useRef, useState} from "react"; 3 | import {listen} from "@tauri-apps/api/event"; 4 | 5 | export default function RightClickContent({position, visible, children}) { 6 | const [contentVisible, setContentVisible] = useState(); 7 | const [menuPosition, setMenuPosition] = useState({x: 0, y: 0}); 8 | const menuRef = useRef(null); 9 | 10 | useEffect(() => { 11 | if (!position) return 12 | setContentVisible(true); 13 | }, [position]) 14 | 15 | useEffect(() => { 16 | setContentVisible(visible?.value); 17 | }, [visible]) 18 | 19 | useEffect(() => { 20 | const unListen = listen('drag-click', (event) => { 21 | setContentVisible(false) 22 | }); 23 | return async () => { 24 | (await unListen)() 25 | } 26 | }, []) 27 | 28 | useEffect(() => { 29 | if (contentVisible) { 30 | if (position.y + menuRef.current.clientHeight > window.innerHeight - 20) { 31 | position.y = position.y - menuRef.current.clientHeight 32 | } 33 | if (position.x + menuRef.current.clientWidth > window.innerWidth - 20) { 34 | position.x = position.x - menuRef.current.clientWidth 35 | } 36 | setMenuPosition(position); 37 | menuRef.current.focus() 38 | } 39 | }, [contentVisible]) 40 | 41 | return ( 42 |
43 | {contentVisible && ( 44 |
45 |
setContentVisible(false)} 54 | > 55 | {children} 56 |
57 |
58 | )} 59 |
60 | ) 61 | } -------------------------------------------------------------------------------- /src/componets/RightClickContent/index.less: -------------------------------------------------------------------------------- 1 | .right-click-content { 2 | .overlay { 3 | z-index: 999; 4 | } 5 | 6 | .content { 7 | z-index: 100; 8 | position: absolute; 9 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5); 10 | border-radius: 5px; 11 | outline: none; 12 | } 13 | } -------------------------------------------------------------------------------- /src/componets/RightClickMenu/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {useEffect, useRef, useState} from "react"; 3 | import {listen} from "@tauri-apps/api/event"; 4 | import RightClickContent from "../RightClickContent/index.jsx"; 5 | 6 | export default function RightClickMenu({position, options, visible = false, onMenuItemClick, filter = [], width}) { 7 | const [menuVisible, setMenuVisible] = useState(); 8 | const [menuPosition, setMenuPosition] = useState({x: 0, y: 0}); 9 | const menuRef = useRef(null); 10 | const [menuOptions, setMenuOption] = useState(options) 11 | 12 | useEffect(() => { 13 | if (!filter || filter.length <= 0) return 14 | let ops = options.filter((option) => { 15 | return !filter.includes(option.key) 16 | }) 17 | setMenuOption(ops) 18 | }, [filter]) 19 | 20 | const onItemClick = (action) => { 21 | setMenuVisible({value: false}) 22 | if (onMenuItemClick) onMenuItemClick(action) 23 | } 24 | 25 | return ( 26 | <> 27 | 28 |
29 | {menuOptions.map((item, index) => { 30 | if (filter.includes(index.key)) { 31 | return <> 32 | } 33 | return ( 34 |
{ 35 | if (e.button === 0) { 36 | onItemClick(item) 37 | } 38 | }}> 39 | {item.label} 40 |
41 | ) 42 | })} 43 |
44 |
45 | 46 | ) 47 | } -------------------------------------------------------------------------------- /src/componets/RightClickMenu/index.less: -------------------------------------------------------------------------------- 1 | 2 | .options { 3 | background-color: rgba(237, 242, 249, 0.7); 4 | backdrop-filter: blur(10px); 5 | border-radius: 5px; 6 | 7 | .option { 8 | padding: 2px 10px; 9 | border-radius: 5px; 10 | cursor: pointer; 11 | font-size: 14px; 12 | height: 28px; 13 | display: flex; 14 | align-items: center; 15 | 16 | &:hover { 17 | background-color: rgb(220, 234, 255); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/componets/VoiceRecorder/index.less: -------------------------------------------------------------------------------- 1 | .voice-recorder-container { 2 | .voice-recorder { 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | height: 300px; 8 | width: 450px; 9 | background-color: #FFFFFF; 10 | border-radius: 10px; 11 | } 12 | } 13 | 14 | .voice-operate { 15 | margin-top: 20px; 16 | font-size: 14px; 17 | display: flex; 18 | user-select: none; 19 | 20 | .operate-item { 21 | width: 40px; 22 | height: 40px; 23 | border-radius: 10px; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | margin: 0 5px; 28 | cursor: pointer; 29 | 30 | &:hover { 31 | background-color: rgba(189, 189, 189, 0.3); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/componets/WindowOperation/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import IconButton from "../IconButton/index.jsx"; 3 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow"; 4 | import {useEffect, useState} from "react"; 5 | 6 | export default function WindowOperation({ 7 | hide = true, 8 | onClose, 9 | onMinimize, 10 | onHide, 11 | height, 12 | isMaximize = true, 13 | isMinimize = true, 14 | style = {} 15 | }) { 16 | 17 | const [isMax, setIsMax] = useState(false) 18 | 19 | useEffect(() => { 20 | let window = WebviewWindow.getCurrent(); 21 | let UnlistenFn = window.listen("tauri://resize", async function () { 22 | setIsMax(await window.isMaximized()) 23 | }); 24 | return async () => { 25 | (await UnlistenFn)(); 26 | } 27 | }, []) 28 | 29 | const handleMinimize = () => { 30 | if (onMinimize) { 31 | onMinimize() 32 | } 33 | WebviewWindow.getCurrent().minimize() 34 | } 35 | 36 | const handleClose = () => { 37 | if (onClose) { 38 | onClose() 39 | } else { 40 | WebviewWindow.getCurrent().close() 41 | } 42 | } 43 | 44 | const handleHide = () => { 45 | if (onHide) { 46 | onMinimize() 47 | } 48 | WebviewWindow.getCurrent().hide() 49 | } 50 | 51 | const handleMaximize = () => { 52 | if (onHide) { 53 | onMinimize() 54 | } 55 | WebviewWindow.getCurrent().maximize() 56 | } 57 | 58 | const handleUnMaximize = () => { 59 | if (onHide) { 60 | onMinimize() 61 | } 62 | WebviewWindow.getCurrent().unmaximize() 63 | } 64 | 65 | return ( 66 |
67 | { 68 | isMinimize && } 70 | onClick={handleMinimize} 71 | /> 72 | } 73 | { 74 | isMaximize && } 77 | onClick={isMax ? handleUnMaximize : handleMaximize} 78 | /> 79 | } 80 | } 83 | onClick={hide ? handleHide : handleClose} 84 | /> 85 |
86 | ) 87 | } -------------------------------------------------------------------------------- /src/componets/WindowOperation/index.less: -------------------------------------------------------------------------------- 1 | .window-operation { 2 | height: 60px; 3 | position: absolute; 4 | right: 10px; 5 | display: flex; 6 | align-items: center; 7 | z-index: 1111; 8 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./styles.css"; 5 | import {BrowserRouter as Router} from "react-router-dom"; 6 | import "./assets/iconfont.css" 7 | import {Provider} from "react-redux"; 8 | import Store from "./store/index.jsx"; 9 | import ToastProvider from "./componets/CustomToast/index.jsx"; 10 | 11 | ReactDOM.createRoot(document.getElementById("root")).render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /src/pages/AboutWindow/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import WindowOperation from "../../componets/WindowOperation/index.jsx"; 3 | import CustomBox from "../../componets/CustomBox/index.jsx"; 4 | 5 | export default function AboutWindow() { 6 | return ( 7 | 8 |
9 | 15 |
16 |
17 |
18 |
19 |
20 | 22 | 25 |
26 | 开源地址:https://github.com/DWHengr/linyu-client 27 |
28 |
29 | 作者:Heath 30 |
31 |
32 | QQ群:729158695 33 |
34 |
42 | 基于 框架 43 |
44 |
45 | 46 |
47 | ) 48 | } -------------------------------------------------------------------------------- /src/pages/AboutWindow/index.less: -------------------------------------------------------------------------------- 1 | .about { 2 | overflow: hidden; 3 | display: flex; 4 | flex-direction: column; 5 | background: linear-gradient(-45deg, #80d2ff, #F9FBFF, #7fb5ff); 6 | animation: gradientAnimation 15s ease infinite; 7 | position: relative; 8 | justify-content: center; 9 | align-items: center; 10 | background-size: 400% 400%; 11 | font-size: 13px; 12 | 13 | .about-wave { 14 | position: absolute; 15 | width: 200%; 16 | height: 100%; 17 | top: 0; 18 | left: 50%; 19 | margin-left: -100%; 20 | background: rgba(255, 255, 255, 0.2); 21 | opacity: 0.5; 22 | border-radius: 43%; 23 | animation: waveAnimation 6s linear infinite; 24 | } 25 | 26 | .about-wave:nth-child(2) { 27 | animation: waveAnimation 7s linear infinite; 28 | opacity: 0.3; 29 | } 30 | 31 | .about-wave:nth-child(3) { 32 | animation: waveAnimation 8s linear infinite; 33 | opacity: 0.2; 34 | } 35 | 36 | @keyframes gradientAnimation { 37 | 0% { 38 | background-position: 0 50%; 39 | } 40 | 50% { 41 | background-position: 100% 50%; 42 | } 43 | 100% { 44 | background-position: 0 50%; 45 | } 46 | } 47 | 48 | @keyframes waveAnimation { 49 | 0% { 50 | transform: translateX(-50%) translateY(0); 51 | } 52 | 50% { 53 | transform: translateX(-50%) translateY(-100px); 54 | } 55 | 100% { 56 | transform: translateX(-50%) translateY(0); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/pages/AboutWindow/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | 3 | export default async function CreateAboutWindow() { 4 | const window = await WebviewWindow.getByLabel('about') 5 | if (window) { 6 | window.show() 7 | window.unminimize() 8 | window.setFocus() 9 | return 10 | } 11 | let webview = new WebviewWindow("about", { 12 | url: "/about", 13 | title: "关于linyu", 14 | width: 360, 15 | height: 510, 16 | decorations: false, 17 | center: true, 18 | transparent: true, 19 | resizable: false, 20 | shadow: false, 21 | focus: true 22 | }); 23 | } -------------------------------------------------------------------------------- /src/pages/ChatGroupNotice/index.less: -------------------------------------------------------------------------------- 1 | .chat-group-notice { 2 | height: 100%; 3 | overflow: hidden; 4 | display: flex; 5 | flex-direction: column; 6 | background-image: linear-gradient(to bottom, #EDF2F9, #fff); 7 | position: relative; 8 | padding: 10px 0 10px 10px; 9 | 10 | .notices { 11 | display: flex; 12 | flex-direction: column; 13 | overflow: hidden; 14 | 15 | .notices-title { 16 | display: flex; 17 | justify-content: space-between; 18 | margin-top: 30px; 19 | align-items: center; 20 | margin-right: 10px; 21 | margin-bottom: 5px; 22 | } 23 | 24 | .notices-content { 25 | flex: 1; 26 | display: flex; 27 | flex-direction: column; 28 | overflow-y: scroll; 29 | padding-right: 5px; 30 | margin-right: 5px; 31 | padding-bottom: 10px; 32 | 33 | .notice-item { 34 | margin: 5px 0; 35 | padding: 10px; 36 | background-color: #FFFFFF; 37 | border-radius: 5px; 38 | font-size: 14px; 39 | border: #FFFFFF 2px solid; 40 | 41 | .item-title { 42 | color: #969696; 43 | display: flex; 44 | align-items: center; 45 | margin-bottom: 5px; 46 | 47 | .item-portrait { 48 | width: 20px; 49 | height: 20px; 50 | border-radius: 20px; 51 | margin-right: 5px; 52 | user-select: none; 53 | } 54 | 55 | .item-operation { 56 | margin-left: 5px; 57 | cursor: pointer; 58 | display: none; 59 | 60 | &:hover { 61 | color: #1F1F1F; 62 | } 63 | } 64 | } 65 | 66 | &:hover { 67 | box-sizing: border-box; 68 | background-color: #EDF2F9; 69 | 70 | .item-title { 71 | .item-operation { 72 | display: flex; 73 | } 74 | } 75 | } 76 | } 77 | 78 | } 79 | } 80 | 81 | .edit { 82 | display: flex; 83 | flex-direction: column; 84 | margin-right: 10px; 85 | height: calc(100% - 20px); 86 | 87 | textarea { 88 | background-color: transparent; 89 | outline: none; 90 | border: none; 91 | resize: none; 92 | width: 100%; 93 | font-size: 14px; 94 | } 95 | 96 | .edit-title { 97 | display: flex; 98 | justify-content: center; 99 | align-items: center; 100 | font-size: 12px; 101 | user-select: none; 102 | margin-bottom: 5px; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/pages/ChatGroupNotice/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | import {setItem} from "../../utils/storage.js"; 3 | 4 | export default async function CreateChatGroupNotice(groupId) { 5 | await setItem("notice", {groupId}) 6 | const window = await WebviewWindow.getByLabel('notice') 7 | if (window) { 8 | window.show() 9 | window.unminimize() 10 | window.setFocus() 11 | return 12 | } 13 | let webview = new WebviewWindow("notice", { 14 | url: "/notice", 15 | title: "群公告", 16 | width: 480, 17 | height: 530, 18 | decorations: false, 19 | center: true, 20 | transparent: true, 21 | resizable: false, 22 | shadow: false, 23 | focus: true 24 | }); 25 | } -------------------------------------------------------------------------------- /src/pages/ChatWindow/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import WindowOperation from "../../componets/WindowOperation/index.jsx"; 3 | import {useEffect, useState} from "react"; 4 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow"; 5 | import ChatListApi from "../../api/chatList.js"; 6 | import CommonChatFrame from "../../componets/CommonChatFrame/index.jsx"; 7 | import {useDispatch} from "react-redux"; 8 | import {addChatWindowUser} from "../../store/chat/action.js"; 9 | import CustomBox from "../../componets/CustomBox/index.jsx"; 10 | import {getItem} from "../../utils/storage.js"; 11 | 12 | export default function ChatWindow() { 13 | 14 | const [userInfo, setUserInfo] = useState(null); 15 | let dispatch = useDispatch(); 16 | 17 | useEffect(() => { 18 | let label = WebviewWindow.getCurrent().label; 19 | let fromId = label.split('--')[1]; 20 | getItem("chat-windows-" + fromId).then(res => { 21 | ChatListApi.detail({targetId: fromId, type: res.type}).then(res => { 22 | if (res.code === 0) { 23 | setUserInfo(res.data) 24 | dispatch(addChatWindowUser(res)) 25 | } 26 | }) 27 | }) 28 | }, []) 29 | 30 | return ( 31 | 32 | {userInfo && } 33 | 34 | 35 | ) 36 | } -------------------------------------------------------------------------------- /src/pages/ChatWindow/index.less: -------------------------------------------------------------------------------- 1 | .chat-window { 2 | border: 1px solid #E1E0E0; 3 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 4 | height: calc(100vh - 10px); 5 | border-radius: 10px; 6 | background-color: #F9FBFF; 7 | display: flex; 8 | flex-direction: row; 9 | overflow: hidden; 10 | position: relative; 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/ChatWindow/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | import {emit} from "@tauri-apps/api/event"; 3 | import {setItem} from "../../utils/storage.js"; 4 | 5 | export default async function CreateChatWindow(userId, username, type) { 6 | await setItem("chat-windows-" + userId, {userId, username, type}) 7 | const window = await WebviewWindow.getByLabel('chat--' + userId) 8 | if (window) { 9 | window.show() 10 | window.unminimize() 11 | window.setFocus() 12 | return 13 | } 14 | let webview = new WebviewWindow("chat--" + userId, { 15 | url: "/chat", 16 | title: username, 17 | center: true, 18 | width: 760, 19 | minWidth: 600, 20 | height: 800, 21 | minHeight: 600, 22 | decorations: false, 23 | transparent: true, 24 | shadow: false 25 | }); 26 | webview.listen("tauri://destroyed", function (event) { 27 | let fromId = webview.label.split('--')[1] 28 | emit("chat-destroyed", {fromId: fromId}) 29 | }); 30 | } -------------------------------------------------------------------------------- /src/pages/Command/index.less: -------------------------------------------------------------------------------- 1 | .command-window-container { 2 | padding: 5px; 3 | position: relative; 4 | 5 | .command-window { 6 | border: 2px solid #C4C4C4; 7 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); 8 | height: calc(100vh - 10px); 9 | border-radius: 10px; 10 | background-color: #EDF2F9; 11 | display: flex; 12 | flex-direction: column; 13 | overflow: hidden; 14 | position: relative; 15 | box-sizing: border-box; 16 | 17 | .command-box { 18 | height: 60px; 19 | background-color: #F9FBFF; 20 | padding: 0 10px; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | box-sizing: border-box; 25 | flex-shrink: 0; 26 | 27 | .command-one { 28 | width: 80px; 29 | height: 34px; 30 | background-color: rgba(66, 66, 66, 0.6); 31 | border-radius: 5px; 32 | margin-right: 5px; 33 | color: #FFFFFF; 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | font-weight: 600; 38 | } 39 | 40 | .command-two { 41 | width: 100px; 42 | height: 34px; 43 | background-color: rgba(31, 31, 31, 0.8); 44 | border-radius: 5px; 45 | color: #FFFFFF; 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | font-weight: 600; 50 | margin-right: 5px; 51 | } 52 | 53 | .command-input { 54 | flex: 1; 55 | 56 | input { 57 | outline: none; 58 | border: none; 59 | font-size: 18px; 60 | width: 100%; 61 | background-color: transparent; 62 | display: flex; 63 | } 64 | } 65 | } 66 | 67 | .content { 68 | height: 100%; 69 | overflow: hidden; 70 | 71 | .cmd-item { 72 | height: 40px; 73 | margin: 5px 10px; 74 | background-color: #fff; 75 | display: flex; 76 | padding: 10px; 77 | align-items: center; 78 | border-radius: 5px; 79 | user-select: none; 80 | cursor: pointer; 81 | 82 | &:hover { 83 | outline: 2px solid #4C9BFF; 84 | } 85 | 86 | &:focus-within { 87 | outline: 2px solid #4C9BFF; 88 | } 89 | } 90 | 91 | .chat-content { 92 | height: 100%; 93 | overflow-y: scroll; 94 | 95 | .chat-content-show-frame { 96 | padding: 10px 15px; 97 | display: flex; 98 | flex-direction: column; 99 | position: relative; 100 | } 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/pages/Command/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | import {PhysicalPosition} from "@tauri-apps/api/window"; 3 | 4 | export default async function CreateCmdWindow() { 5 | let webview = await WebviewWindow.getByLabel('command') 6 | if (webview) { 7 | await webview.show() 8 | await webview.setFocus() 9 | await webview.unminimize() 10 | return 11 | } 12 | webview = new WebviewWindow("command", { 13 | url: "/command", 14 | title: "命名行模式", 15 | width: 800, 16 | height: 500, 17 | decorations: false, 18 | transparent: true, 19 | shadow: false, 20 | resizable: false, 21 | alwaysOnTop: true, 22 | skipTaskbar: true, 23 | x: 0, 24 | y: window.screen.height + 100 25 | }); 26 | await webview.listen("tauri://window-created", async function () { 27 | await webview.hide(); 28 | await webview.center() 29 | }); 30 | } -------------------------------------------------------------------------------- /src/pages/ForgetPassword/index.less: -------------------------------------------------------------------------------- 1 | .forget{ 2 | height: 100%; 3 | overflow: hidden; 4 | display: flex; 5 | flex-direction: column; 6 | background: linear-gradient(-45deg, #80d2ff, #d7e3fd, #7fb5ff); 7 | animation: gradientAnimation 15s ease infinite; 8 | position: relative; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/ForgetPassword/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | 3 | export default async function CreateForgetWindow() { 4 | const window = await WebviewWindow.getByLabel('forget') 5 | if (window) { 6 | window.show() 7 | window.unminimize() 8 | window.setFocus() 9 | return 10 | } 11 | let webview = new WebviewWindow("forget", { 12 | url: "/forget", 13 | title: "找回密码", 14 | width: 640, 15 | height: 500, 16 | decorations: false, 17 | center: true, 18 | transparent: true, 19 | resizable: false, 20 | shadow: false, 21 | focus: true 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/Home/Notify/FriendNotify/index.less: -------------------------------------------------------------------------------- 1 | .friend-notify { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | 6 | .friend-notify-title { 7 | height: 60px; 8 | display: flex; 9 | align-items: center; 10 | padding: 0 20px; 11 | font-weight: 600; 12 | } 13 | 14 | .friend-notify-content { 15 | flex: 1; 16 | background-color: #EDF2F9; 17 | padding: 15px; 18 | overflow-y: scroll; 19 | 20 | .friend-notify-item { 21 | background-color: #fff; 22 | height: 70px; 23 | border-radius: 10px; 24 | margin: 20px; 25 | display: flex; 26 | align-items: center; 27 | padding: 0 20px; 28 | justify-content: space-between; 29 | font-size: 12px; 30 | 31 | .friend-notify-item-left { 32 | display: flex; 33 | 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/pages/Home/Notify/SystemNotify/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import CustomDragDiv from "../../../../componets/CustomDragDiv/index.jsx"; 3 | import {formatTime} from "../../../../utils/date.js"; 4 | import {useEffect, useRef, useState} from "react"; 5 | import {invoke} from "@tauri-apps/api/core"; 6 | import NotifyApi from "../../../../api/notify.js"; 7 | import Time from "../../../../componets/CommonChatFrame/ChatContent/Time/index.jsx"; 8 | 9 | export default function SystemNotify() { 10 | 11 | const currentUserId = useRef(null) 12 | const [notices, setNotices] = useState([]) 13 | 14 | useEffect(() => { 15 | invoke("get_user_info", {}).then(res => { 16 | currentUserId.current = res.user_id 17 | onGetSystemNotifyList() 18 | }) 19 | }, []) 20 | 21 | let onGetSystemNotifyList = () => { 22 | NotifyApi.systemList().then(res => { 23 | if (res.code === 0) { 24 | setNotices(res.data) 25 | } 26 | }) 27 | } 28 | return ( 29 |
30 | 31 |
系统通知
32 |
33 |
34 | { 35 | notices?.map(notify => { 36 | return ( 37 |
38 |
48 | ) 49 | }) 50 | } 51 | { 52 | notices.length <= 0 && 53 | 56 | 57 | 58 | } 59 |
60 |
61 | ) 62 | } -------------------------------------------------------------------------------- /src/pages/Home/Notify/SystemNotify/index.less: -------------------------------------------------------------------------------- 1 | .system-notify { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | 6 | .system-notify-title { 7 | height: 60px; 8 | display: flex; 9 | align-items: center; 10 | padding: 0 20px; 11 | font-weight: 600; 12 | } 13 | 14 | .system-notify-content { 15 | flex: 1; 16 | background-color: #EDF2F9; 17 | overflow-y: scroll; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | 22 | .system-notify-item { 23 | width: 420px; 24 | height: 280px; 25 | border-radius: 10px; 26 | background-color: #fff; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | padding: 10px; 31 | margin-bottom: 20px; 32 | 33 | .system-notify-item-img { 34 | width: 100%; 35 | height: 240px; 36 | position: relative; 37 | } 38 | 39 | .system-notify-item-title { 40 | background-color: rgba(255, 255, 255, 0.5); 41 | width: 100%; 42 | position: absolute; 43 | bottom: 0; 44 | } 45 | 46 | .system-notify-item-text { 47 | flex: 1px; 48 | width: 100%; 49 | margin-top: 10px; 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/pages/Home/Notify/index.less: -------------------------------------------------------------------------------- 1 | .notify { 2 | background-color: #F9FBFF; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: row; 6 | 7 | .notify-list { 8 | padding: 15px; 9 | width: 280px; 10 | user-select: none; 11 | border-right: 1px solid #E1E0E0; 12 | display: flex; 13 | flex-direction: column; 14 | 15 | .notify-list-top { 16 | height: 45px; 17 | 18 | .notify-list-top-title { 19 | font-size: 26px; 20 | font-weight: bold; 21 | } 22 | } 23 | 24 | .notify-list-items { 25 | flex: 1; 26 | overflow-y: scroll; 27 | padding: 0 5px 0; 28 | 29 | .notify-list-item { 30 | height: 36px; 31 | display: flex; 32 | align-items: center; 33 | margin-bottom: 2px; 34 | padding: 0 15px; 35 | border-radius: 5px; 36 | font-weight: 600; 37 | font-size: 14px; 38 | user-select: none; 39 | cursor: pointer; 40 | background-color: #fff; 41 | 42 | &:hover { 43 | background-color: #EDF2F9; 44 | } 45 | 46 | &.selected { 47 | color: #fff; 48 | background-color: #4C9BFF; 49 | } 50 | } 51 | } 52 | } 53 | 54 | .notify-content { 55 | flex: 1; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/pages/Home/Set/General/index.less: -------------------------------------------------------------------------------- 1 | .general { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | 6 | .general-title { 7 | height: 60px; 8 | display: flex; 9 | align-items: center; 10 | padding: 0 20px; 11 | font-weight: 600; 12 | } 13 | 14 | .general-content { 15 | flex: 1; 16 | background-color: #EDF2F9; 17 | padding: 20px; 18 | overflow-y: scroll; 19 | } 20 | } -------------------------------------------------------------------------------- /src/pages/Home/Set/MessageNotify/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import CustomDragDiv from "../../../../componets/CustomDragDiv/index.jsx"; 3 | import CustomLine from "../../../../componets/CustomLine/index.jsx"; 4 | import CustomSwitch from "../../../../componets/CustomSwitch/index.jsx"; 5 | import {useEffect, useState} from "react"; 6 | import {getItem, setItem} from "../../../../utils/storage.js"; 7 | import UserSetApi from "../../../../api/userSet.js"; 8 | 9 | export default function MessageNotify() { 10 | const [userSets, SetUserSets] = useState({}) 11 | 12 | useEffect(() => { 13 | getItem("user-sets").then(value => { 14 | SetUserSets(value) 15 | }) 16 | }, []) 17 | 18 | const handleOnChange = (key, value) => { 19 | SetUserSets(pre => { 20 | let newPre = {...pre, [key]: value} 21 | UserSetApi.update({key, value}) 22 | setItem("user-sets", newPre) 23 | return newPre; 24 | }) 25 | } 26 | 27 | return ( 28 |
29 | 30 |
消息通知
31 |
32 |
33 |
34 |
通知提醒
35 |
36 |
37 |
好友消息
38 | handleOnChange("friendMsgNotify", !userSets.friendMsgNotify)}/> 41 |
42 |
43 |
44 |
45 |
提示音
46 |
47 |
48 |
消息提示音
49 | handleOnChange("msgTone", !userSets.msgTone)}/> 52 |
53 | 54 |
55 |
音视频提示音
56 | handleOnChange("audioVideoTone", !userSets.audioVideoTone)}/> 59 |
60 |
61 |
62 |
63 |
64 | ) 65 | } -------------------------------------------------------------------------------- /src/pages/Home/Set/MessageNotify/index.less: -------------------------------------------------------------------------------- 1 | .message-notify-set { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | 6 | .message-notify-set-title { 7 | height: 60px; 8 | display: flex; 9 | align-items: center; 10 | padding: 0 20px; 11 | font-weight: 600; 12 | } 13 | 14 | .message-notify-set-content { 15 | flex: 1; 16 | background-color: #EDF2F9; 17 | padding: 20px; 18 | overflow-y: scroll; 19 | } 20 | } -------------------------------------------------------------------------------- /src/pages/Home/Set/Shortcut/index.less: -------------------------------------------------------------------------------- 1 | .shortcut-set { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | 6 | .shortcut-set-title { 7 | height: 60px; 8 | display: flex; 9 | align-items: center; 10 | padding: 0 20px; 11 | font-weight: 600; 12 | } 13 | 14 | .shortcut-set-content { 15 | flex: 1; 16 | background-color: #EDF2F9; 17 | padding: 20px; 18 | overflow-y: scroll; 19 | } 20 | } -------------------------------------------------------------------------------- /src/pages/Home/Set/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import CustomDragDiv from "../../../componets/CustomDragDiv/index.jsx"; 3 | import {useState} from "react"; 4 | import {Redirect, Route, Switch, useHistory} from "react-router-dom"; 5 | import General from "./General/index.jsx"; 6 | import Shortcut from "./Shortcut/index.jsx"; 7 | import MessageNotify from "./MessageNotify/index.jsx"; 8 | 9 | export default function Set() { 10 | 11 | const [selectedSetIndex, setSelectedSetIndex] = useState(0) 12 | const h = useHistory(); 13 | 14 | const sets = [ 15 | {label: "通用", page: "/home/set/general", icon: "icon-tongyongshezhi"}, 16 | {label: "快捷键", page: "/home/set/shortcut", icon: "icon-kuaijiejian"}, 17 | {label: "消息通知", page: "/home/set/message-notify", icon: "icon-tongzhi"} 18 | ] 19 | 20 | return ( 21 |
22 |
23 | 24 | 25 | 26 |
27 | { 28 | sets?.map((set, index) => { 29 | let isSelected = index === selectedSetIndex 30 | return ( 31 |
{ 34 | setSelectedSetIndex(index) 35 | h.push(set.page) 36 | }} 37 | > 38 |
45 | 46 |
47 |
48 | {set.label} 49 |
50 |
51 | ) 52 | }) 53 | } 54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 | ) 66 | } -------------------------------------------------------------------------------- /src/pages/Home/Set/index.less: -------------------------------------------------------------------------------- 1 | .set { 2 | background-color: #F9FBFF; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: row; 6 | 7 | .set-list { 8 | padding: 15px; 9 | width: 280px; 10 | user-select: none; 11 | border-right: 1px solid #E1E0E0; 12 | display: flex; 13 | flex-direction: column; 14 | 15 | .set-list-top { 16 | height: 45px; 17 | 18 | .set-list-top-title { 19 | font-size: 26px; 20 | font-weight: bold; 21 | } 22 | } 23 | 24 | .set-list-items { 25 | flex: 1; 26 | overflow-y: scroll; 27 | padding: 0 5px 0; 28 | 29 | .set-list-item { 30 | height: 36px; 31 | display: flex; 32 | align-items: center; 33 | margin-bottom: 2px; 34 | padding: 0 15px; 35 | border-radius: 5px; 36 | font-weight: 600; 37 | font-size: 14px; 38 | user-select: none; 39 | cursor: pointer; 40 | background-color: #fff; 41 | 42 | &:hover { 43 | background-color: #EDF2F9; 44 | } 45 | 46 | &.selected { 47 | color: #fff; 48 | background-color: #4C9BFF; 49 | } 50 | } 51 | } 52 | } 53 | 54 | .set-content { 55 | flex: 1; 56 | } 57 | } 58 | 59 | .set-item { 60 | font-size: 14px; 61 | user-select: none; 62 | margin-bottom: 30px; 63 | 64 | .set-item-label { 65 | font-weight: 600; 66 | margin-bottom: 5px; 67 | } 68 | 69 | .set-item-options { 70 | background-color: #fff; 71 | padding: 10px 20px; 72 | border-radius: 10px; 73 | 74 | .set-item-option { 75 | padding: 5px 0; 76 | display: flex; 77 | justify-content: space-between; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/pages/Home/Talk/AllTalk/index.less: -------------------------------------------------------------------------------- 1 | .all-talk-container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | background-color: #EDF2F9; 6 | position: relative; 7 | align-items: center; 8 | justify-content: center; 9 | } -------------------------------------------------------------------------------- /src/pages/Home/Talk/DetailTalk/index.less: -------------------------------------------------------------------------------- 1 | .details-talk-container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | background-color: #EDF2F9; 6 | position: relative; 7 | align-items: center; 8 | justify-content: center; 9 | } 10 | 11 | .like-content-item { 12 | border: rgba(76, 155, 255, 0.5) .1px solid; 13 | width: 80px; 14 | height: 20px; 15 | background-color: #E3ECFF; 16 | border-radius: 10px; 17 | margin-right: 5px; 18 | padding: 1px 5px; 19 | display: flex; 20 | align-items: center; 21 | overflow: hidden; 22 | } -------------------------------------------------------------------------------- /src/pages/Home/Talk/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {Redirect, Route, Switch} from "react-router-dom"; 3 | import AllTalk from "./AllTalk/index.jsx"; 4 | import DetailTalk from "./DetailTalk/index.jsx"; 5 | import CreateTalk from "./CreateTalk/index.jsx"; 6 | import CustomEmpty from "../../../componets/CustomEmpty/index.jsx"; 7 | 8 | export default function Talk() { 9 | 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | ) 21 | } -------------------------------------------------------------------------------- /src/pages/Home/Talk/index.less: -------------------------------------------------------------------------------- 1 | .talk-container { 2 | height: 100%; 3 | 4 | .float-container { 5 | position: absolute; 6 | width: 600px; 7 | height: 80%; 8 | 9 | .operate { 10 | bottom: 0; 11 | right: -70px; 12 | height: 40px; 13 | width: 40px; 14 | border-radius: 40px; 15 | background-color: #fff; 16 | position: absolute; 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | cursor: pointer; 21 | color: #646464; 22 | } 23 | } 24 | 25 | .talks { 26 | height: 80%; 27 | overflow-y: scroll; 28 | z-index: 1; 29 | 30 | .talk { 31 | width: 600px; 32 | background-color: #F9FBFF; 33 | border-radius: 15px; 34 | margin: 10px; 35 | padding: 10px; 36 | 37 | .talk-title { 38 | display: flex; 39 | align-items: center; 40 | 41 | .talk-title-portrait { 42 | width: 45px; 43 | height: 45px; 44 | background-color: #4C9BFF; 45 | border-radius: 40px; 46 | } 47 | 48 | .talk-title-info { 49 | display: flex; 50 | flex-direction: column; 51 | justify-content: center; 52 | margin: 5px; 53 | 54 | .talk-title-info-name { 55 | font-size: 18px; 56 | line-height: 24px; 57 | } 58 | 59 | .talk-title-info-time { 60 | font-size: 10px; 61 | line-height: 14px; 62 | } 63 | } 64 | 65 | } 66 | 67 | .talk-content { 68 | display: flex; 69 | flex-direction: column; 70 | background-color: #fff; 71 | padding: 5px; 72 | margin-top: 10px; 73 | border-radius: 15px; 74 | font-size: 14px; 75 | 76 | .talk-content-img { 77 | width: 100px; 78 | height: 100px; 79 | object-fit: cover; 80 | border-radius: 5px; 81 | } 82 | } 83 | 84 | .talk-bottom { 85 | display: flex; 86 | flex-direction: column; 87 | font-size: 12px; 88 | user-select: none; 89 | 90 | .talk-bottom-operation { 91 | display: flex; 92 | justify-content: space-between; 93 | 94 | .talk-bottom-operation-item { 95 | display: flex; 96 | padding: 0 3px; 97 | border-radius: 5px; 98 | cursor: pointer; 99 | 100 | &:hover { 101 | background-color: #EDF2F9; 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/pages/Home/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | import {listen} from "@tauri-apps/api/event"; 3 | import {PhysicalPosition} from "@tauri-apps/api/window"; 4 | import {trayWindowHeight} from "../TrayMenu/window.jsx"; 5 | import {invoke} from "@tauri-apps/api/core"; 6 | import {TrayIcon} from "@tauri-apps/api/tray"; 7 | 8 | listen('tray_menu', async (event) => { 9 | 10 | const homeWindow = await WebviewWindow.getByLabel('home') 11 | let trayWindow = await WebviewWindow.getByLabel('tray_menu') 12 | if (!homeWindow) return 13 | 14 | let position = event.payload; 15 | let scaleFactor = await trayWindow.scaleFactor(); 16 | let logicalPosition = new PhysicalPosition(position.x, position.y).toLogical(scaleFactor); 17 | logicalPosition.y = logicalPosition.y - trayWindowHeight 18 | if (trayWindow) { 19 | await trayWindow.setAlwaysOnTop(true) 20 | await trayWindow.setPosition(logicalPosition) 21 | await trayWindow.show() 22 | await trayWindow.setFocus() 23 | } 24 | }) 25 | 26 | export default function CreateHomeWindow() { 27 | TrayIcon.getById("tray").then(async (res) => { 28 | let userInfo = await invoke("get_user_info", {}) 29 | res.setTooltip(userInfo.username ? userInfo.username : "linyu") 30 | }) 31 | let webview = new WebviewWindow("home", { 32 | url: "/home", 33 | title: "linyu", 34 | center: true, 35 | width: 1010, 36 | minWidth: 900, 37 | height: 750, 38 | minHeight: 600, 39 | decorations: false, 40 | transparent: true, 41 | shadow: false 42 | }); 43 | webview.once("tauri://webview-created", async function () { 44 | const appWindow = await WebviewWindow.getByLabel('login') 45 | appWindow?.close(); 46 | }); 47 | } -------------------------------------------------------------------------------- /src/pages/ImageViewer/index.less: -------------------------------------------------------------------------------- 1 | .image-viewer-container { 2 | position: relative; 3 | 4 | .image-viewer { 5 | color: #FFFFFF; 6 | width: 100vw; 7 | height: 100vh; 8 | background-color: #1F1F1F; 9 | overflow: hidden; 10 | display: flex; 11 | flex-direction: column; 12 | 13 | .top-bar { 14 | width: 100%; 15 | height: 40px; 16 | z-index: 1100; 17 | background-color: #2d2d2d; 18 | } 19 | } 20 | } 21 | 22 | .react-viewer { 23 | .react-viewer-mask { 24 | background-color: transparent !important; 25 | } 26 | 27 | .react-viewer-footer { 28 | height: 60px; 29 | } 30 | 31 | .react-viewer-image { 32 | margin-top: 40px; 33 | } 34 | } -------------------------------------------------------------------------------- /src/pages/ImageViewer/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | import {setItem} from "../../utils/storage.js"; 3 | 4 | export default async function CreateImageViewer(fileName, url) { 5 | await setItem("image-viewer-url", {fileName, url}) 6 | const window = await WebviewWindow.getByLabel("image-viewer") 7 | if (window) { 8 | await window.show() 9 | await window.setFocus() 10 | return 11 | } 12 | let webview = new WebviewWindow("image-viewer", { 13 | url: "/imageviewer", 14 | width: 800, 15 | title: "linyu", 16 | height: 650, 17 | center: true, 18 | decorations: false, 19 | resizable: false, 20 | focus: true, 21 | }); 22 | 23 | } -------------------------------------------------------------------------------- /src/pages/Login/LoginSet/index.jsx: -------------------------------------------------------------------------------- 1 | import CustomDragDiv from "../../../componets/CustomDragDiv/index.jsx"; 2 | import CustomInput from "../../../componets/CustomInput/index.jsx"; 3 | import CustomButton from "../../../componets/CustomButton/index.jsx"; 4 | import {getLocalItem, setLocalItem} from "../../../utils/storage.js"; 5 | import {useEffect, useState} from "react"; 6 | import {useHistory} from "react-router-dom"; 7 | import {useToast} from "../../../componets/CustomToast/index.jsx"; 8 | 9 | export default function LoginSet() { 10 | 11 | const h = useHistory(); 12 | const [serverIp, setServerIp] = useState("") 13 | const [serverWs, setServerWs] = useState("") 14 | const showToast = useToast(); 15 | 16 | useEffect(() => { 17 | let ip = getLocalItem("serverIp") 18 | let ws = getLocalItem("serverWs") 19 | setServerIp(ip ? ip : "http://127.0.0.1:9200") 20 | setServerWs(ws ? ws : "ws://127.0.0.1:9100") 21 | }, []) 22 | 23 | return ( 24 | 31 | 设置服务器 32 |
33 |
IP地址:
34 | setServerIp(v)}/> 35 |
36 |
37 |
WebSocket地址:
38 | setServerWs(v)}/> 39 |
40 |
41 | { 43 | setLocalItem("serverIp", serverIp) 44 | setLocalItem("serverWs", serverWs) 45 | showToast("设置成功~") 46 | }} 47 | > 48 | 确定 49 | 50 | h.push('/login/account')} 52 | > 53 | 取消 54 | 55 |
56 |
57 | ) 58 | } -------------------------------------------------------------------------------- /src/pages/Login/QrCodeLogin/index.less: -------------------------------------------------------------------------------- 1 | .qr-code-login { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | width: 100%; 7 | height: 100%; 8 | user-select: none; 9 | 10 | .qr-code-content { 11 | width: 200px; 12 | height: 200px; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | background-color: #FFFFFF; 17 | border-radius: 10px; 18 | 19 | .qr-expired { 20 | width: 200px; 21 | height: 200px; 22 | display: flex; 23 | position: absolute; 24 | justify-content: center; 25 | align-items: center; 26 | background-color: rgba(255, 255, 255, 0.97); 27 | border-radius: 10px; 28 | border: 1px dashed #999; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/pages/Login/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import IconButton from "../../componets/IconButton/index.jsx"; 3 | import CustomDragDiv from "../../componets/CustomDragDiv/index.jsx"; 4 | import {useEffect} from "react"; 5 | import CreateAboutWindow from "../AboutWindow/window.jsx"; 6 | import CustomBox from "../../componets/CustomBox/index.jsx"; 7 | import {exit} from "@tauri-apps/plugin-process"; 8 | import {getAllWindows} from "@tauri-apps/api/window"; 9 | import Ws from "../../utils/ws.js"; 10 | import {Redirect, Route, Switch, useHistory} from "react-router-dom"; 11 | import AccountLogin from "./AccountLogin/index.jsx"; 12 | import QrCodeLogin from "./QrCodeLogin/index.jsx"; 13 | import LoginSet from "./LoginSet/index.jsx"; 14 | 15 | export default function Login() { 16 | 17 | const h = useHistory(); 18 | 19 | useEffect(() => { 20 | (async () => { 21 | Ws.disconnect() 22 | let windows = await getAllWindows() 23 | windows?.map(w => { 24 | if (w.label !== 'login') { 25 | w.close(); 26 | } 27 | }) 28 | })() 29 | }) 30 | 31 | return ( 32 | 33 | 34 |
35 | } 37 | onClick={CreateAboutWindow} 38 | /> 39 |
40 |
41 | } 43 | onClick={() => h.push('/login/set')} 44 | /> 45 | } 48 | onClick={exit} 49 | /> 50 |
51 |
52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/pages/Login/index.less: -------------------------------------------------------------------------------- 1 | .login { 2 | background-image: linear-gradient(to bottom, #EDF2F9, #fff); 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | overflow: hidden; 7 | 8 | .login-drag { 9 | position: absolute; 10 | height: 60px; 11 | width: 100%; 12 | } 13 | 14 | .login-operate { 15 | top: 10px; 16 | position: absolute; 17 | color: #444444; 18 | display: flex; 19 | justify-content: space-between; 20 | width: 100%; 21 | } 22 | 23 | .login-icon { 24 | margin-top: 55px; 25 | user-select: none; 26 | } 27 | 28 | .login-name-input { 29 | margin: 15px; 30 | } 31 | 32 | .login-pwd-input { 33 | margin: 10px; 34 | width: 260px; 35 | height: 40px; 36 | border-radius: 10px; 37 | background-color: #FFFFFF; 38 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 39 | } 40 | 41 | .login-button { 42 | margin-top: 25px; 43 | width: 260px; 44 | height: 40px; 45 | border-radius: 10px; 46 | background-color: #4C9BFF; 47 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | font-weight: 500; 52 | font-size: 18px; 53 | color: #FFFFFF; 54 | user-select: none; 55 | cursor: pointer; 56 | } 57 | 58 | .login-button.disabled { 59 | cursor: not-allowed; 60 | background-color: rgba(76, 155, 255, 0.5); 61 | 62 | &:hover { 63 | background-color: rgba(76, 155, 255, 0.5); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/pages/Login/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | import {setItem} from "../../utils/storage.js"; 3 | import {getAllWindows} from "@tauri-apps/api/window"; 4 | import {TrayIcon} from "@tauri-apps/api/tray"; 5 | import {invoke} from "@tauri-apps/api/core"; 6 | import Ws from "../../utils/ws.js"; 7 | 8 | export default async function CreateLogin() { 9 | TrayIcon.getById("tray").then(async (res) => { 10 | res.setTooltip("linyu") 11 | }) 12 | const window = await WebviewWindow.getByLabel(`login`) 13 | if (window) { 14 | await window.show() 15 | await window.setFocus() 16 | return 17 | } 18 | let webview = new WebviewWindow(`login`, { 19 | url: "/login", 20 | width: 360, 21 | height: 510, 22 | title: "林语", 23 | center: true, 24 | transparent: true, 25 | decorations: false, 26 | resizable: false, 27 | fullscreen: false, 28 | shadow: false, 29 | }); 30 | webview.once("tauri://webview-created", async function () { 31 | Ws.disconnect() 32 | let windows = await getAllWindows() 33 | windows?.map(w => { 34 | if (w.label !== 'login') { 35 | w.close(); 36 | } 37 | }) 38 | }); 39 | } -------------------------------------------------------------------------------- /src/pages/MessageBox/index.less: -------------------------------------------------------------------------------- 1 | .message-box-container { 2 | padding: 5px; 3 | 4 | .message-box { 5 | background-image: linear-gradient(to bottom, #EDF2F9, #fff); 6 | border: 1px solid #E1E0E0; 7 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 8 | height: calc(100vh - 10px); 9 | border-radius: 10px; 10 | user-select: none; 11 | 12 | .message-box-top { 13 | height: 40px; 14 | margin: 0 10px; 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | .message-box-content { 20 | 21 | .message-box-content-item { 22 | padding: 10px; 23 | display: flex; 24 | align-items: center; 25 | height: 50px; 26 | cursor: pointer; 27 | 28 | .chat-card-portrait { 29 | width: 45px; 30 | height: 45px; 31 | border-radius: 45px; 32 | margin-right: 5px; 33 | } 34 | 35 | .chat-card-content { 36 | height: 45px; 37 | width: calc(100% - 50px); 38 | display: flex; 39 | flex-direction: column; 40 | 41 | .chat-card-content-item { 42 | display: flex; 43 | justify-content: space-between; 44 | align-items: center; 45 | width: 100%; 46 | 47 | .ellipsis { 48 | white-space: nowrap; 49 | overflow: hidden; 50 | text-overflow: ellipsis; 51 | } 52 | } 53 | } 54 | 55 | &:hover { 56 | background-color: #EDF2F9; 57 | } 58 | } 59 | } 60 | 61 | .message-box-bottom { 62 | font-weight: 600; 63 | color: #4C9BFF; 64 | font-size: 14px; 65 | height: 40px; 66 | margin: 0 10px; 67 | display: flex; 68 | align-items: center; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/pages/MessageBox/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | 3 | export let messageBoxWindowWidth = 280 4 | export let messageBoxWindowHeight = 90 5 | 6 | export default async function CrateMessageBox() { 7 | let webview = new WebviewWindow("massage-box", { 8 | url: "/messagebox", 9 | title: "linyu", 10 | width: messageBoxWindowWidth, 11 | height: messageBoxWindowHeight, 12 | skipTaskbar: true, 13 | decorations: false, 14 | center: false, 15 | transparent: true, 16 | resizable: false, 17 | shadow: false, 18 | alwaysOnTop: true, 19 | focus: true, 20 | x: 0, 21 | y: window.screen.height + 100 22 | }); 23 | await webview.listen("tauri://blur", async function () { 24 | const window = await WebviewWindow.getByLabel('massage-box') 25 | window.hide(); 26 | }); 27 | await webview.listen("tauri://window-created", async function () { 28 | const window = await WebviewWindow.getByLabel('massage-box') 29 | window.hide(); 30 | }); 31 | } -------------------------------------------------------------------------------- /src/pages/Register/index.less: -------------------------------------------------------------------------------- 1 | .register { 2 | height: 100%; 3 | overflow: hidden; 4 | display: flex; 5 | flex-direction: column; 6 | background: linear-gradient(-45deg, #80d2ff, #d7e3fd, #7fb5ff); 7 | animation: gradientAnimation 15s ease infinite; 8 | position: relative; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/Register/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | 3 | export default async function CreateRegisterWindow() { 4 | const window = await WebviewWindow.getByLabel('register') 5 | if (window) { 6 | window.show() 7 | window.unminimize() 8 | window.setFocus() 9 | return 10 | } 11 | let webview = new WebviewWindow("register", { 12 | url: "/register", 13 | title: "注册", 14 | width: 640, 15 | height: 500, 16 | decorations: false, 17 | center: true, 18 | transparent: true, 19 | resizable: false, 20 | shadow: false, 21 | focus: true 22 | }); 23 | } -------------------------------------------------------------------------------- /src/pages/TrayMenu/index.jsx: -------------------------------------------------------------------------------- 1 | import "./index.less" 2 | import {exit} from "@tauri-apps/plugin-process"; 3 | import CustomLine from "../../componets/CustomLine/index.jsx"; 4 | import {useEffect, useState} from "react"; 5 | import {invoke} from "@tauri-apps/api/core"; 6 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow"; 7 | import {listen} from "@tauri-apps/api/event"; 8 | 9 | export default function TrayMenu() { 10 | const [userInfo, setUserInfo] = useState(""); 11 | 12 | useEffect(() => { 13 | (async () => { 14 | let userInfo = await invoke("get_user_info", {}) 15 | setUserInfo(userInfo) 16 | })() 17 | let unListen = listen('user-info-reload', async (event) => { 18 | setUserInfo(event.payload) 19 | }) 20 | return async () => { 21 | (await unListen)(); 22 | } 23 | }, []) 24 | 25 | const onShowHome = async () => { 26 | const homeWindow = await WebviewWindow.getByLabel('home') 27 | await homeWindow.show() 28 | await homeWindow.unminimize() 29 | await homeWindow.setFocus() 30 | const trayWindow = await WebviewWindow.getByLabel('tray_menu') 31 | await trayWindow.hide() 32 | } 33 | 34 | const onQuit = () => { 35 | exit() 36 | } 37 | 38 | return ( 39 |
40 |
41 | {userInfo.portrait}/ 46 |
47 | {userInfo.username} 48 |
49 | 50 |
51 |
52 | 53 | 打开主页面 54 |
55 |
56 | 57 | 退出 58 |
59 |
60 |
61 |
62 | ) 63 | } -------------------------------------------------------------------------------- /src/pages/TrayMenu/index.less: -------------------------------------------------------------------------------- 1 | .tray-menu-container { 2 | padding: 5px; 3 | 4 | .tray-menu { 5 | background-image: linear-gradient(to bottom, #EDF2F9, #fff); 6 | border: 1px solid #E1E0E0; 7 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 8 | height: calc(100vh - 10px); 9 | border-radius: 10px; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | overflow: hidden; 14 | user-select: none; 15 | padding: 0 10px; 16 | 17 | .tray-menu-portrait { 18 | border-radius: 50px; 19 | margin-top: 5px; 20 | margin-bottom: 2px; 21 | width: 50px; 22 | height: 50px; 23 | background-color: #4C9BFF; 24 | } 25 | 26 | .tray-menu-operation { 27 | flex: 1; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | 32 | .tray-menu-operation-item { 33 | font-size: 14px; 34 | height: 30px; 35 | width: 100px; 36 | display: flex; 37 | align-items: center; 38 | padding: 0 5px; 39 | border-radius: 5px; 40 | cursor: pointer; 41 | 42 | &:hover { 43 | background-color: #EDF2F9; 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/pages/TrayMenu/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | 3 | 4 | export let trayWindowWidth = 120 5 | export let trayWindowHeight = 160 6 | 7 | export default async function CreateTrayWindow() { 8 | let webview = new WebviewWindow("tray_menu", { 9 | url: "/tray", 10 | title: "linyu", 11 | width: trayWindowWidth, 12 | height: trayWindowHeight, 13 | skipTaskbar: true, 14 | decorations: false, 15 | center: false, 16 | transparent: true, 17 | resizable: false, 18 | shadow: false, 19 | alwaysOnTop: true, 20 | focus: true, 21 | x: 0, 22 | y: window.screen.height + 100 23 | }); 24 | await webview.listen("tauri://blur", async function () { 25 | const trayWindow = await WebviewWindow.getByLabel('tray_menu') 26 | trayWindow.hide(); 27 | }); 28 | await webview.listen("tauri://window-created", async function () { 29 | const trayWindow = await WebviewWindow.getByLabel('tray_menu') 30 | trayWindow.hide(); 31 | }); 32 | } -------------------------------------------------------------------------------- /src/pages/VideoChat/index.less: -------------------------------------------------------------------------------- 1 | .video-chat { 2 | background-color: #F9FBFF; 3 | overflow: hidden; 4 | display: flex; 5 | flex-direction: column; 6 | 7 | .video-container { 8 | display: flex; 9 | justify-content: center; 10 | width: 100%; 11 | height: 100%; 12 | color: #FFFFFF; 13 | 14 | .video { 15 | width: 100%; 16 | height: 100%; 17 | position: relative; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | 22 | .info-bar { 23 | width: 100%; 24 | position: absolute; 25 | top: 0; 26 | background-color: rgba(152, 152, 152, 0.3); 27 | backdrop-filter: blur(5px); 28 | height: 40px; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | z-index: 999; 33 | user-select: none; 34 | color: #FFFFFF; 35 | } 36 | 37 | .operate-bar { 38 | width: 100%; 39 | position: absolute; 40 | bottom: 15px; 41 | height: 80px; 42 | display: flex; 43 | flex-direction: column; 44 | justify-content: center; 45 | align-items: center; 46 | z-index: 999; 47 | user-select: none; 48 | 49 | .operate { 50 | color: #FFFFFF; 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | border-radius: 40px; 55 | width: 40px; 56 | height: 40px; 57 | margin: 10px; 58 | background-color: rgba(182, 182, 182, 0.5); 59 | backdrop-filter: blur(10px); 60 | cursor: pointer; 61 | } 62 | 63 | .operate.hangup { 64 | width: 50px; 65 | height: 50px; 66 | border-radius: 15px; 67 | background-color: #ff4c4c; 68 | } 69 | 70 | .operate.accept { 71 | width: 50px; 72 | height: 50px; 73 | border-radius: 15px; 74 | background-color: #4C9BFF; 75 | } 76 | } 77 | 78 | .max-window { 79 | height: 100%; 80 | border-radius: 10px; 81 | transform: scaleX(-1); 82 | } 83 | 84 | .min-window { 85 | position: absolute; 86 | height: 90px; 87 | width: 120px; 88 | border-radius: 10px; 89 | top: 70px; 90 | right: 10px; 91 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 92 | z-index: 100; 93 | transform: scaleX(-1); 94 | } 95 | 96 | .dots { 97 | border-radius: 10px; 98 | color: #FFFFFF; 99 | user-select: none; 100 | margin-top: 10px; 101 | width: 200px; 102 | text-align: center; 103 | background-color: rgba(182, 182, 182, 0.4); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/pages/VideoChat/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | import {setItem} from "../../utils/storage.js"; 3 | 4 | export default async function CreateVideoChat(userId, isSender, isOnlyAudio = false) { 5 | await setItem("video-chat", {userId, isSender, isOnlyAudio}) 6 | const window = await WebviewWindow.getByLabel(`video-chat`) 7 | if (window) { 8 | await window.show() 9 | await window.setFocus() 10 | return 11 | } 12 | let webview = new WebviewWindow(`video-chat`, { 13 | url: "/videochat", 14 | width: isOnlyAudio ? 370 : 750, 15 | height: 600, 16 | title: "linyu", 17 | center: true, 18 | transparent: true, 19 | decorations: false, 20 | resizable: false, 21 | shadow: false, 22 | focus: true, 23 | }); 24 | } -------------------------------------------------------------------------------- /src/pages/screenshot/index.less: -------------------------------------------------------------------------------- 1 | @ball-size: 5px; 2 | @mask-border: 2px; 3 | @ball-position: ~"calc(-@{ball-size} - @{mask-border})"; 4 | @blue: #4C9BFF; 5 | 6 | #screenshot { 7 | width: 100vw; 8 | height: 100vh; 9 | 10 | &::before { 11 | content: ""; 12 | position: absolute; 13 | left: var(--left); 14 | top: var(--top); 15 | width: var(--width); 16 | height: var(--height); 17 | box-shadow: 0 0 0 999vw rgba(0, 0, 0, 0.5); 18 | } 19 | 20 | #rectangle { 21 | will-change: left, top, width, height; 22 | cursor: move; 23 | position: absolute; 24 | border: @mask-border solid @blue; 25 | box-sizing: border-box; 26 | 27 | 28 | .toolbar { 29 | position: absolute; 30 | background-color: white; 31 | border: 0.5px solid @blue; 32 | border-radius: 3px; 33 | 34 | .tool { 35 | cursor: pointer; 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | 40 | &:hover { 41 | background-color: rgb(230, 230, 230); 42 | } 43 | } 44 | } 45 | 46 | .ball { 47 | width: ~"calc(@{ball-size} * 2)"; 48 | height: ~"calc(@{ball-size} * 2)"; 49 | border: 1px solid white; 50 | border-radius: 50%; 51 | background-color: @blue; 52 | position: absolute; 53 | 54 | &.top-left { 55 | top: @ball-position; 56 | left: @ball-position; 57 | cursor: nw-resize; 58 | } 59 | 60 | &.top-middle { 61 | top: @ball-position; 62 | left: 50%; 63 | transform: translateX(-50%); 64 | cursor: n-resize; 65 | } 66 | 67 | &.top-right { 68 | top: @ball-position; 69 | right: @ball-position; 70 | cursor: ne-resize; 71 | } 72 | 73 | &.middle-left { 74 | top: 50%; 75 | left: @ball-position; 76 | transform: translateY(-50%); 77 | cursor: w-resize; 78 | } 79 | 80 | &.middle-right { 81 | top: 50%; 82 | right: @ball-position; 83 | transform: translateY(-50%); 84 | cursor: e-resize; 85 | } 86 | 87 | &.bottom-left { 88 | bottom: @ball-position; 89 | left: @ball-position; 90 | cursor: sw-resize; 91 | } 92 | 93 | &.bottom-middle { 94 | bottom: @ball-position; 95 | left: 50%; 96 | transform: translateX(-50%); 97 | cursor: s-resize; 98 | } 99 | 100 | &.bottom-right { 101 | bottom: @ball-position; 102 | right: @ball-position; 103 | cursor: se-resize; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/pages/screenshot/window.jsx: -------------------------------------------------------------------------------- 1 | import {WebviewWindow} from "@tauri-apps/api/WebviewWindow" 2 | import {setItem} from "../../utils/storage.js"; 3 | 4 | export default async function CreateScreenshot(toUserWindowLabel) { 5 | await setItem("screenshot", {toUserWindowLabel}) 6 | const window = await WebviewWindow.getByLabel("screenshot") 7 | if (window) { 8 | await window.show() 9 | await window.setFocus() 10 | return 11 | } 12 | let webview = new WebviewWindow("screenshot", { 13 | url: "/screenshot", 14 | width: 500, 15 | height: 500, 16 | center: false, 17 | transparent: true, 18 | resizable: false, 19 | shadow: false, 20 | alwaysOnTop: true, 21 | focus: true, 22 | fullscreen: true, 23 | }); 24 | } -------------------------------------------------------------------------------- /src/store/chat/action.js: -------------------------------------------------------------------------------- 1 | import * as type from "./type"; 2 | export const setCurrentChatId = (currentChatId, currentChatUserInfo) => { 3 | return { 4 | type: type.Set_CurrentChatId, 5 | currentChatId: currentChatId, 6 | currentChatUserInfo: currentChatUserInfo 7 | }; 8 | }; 9 | 10 | export const addChatWindowUser = (userInfo) => { 11 | return { 12 | type: type.Add_Chat_Window_User, 13 | userInfo: userInfo 14 | }; 15 | }; 16 | 17 | export const deleteChatWindowUser = (userId) => { 18 | return { 19 | type: type.Delete_Chat_Window_User, 20 | userId: userId 21 | }; 22 | }; -------------------------------------------------------------------------------- /src/store/chat/reducer.js: -------------------------------------------------------------------------------- 1 | import * as type from "./type"; 2 | 3 | 4 | let defaultState = { 5 | currentChatId: "", 6 | currentChatUserInfo: null, 7 | chatWindowUsers: new Map(), 8 | }; 9 | 10 | export const chatData = (state = defaultState, action) => { 11 | switch (action.type) { 12 | case type.Set_CurrentChatId: 13 | return { 14 | ...state, 15 | ...{currentChatId: action.currentChatId, currentChatUserInfo: action.currentChatUserInfo}, 16 | }; 17 | case type.Add_Chat_Window_User: 18 | const updatedChatWindowUsers = new Map(state.chatWindowUsers); 19 | updatedChatWindowUsers.set(action.userInfo.fromId, action.userInfo); 20 | return { 21 | ...state, 22 | chatWindowUsers: updatedChatWindowUsers 23 | }; 24 | case type.Delete_Chat_Window_User: 25 | const chatWindowUsersAfterDeletion = new Map(state.chatWindowUsers); 26 | chatWindowUsersAfterDeletion.delete(action.userId); 27 | return { 28 | ...state, 29 | chatWindowUsers: chatWindowUsersAfterDeletion 30 | }; 31 | default: 32 | return state; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/store/chat/type.js: -------------------------------------------------------------------------------- 1 | // 设置当前聊天会话id 2 | export const Set_CurrentChatId = "Set_CurrentChatId"; 3 | //添加独立聊天窗口的用户 4 | export const Add_Chat_Window_User = "Add_Chat_Window_User"; 5 | //添加独立聊天窗口的用户 6 | export const Delete_Chat_Window_User = "Delete_Chat_Window_User"; 7 | -------------------------------------------------------------------------------- /src/store/home/action.js: -------------------------------------------------------------------------------- 1 | import * as type from "./type"; 2 | 3 | export const setCurrentOption = (currentOption) => { 4 | return { 5 | type: type.Set_CurrentOption, 6 | currentOption: currentOption, 7 | }; 8 | }; 9 | 10 | export const setCurrentLoginUserInfo = (userId, username, account, portrait) => { 11 | return { 12 | type: type.Set_User_Info, 13 | userId, 14 | username, 15 | account, 16 | portrait 17 | }; 18 | }; 19 | 20 | export const setFileFileProgress = (fileName, progress) => { 21 | return { 22 | type: type.Set_File_Progress, 23 | fileName: fileName, 24 | progress: progress 25 | }; 26 | }; -------------------------------------------------------------------------------- /src/store/home/reducer.js: -------------------------------------------------------------------------------- 1 | import * as type from "./type"; 2 | 3 | 4 | let defaultState = { 5 | currentOption: "chat", 6 | userId: "", 7 | username: "", 8 | account: "", 9 | portrait: "", 10 | fileProgress: {} 11 | }; 12 | 13 | export const homeData = (state = defaultState, action) => { 14 | switch (action.type) { 15 | case type.Set_CurrentOption: 16 | return { 17 | ...state, 18 | ...{currentOption: action.currentOption}, 19 | }; 20 | case type.Set_User_Info: 21 | return { 22 | ...state, 23 | ...action, 24 | }; 25 | case type.Set_File_Progress: 26 | state.fileProgress[action.fileName] = action.progress; 27 | return { 28 | ...state 29 | }; 30 | default: 31 | return state; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/store/home/type.js: -------------------------------------------------------------------------------- 1 | // 设置当前聊天会话id 2 | export const Set_CurrentOption = "Set_CurrentOption"; 3 | //设置当前登录用户信息 4 | export const Set_User_Info = "Set_User_Info"; 5 | //设置当前登录用户信息 6 | export const Set_File_Progress = "Set_File_progress"; 7 | -------------------------------------------------------------------------------- /src/store/index.jsx: -------------------------------------------------------------------------------- 1 | import {legacy_createStore as createStore, combineReducers} from "redux"; 2 | import * as chat from "./chat/reducer"; 3 | import * as home from "./home/reducer"; 4 | 5 | let store = createStore( 6 | combineReducers({...chat, ...home}) 7 | ); 8 | 9 | export default store; 10 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color: #1F1F1F; 8 | background-color: transparent; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | padding: 0; 19 | overflow: hidden; 20 | } 21 | 22 | img { 23 | -webkit-user-drag: none; 24 | } 25 | 26 | textarea { 27 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/emoji.js: -------------------------------------------------------------------------------- 1 | export const emojis = [ 2 | '😀', 3 | '😃', 4 | '😄', 5 | '😁', 6 | '😆', 7 | '😅', 8 | '😂', 9 | '🤣', 10 | '😊', 11 | '😇', 12 | '🙂', 13 | '🙃', 14 | '😉', 15 | '😌', 16 | '😍', 17 | '😘', 18 | '😗', 19 | '😙', 20 | '😚', 21 | '😋', 22 | '😛', 23 | '😝', 24 | '😜', 25 | '🤪', 26 | '🤨', 27 | '🧐', 28 | '🤓', 29 | '😎', 30 | '🤩', 31 | '😏', 32 | '😒', 33 | '😞', 34 | '😔', 35 | '😟', 36 | '😕', 37 | '🙁', 38 | '😣', 39 | '😖', 40 | '😫', 41 | '😩', 42 | '😢', 43 | '😭', 44 | '😮', 45 | '💨', 46 | '😤', 47 | '😠', 48 | '😡', 49 | '🤬', 50 | '🤯', 51 | '😳', 52 | '😱', 53 | '😨', 54 | '😰', 55 | '😥', 56 | '😓', 57 | '🤗', 58 | '🤔', 59 | '🤭', 60 | '🤫', 61 | '🤥', 62 | '😶', 63 | '🌫️', 64 | '😐', 65 | '😑', 66 | '😬', 67 | '🙄', 68 | '😯', 69 | '😦', 70 | '😧', 71 | '😮', 72 | '😲', 73 | '😴', 74 | '🤤', 75 | '😪', 76 | '😵', 77 | '💫', 78 | '🤐', 79 | '🤢', 80 | '🤮', 81 | '🤧', 82 | '😷', 83 | '🤒', 84 | '🤕', 85 | '🤑', 86 | '🤠', 87 | '😈', 88 | '👿', 89 | '👹', 90 | '👺', 91 | '🤡', 92 | '💩', 93 | '👻', 94 | '💀', 95 | '👽', 96 | '👾', 97 | '🤖', 98 | '🎃', 99 | '😺', 100 | '😸', 101 | '😹', 102 | '😻', 103 | '😼', 104 | '😽', 105 | '🙀', 106 | '😿', 107 | '😾', 108 | '🐶', 109 | ] 110 | -------------------------------------------------------------------------------- /src/utils/img.js: -------------------------------------------------------------------------------- 1 | export function base64ToArrayBuffer(base64) { 2 | const binaryString = atob(base64); 3 | const arrayBuffer = new Uint8Array(binaryString.length); 4 | for (let i = 0; i < binaryString.length; i++) { 5 | arrayBuffer[i] = binaryString.charCodeAt(i); 6 | } 7 | return arrayBuffer.buffer 8 | } -------------------------------------------------------------------------------- /src/utils/shortcut.js: -------------------------------------------------------------------------------- 1 | import {register, unregister} from "@tauri-apps/plugin-global-shortcut"; 2 | import {emit} from "@tauri-apps/api/event"; 3 | 4 | export function shortcutRegister(shortcut, handler) { 5 | register(shortcut, handler) 6 | } 7 | 8 | export function shortcutRegisterAndEmit(shortcut, emitName) { 9 | register(shortcut, (e) => { 10 | if (e.state === "Pressed") { 11 | emit(emitName, e) 12 | } 13 | }) 14 | } 15 | 16 | export function UpdateShortcutRegister(shortcut, newShortcut, emitName) { 17 | if (shortcut) { 18 | unregister(shortcut) 19 | } 20 | if (newShortcut) { 21 | register(newShortcut, (e) => { 22 | if (e.state === "Pressed") { 23 | emit(emitName, e) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | export function isValidShortcut(shortcut) { 30 | const validModifiers = ["Ctrl", "Alt", "Shift", "Meta"]; 31 | const validKeys = [ 32 | "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", 33 | "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", 34 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "F1", "F2", "F3", 35 | "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "Enter", 36 | "Escape", "Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight" 37 | ]; 38 | const keys = shortcut.split('+').map(key => key.trim()); 39 | if (keys.length === 0) { 40 | return false; 41 | } 42 | 43 | let hasMainKey = false; 44 | 45 | for (let i = 0; i < keys.length; i++) { 46 | const key = keys[i]; 47 | if (validModifiers.includes(key)) { 48 | if (hasMainKey) { 49 | return false; 50 | } 51 | } else if (validKeys.includes(key)) { 52 | if (hasMainKey) { 53 | return false; 54 | } 55 | hasMainKey = true; 56 | } else { 57 | return false; 58 | } 59 | } 60 | return hasMainKey; 61 | } -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | import {invoke} from "@tauri-apps/api/core"; 2 | 3 | export async function setItem(key, value) { 4 | let userinfo = await invoke("get_user_info", {}) 5 | let userKey = "user" + userinfo.user_id 6 | let data = JSON.parse(localStorage.getItem(userKey)) 7 | localStorage.setItem(userKey, JSON.stringify({...data, [key]: value})) 8 | } 9 | 10 | export async function getItem(key) { 11 | let userinfo = await invoke("get_user_info", {}) 12 | let userKey = "user" + userinfo.user_id 13 | let data = JSON.parse(localStorage.getItem(userKey)) 14 | return data[key] 15 | } 16 | 17 | export function getLocalItem(key) { 18 | return JSON.parse(localStorage.getItem(key)) 19 | } 20 | 21 | export function setLocalItem(key, value) { 22 | localStorage.setItem(key, JSON.stringify(value)) 23 | } -------------------------------------------------------------------------------- /src/utils/string.js: -------------------------------------------------------------------------------- 1 | export function getFileNameAndType(url) { 2 | const urlObj = new URL(url); 3 | const pathname = urlObj.pathname; 4 | const regex = /\/([^\/]+)$/; 5 | const match = pathname.match(regex); 6 | if (match && match.length > 1) { 7 | const fileName = match[1]; 8 | const fileNameParts = fileName.split('.'); 9 | const fileType = fileNameParts.length > 1 ? fileNameParts.pop() : null; 10 | return { 11 | fileName: fileName, 12 | fileType: fileType 13 | }; 14 | } else { 15 | return { 16 | fileName: null, 17 | fileType: null 18 | }; 19 | } 20 | } 21 | 22 | export function isImageFile(filename) { 23 | const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tiff']; 24 | const fileExtension = filename.split('.').pop().toLowerCase(); 25 | return imageExtensions.includes(fileExtension); 26 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig(async () => ({ 6 | plugins: [react()], 7 | 8 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 9 | // 10 | // 1. prevent vite from obscuring rust errors 11 | clearScreen: false, 12 | // 2. tauri expects a fixed port, fail if that port is not available 13 | server: { 14 | port: 1420, 15 | strictPort: true, 16 | watch: { 17 | // 3. tell vite to ignore watching `src-tauri` 18 | ignored: ["**/src-tauri/**"], 19 | }, 20 | }, 21 | })); 22 | --------------------------------------------------------------------------------