mirror of
https://gitee.com/HuLaSpark/HuLa.git
synced 2024-11-29 18:28:30 +08:00
commit
156bb4ea28
@ -1,5 +1,7 @@
|
||||
# 后端服务地址
|
||||
VITE_SERVICE_URL="http://localhost:9095"
|
||||
VITE_SERVICE_URL="http://127.0.0.1:9190"
|
||||
# websocket服务地址
|
||||
VITE_WEBSOCKET_URL="ws://127.0.0.1:8090"
|
||||
# 项目标题
|
||||
VITE_APP_TITLE="HuLa—IM"
|
||||
# 标签后缀
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -22,7 +22,3 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# 忽略自动导入的文件
|
||||
/src/typings/auto-imports.d.ts
|
||||
/src/typings/components.d.ts
|
44
build/config/console.ts
Normal file
44
build/config/console.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import pkg from '../../package.json'
|
||||
|
||||
/**
|
||||
* 启动时打印信息
|
||||
* @param env 环境变量
|
||||
* @param mode 运行模式
|
||||
*/
|
||||
export const atStartup = (env: { [key: string]: string }, mode: string) => {
|
||||
return () => {
|
||||
if (mode === 'dev') {
|
||||
console.log(
|
||||
` 🍀 ${'\x1b[32m'}${'\x1b[38;2;19;152;127m'}${env.VITE_APP_NAME} ${'\x1b[0m'}${'\x1b[90m'}${pkg.version}${'\x1b[0m'}`
|
||||
)
|
||||
console.log(
|
||||
` ${'\u001b[38;2;26;178;146m'}${'\x1b[1m'}${'➜'}${'\x1b[0m'} ` +
|
||||
`${'当前环境: '}` +
|
||||
`${'\x1b[38;2;215;95;135m'}${mode}${'\x1b[0m'}`
|
||||
)
|
||||
console.log(
|
||||
` ${'\u001b[38;2;26;178;146m'}${'\x1b[1m'}${'➜'}${'\x1b[0m'} ${'\x1b[38;2;0;215;175m'}${'\x1b[1m'}${'Vue'}${'\x1b[0m'}: ` +
|
||||
`${'\x1b[90m'}${pkg.dependencies.vue}${'\x1b[0m'}` +
|
||||
` ${'\x1b[38;2;95;215;255m'}${'\x1b[1m'}${'Vite'}${'\x1b[0m'}: ` +
|
||||
`${'\x1b[90m'}${pkg.devDependencies.vite}${'\x1b[0m'}` +
|
||||
` ${'\u001b[38;5;33m'}${'\x1b[1m'}${'Node.js'}${'\x1b[0m'}: ` +
|
||||
`${'\x1b[90m'}${process.version}${'\x1b[0m'}`
|
||||
)
|
||||
console.log(
|
||||
` ${'\u001b[38;2;26;178;146m'}${'\x1b[1m'}${'➜'}${'\x1b[0m'} ` +
|
||||
`当前 ${'\x1b[38;2;215;135;0m'}${'\x1b[1m'}Tauri${'\x1b[0m'} 版本: `,
|
||||
`${'\x1b[90m'}${pkg.dependencies['@tauri-apps/api']}${'\x1b[0m'}`
|
||||
)
|
||||
console.log(
|
||||
` ${'\u001b[38;2;26;178;146m'}${'\x1b[1m'}${'\u001b[2m'}${'➜'}${'\x1b[0m'} ` +
|
||||
'后端服务地址: ' +
|
||||
`${'\x1b[38;2;95;175;255m'}https://github.com/nongyehong/HuLa-IM-Server${'\x1b[0m'}`
|
||||
)
|
||||
console.log(
|
||||
` ${'\u001b[38;2;26;178;146m'}${'\x1b[1m'}${'\u001b[2m'}${'➜'}${'\x1b[0m'} ` +
|
||||
'项目地址: ' +
|
||||
`${pkg.author.url}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
42
build/config/version.ts
Normal file
42
build/config/version.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import pkg from '../../package.json'
|
||||
|
||||
/** npm依赖包版本信息 */
|
||||
export interface PkgVersionInfo {
|
||||
name: string
|
||||
version: string
|
||||
}
|
||||
|
||||
interface Package {
|
||||
name: string
|
||||
version: string
|
||||
author: Record<string, string>
|
||||
dependencies: Record<string, string>
|
||||
devDependencies: Record<string, string>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface PkgJson {
|
||||
name: string
|
||||
version: string
|
||||
author: Record<string, string>
|
||||
dependencies: PkgVersionInfo[]
|
||||
devDependencies: PkgVersionInfo[]
|
||||
}
|
||||
|
||||
const pkgWithType = pkg as Package
|
||||
|
||||
const transformVersionData = (tuple: [string, string]): PkgVersionInfo => {
|
||||
const [name, version] = tuple
|
||||
return {
|
||||
name,
|
||||
version
|
||||
}
|
||||
}
|
||||
|
||||
export const pkgJson: PkgJson = {
|
||||
name: pkgWithType.name,
|
||||
version: pkgWithType.version,
|
||||
author: pkgWithType.author,
|
||||
dependencies: Object.entries(pkgWithType.dependencies).map((item) => transformVersionData(item)),
|
||||
devDependencies: Object.entries(pkgWithType.devDependencies).map((item) => transformVersionData(item))
|
||||
}
|
@ -1,4 +1,13 @@
|
||||
/** @type {import('cz-git').UserConfig} */
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/* 获取项目的修改范围 */
|
||||
const scopes = fs
|
||||
.readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name.replace(/s$/, ''))
|
||||
|
||||
module.exports = {
|
||||
// 继承的规则
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
@ -61,12 +70,10 @@ module.exports = {
|
||||
useAI: false,
|
||||
aiNumber: 1,
|
||||
themeColorCode: "38;5;168",
|
||||
scopes: [],
|
||||
allowCustomScopes: true,
|
||||
allowEmptyScopes: true,
|
||||
customScopesAlign: "bottom",
|
||||
customScopesAlias: "custom",
|
||||
emptyScopesAlias: "empty",
|
||||
customScopesAlign: 'bottom',
|
||||
scopes: [...scopes],
|
||||
upperCaseSubject: false,
|
||||
markBreakingChangeMode: false,
|
||||
allowBreakingChanges: ["feat", "fix"],
|
||||
@ -88,7 +95,6 @@ module.exports = {
|
||||
scopeOverrides: undefined,
|
||||
defaultBody: "",
|
||||
defaultIssues: "",
|
||||
defaultScope: "",
|
||||
defaultSubject: "",
|
||||
},
|
||||
};
|
||||
|
@ -7,7 +7,7 @@
|
||||
<title>HuLa</title>
|
||||
|
||||
<!--引入iconpark图标库-->
|
||||
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_78.2ef5ae05e210de3f66b0fe5c58a7a130.js"></script>
|
||||
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_81.5cb4aedb87267e96f93088d7f30024fe.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
31
package.json
31
package.json
@ -26,7 +26,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite --mode dev",
|
||||
"prod": "vite --mode prod",
|
||||
"build": "vue-tsc --noEmit && vite build --mode dev",
|
||||
"build": "vue-tsc --noEmit && vite build --mode pord",
|
||||
"preview": "vite preview",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build",
|
||||
@ -38,17 +38,18 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"@tauri-apps/api": "^1.5.6",
|
||||
"axios": "^1.6.8",
|
||||
"colorthief": "^2.4.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"naive-ui": "^2.38.1",
|
||||
"naive-ui": "^2.38.2",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"pinia-shared-state": "^0.5.1",
|
||||
"vue": "^3.4.23",
|
||||
"vue": "^3.4.27",
|
||||
"vue-draggable-plus": "^0.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
},
|
||||
@ -57,22 +58,22 @@
|
||||
"@commitlint/cli": "^19.2.2",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@tauri-apps/cli": "^1.5.11",
|
||||
"@tauri-apps/cli": "1.5.14",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"@unocss/preset-uno": "^0.59.4",
|
||||
"@unocss/reset": "^0.59.4",
|
||||
"@unocss/transformer-directives": "^0.59.4",
|
||||
"@unocss/transformer-variant-group": "^0.59.4",
|
||||
"@unocss/vite": "^0.59.4",
|
||||
"@unocss/preset-uno": "^0.61.0",
|
||||
"@unocss/reset": "^0.61.0",
|
||||
"@unocss/transformer-directives": "^0.61.0",
|
||||
"@unocss/transformer-variant-group": "^0.61.0",
|
||||
"@unocss/vite": "^0.61.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||
"@vueuse/core": "^10.8.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"conventional-changelog": "^5.1.0",
|
||||
"conventional-changelog-cli": "^4.1.0",
|
||||
"conventional-changelog": "^6.0.0",
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"cz-git": "^1.9.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@ -85,10 +86,10 @@
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.71.1",
|
||||
"sass-loader": "^14.1.1",
|
||||
"typescript": "^5.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"unplugin-auto-import": "^0.17.2",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "5.2.9",
|
||||
"vite": "5.3.1",
|
||||
"vue-tsc": "^2.0.6"
|
||||
},
|
||||
"config": {
|
||||
|
2060
pnpm-lock.yaml
2060
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
480
src-tauri/Cargo.lock
generated
480
src-tauri/Cargo.lock
generated
@ -127,7 +127,7 @@ dependencies = [
|
||||
"polling 2.8.0",
|
||||
"rustix 0.37.27",
|
||||
"slab",
|
||||
"socket2",
|
||||
"socket2 0.4.10",
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
@ -393,6 +393,9 @@ name = "bytes"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
@ -866,7 +869,7 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"toml 0.8.8",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
"winreg 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1488,6 +1491,25 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap 2.1.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@ -1515,6 +1537,12 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.3"
|
||||
@ -1527,20 +1555,6 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mac",
|
||||
"markup5ever 0.10.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.26.0"
|
||||
@ -1549,7 +1563,7 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mac",
|
||||
"markup5ever 0.11.0",
|
||||
"markup5ever",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
@ -1566,12 +1580,35 @@ dependencies = [
|
||||
"itoa 1.0.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hula"
|
||||
version = "1.5.0"
|
||||
@ -1585,6 +1622,43 @@ dependencies = [
|
||||
"window-vibrancy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.10",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.7",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.58"
|
||||
@ -1733,6 +1807,12 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
@ -1809,18 +1889,6 @@ dependencies = [
|
||||
"treediff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchiki"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358"
|
||||
dependencies = [
|
||||
"cssparser",
|
||||
"html5ever 0.25.2",
|
||||
"matches",
|
||||
"selectors",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.2"
|
||||
@ -1828,7 +1896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
|
||||
dependencies = [
|
||||
"cssparser",
|
||||
"html5ever 0.26.0",
|
||||
"html5ever",
|
||||
"indexmap 1.9.3",
|
||||
"matches",
|
||||
"selectors",
|
||||
@ -1971,20 +2039,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
|
||||
dependencies = [
|
||||
"log",
|
||||
"phf 0.8.0",
|
||||
"phf_codegen 0.8.0",
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.11.0"
|
||||
@ -2038,6 +2092,12 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
@ -2048,6 +2108,35 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.6.0"
|
||||
@ -2248,6 +2337,50 @@ dependencies = [
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.41",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@ -2806,6 +2939,48 @@ version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.10.0"
|
||||
@ -2872,6 +3047,15 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
@ -2899,6 +3083,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@ -2911,6 +3104,29 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.22.0"
|
||||
@ -2992,6 +3208,18 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa 1.0.10",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.4.0"
|
||||
@ -3130,6 +3358,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soup2"
|
||||
version = "0.2.1"
|
||||
@ -3233,6 +3471,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.2.4"
|
||||
@ -3246,6 +3490,27 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "5.0.0"
|
||||
@ -3351,11 +3616,12 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.6.1"
|
||||
version = "1.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f078117725e36d55d29fafcbb4b1e909073807ca328ae8deb8c0b3843aac0fed"
|
||||
checksum = "77567d2b3b74de4588d544147142d02297f3eaa171a25a065252141d8597a516"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"cocoa 0.24.1",
|
||||
"dirs-next",
|
||||
"dunce",
|
||||
@ -3363,13 +3629,15 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"getrandom 0.2.11",
|
||||
"glib",
|
||||
"glob",
|
||||
"gtk",
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"ico 0.2.0",
|
||||
"ignore",
|
||||
"indexmap 1.9.3",
|
||||
"infer 0.9.0",
|
||||
"nix",
|
||||
"notify-rust",
|
||||
@ -3382,6 +3650,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
"semver",
|
||||
"serde",
|
||||
@ -3407,14 +3676,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.5.0"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c"
|
||||
checksum = "ab30cba12974d0f9b09794f61e72cad6da2142d3ceb81e519321bab86ce53312"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"dirs-next",
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"json-patch",
|
||||
"semver",
|
||||
"serde",
|
||||
@ -3426,9 +3695,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc"
|
||||
checksum = "c3a1d90db526a8cdfd54444ad3f34d8d4d58fa5c536463915942393743bd06f8"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
"brotli",
|
||||
@ -3452,11 +3721,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875"
|
||||
checksum = "6a582d75414250122e4a597b9dd7d3c910a2c77906648fc2ac9353845ff0feec"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
@ -3466,9 +3735,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.14.2"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76"
|
||||
checksum = "cd7ffddf36d450791018e63a3ddf54979b9581d9644c584a5fb5611e6b5f20b4"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@ -3487,9 +3756,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "0.14.5"
|
||||
version = "0.14.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "067c56fc153b3caf406d7cd6de4486c80d1d66c0f414f39e94cb2f5543f6445f"
|
||||
checksum = "1989b3b4d611f5428b3414a4abae6fa6df30c7eb8ed33250ca90a5f7e5bb3655"
|
||||
dependencies = [
|
||||
"cocoa 0.24.1",
|
||||
"gtk",
|
||||
@ -3507,16 +3776,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.5.3"
|
||||
version = "1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75ad0bbb31fccd1f4c56275d0a5c3abdf1f59999f72cb4ef8b79b4ed42082a21"
|
||||
checksum = "450b17a7102e5d46d4bdabae0d1590fd27953e704e691fc081f06c06d2253b35"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"ctor",
|
||||
"dunce",
|
||||
"glob",
|
||||
"heck 0.4.1",
|
||||
"html5ever 0.26.0",
|
||||
"heck 0.5.0",
|
||||
"html5ever",
|
||||
"infer 0.13.0",
|
||||
"json-patch",
|
||||
"kuchikiki",
|
||||
@ -3667,8 +3936,11 @@ checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.7",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
@ -3684,6 +3956,30 @@ dependencies = [
|
||||
"syn 2.0.41",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
@ -3752,6 +4048,12 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.40"
|
||||
@ -3822,6 +4124,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
@ -3899,6 +4207,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.0.11"
|
||||
@ -3953,6 +4267,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
||||
dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
@ -4031,6 +4354,19 @@ version = "0.2.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.66"
|
||||
@ -4560,6 +4896,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.51.0"
|
||||
@ -4572,9 +4918,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.24.6"
|
||||
version = "0.24.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64a70547e8f9d85da0f5af609143f7bde3ac7457a6e1073104d9b73d6c5ac744"
|
||||
checksum = "00711278ed357350d44c749c286786ecac644e044e4da410d466212152383b45"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"block",
|
||||
@ -4586,9 +4932,9 @@ dependencies = [
|
||||
"gio",
|
||||
"glib",
|
||||
"gtk",
|
||||
"html5ever 0.25.2",
|
||||
"html5ever",
|
||||
"http",
|
||||
"kuchiki",
|
||||
"kuchikiki",
|
||||
"libc",
|
||||
"log",
|
||||
"objc",
|
||||
|
@ -17,10 +17,10 @@ strip = true # 删除调试符号
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
tauri-build = { version = "1.5.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.6.1", features = [ "os-all", "process-all", "notification-all", "macos-private-api", "fs-all", "window-all", "system-tray", "shell-open", "icon-png", "icon-ico"] }
|
||||
tauri = { version = "1.6.8", features = [ "http-all", "os-all", "process-all", "notification-all", "macos-private-api", "fs-all", "window-all", "system-tray", "shell-open", "icon-png", "icon-ico"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
window-shadows = "0.2.2" # windows 阴影和圆角
|
||||
|
@ -8,13 +8,13 @@ pub fn set_window_attribute<R: Runtime>(app: &tauri::App<R>) {
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
set_shadow(&window, true).unwrap();
|
||||
|
||||
// 设置窗口的磨砂背景
|
||||
#[cfg(target_os = "macos")]
|
||||
window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
|
||||
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
|
||||
.expect("Unsupported platform! 'apply_blur' is only supported on Windows");
|
||||
// // 设置窗口的磨砂背景
|
||||
// #[cfg(target_os = "macos")]
|
||||
// window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
|
||||
// .expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
||||
//
|
||||
// #[cfg(target_os = "windows")]
|
||||
// window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
|
||||
// .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
|
||||
}
|
||||
}
|
@ -32,7 +32,9 @@
|
||||
"open": true
|
||||
},
|
||||
"http": {
|
||||
"scope": ["http://**", "https://**"]
|
||||
"all": true,
|
||||
"request": true,
|
||||
"scope": ["http://127.0.0.1:9190/*", "https://**"]
|
||||
},
|
||||
"fs": {
|
||||
"all": true,
|
||||
|
1
src/assets/img/loading-one.svg
Normal file
1
src/assets/img/loading-one.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24"><circle cx="12" cy="2" r="0" fill="#666666"><animate attributeName="r" begin="0" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#666666" transform="rotate(45 12 12)"><animate attributeName="r" begin="0.125s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#666666" transform="rotate(90 12 12)"><animate attributeName="r" begin="0.25s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#666666" transform="rotate(135 12 12)"><animate attributeName="r" begin="0.375s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#666666" transform="rotate(180 12 12)"><animate attributeName="r" begin="0.5s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#666666" transform="rotate(225 12 12)"><animate attributeName="r" begin="0.625s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#666666" transform="rotate(270 12 12)"><animate attributeName="r" begin="0.75s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#666666" transform="rotate(315 12 12)"><animate attributeName="r" begin="0.875s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle></svg>
|
After Width: | Height: | Size: 2.0 KiB |
2
src/common/group.ts
Normal file
2
src/common/group.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// 最大管理员数量
|
||||
export const MAX_ADMIN_COUNT = 3
|
12
src/common/message.ts
Normal file
12
src/common/message.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { MsgEnum } from '@/enums'
|
||||
|
||||
// 消息回复映射表
|
||||
export const MSG_REPLY_TEXT_MAP: Record<number, string> = {
|
||||
[MsgEnum.UNKNOWN]: '[未知]',
|
||||
[MsgEnum.RECALL]: '[撤回消息]',
|
||||
[MsgEnum.IMAGE]: '[图片]',
|
||||
[MsgEnum.FILE]: '[文件]',
|
||||
[MsgEnum.VOICE]: '[语音]',
|
||||
[MsgEnum.VIDEO]: '[音频]',
|
||||
[MsgEnum.EMOJI]: '[表情]'
|
||||
}
|
@ -18,9 +18,14 @@
|
||||
align="center"
|
||||
justify="space-between"
|
||||
class="emoji-list">
|
||||
<n-flex :size="0" align="center" justify="center" class="emoji-item" @click="handleReplyEmoji(item)">
|
||||
{{ item.label }}
|
||||
</n-flex>
|
||||
<n-popover trigger="hover" :show-arrow="false" placement="top">
|
||||
<template #trigger>
|
||||
<n-flex :size="0" align="center" justify="center" class="emoji-item" @click="handleReplyEmoji(item)">
|
||||
{{ item.label }}
|
||||
</n-flex>
|
||||
</template>
|
||||
<span>{{ item.title }}</span>
|
||||
</n-popover>
|
||||
</n-flex>
|
||||
</div>
|
||||
<!-- 普通右键菜单 -->
|
||||
|
@ -22,6 +22,15 @@
|
||||
<span class="text-[--info-text-color]">所在地</span>
|
||||
<span>中国</span>
|
||||
</n-flex>
|
||||
<!-- 获得的徽章 -->
|
||||
<n-flex v-if="isCurrentUser.itemIds && isCurrentUser.itemIds.length > 0" :size="26" class="select-none">
|
||||
<span class="text-[--info-text-color]">获得的徽章</span>
|
||||
<n-flex>
|
||||
<template v-for="id in isCurrentUser.itemIds" :key="id">
|
||||
<img class="size-38px" :src="useBadgeInfo(id).value.img" alt="" />
|
||||
</template>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
<!-- 动态 -->
|
||||
<n-flex :size="40" class="select-none">
|
||||
<span class="text-[--info-text-color]">动态</span>
|
||||
@ -45,24 +54,19 @@
|
||||
|
||||
<!-- 背景 -->
|
||||
<img
|
||||
class="size-full rounded-8px box-border p-20px absolute top-0 left-0"
|
||||
style="filter: blur(28px); opacity: 0.8"
|
||||
class="size-full rounded-8px box-border p-20px absolute top-0 left-0 blur-xl opacity-80"
|
||||
:src="isCurrentUser.avatar"
|
||||
alt="" />
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useBadgeInfo, useUserInfo } from '@/hooks/useCached.ts'
|
||||
|
||||
const { info } = defineProps<{
|
||||
info?: MockItem
|
||||
const { uid } = defineProps<{
|
||||
uid: number
|
||||
}>()
|
||||
const settingStore = setting()
|
||||
const { login } = storeToRefs(settingStore)
|
||||
const isCurrentUser = computed(() => (info ? info : login.value.accountInfo))
|
||||
const isCurrentUser = computed(() => useUserInfo(uid).value)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -8,8 +8,10 @@
|
||||
<n-dialog-provider>
|
||||
<n-notification-provider :max="notificMax">
|
||||
<n-message-provider :max="messageMax">
|
||||
<slot></slot>
|
||||
<naive-provider-content />
|
||||
<n-modal-provider>
|
||||
<slot></slot>
|
||||
<naive-provider-content />
|
||||
</n-modal-provider>
|
||||
</n-message-provider>
|
||||
</n-notification-provider>
|
||||
</n-dialog-provider>
|
||||
@ -135,6 +137,7 @@ const registerNaiveTools = () => {
|
||||
window.$dialog = useDialog()
|
||||
window.$message = useMessage()
|
||||
window.$notification = useNotification()
|
||||
window.$modal = useModal()
|
||||
}
|
||||
|
||||
const NaiveProviderContent = defineComponent({
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- 好友详情 -->
|
||||
<n-flex v-if="item.type === RoomTypeEnum.SINGLE" vertical align="center" :size="30" class="mt-60px select-none">
|
||||
<n-flex v-if="content.type === RoomTypeEnum.SINGLE" vertical align="center" :size="30" class="mt-60px select-none">
|
||||
<n-image
|
||||
width="146px"
|
||||
height="146px"
|
||||
@ -9,13 +9,23 @@
|
||||
:src="item.avatar"
|
||||
alt="" />
|
||||
|
||||
<span class="text-(20px [--text-color])">{{ item.accountName }}</span>
|
||||
<span class="text-(20px [--text-color])">{{ item.name }}</span>
|
||||
|
||||
<span class="text-(14px #909090)">这个人很高冷,暂时没有留下什么</span>
|
||||
|
||||
<n-flex align="center" justify="space-between" :size="30" class="text-#606060">
|
||||
<span>性别:男</span>
|
||||
<span>电话:13213213213</span>
|
||||
<span>地区:{{ item.locPlace || '未知' }}</span>
|
||||
<n-flex align="center">
|
||||
<span>徽章:</span>
|
||||
<template v-for="badge in item.itemIds" :key="badge">
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<img class="size-34px" :src="useBadgeInfo(badge).value.img" alt="" />
|
||||
</template>
|
||||
<span>{{ useBadgeInfo(badge).value.describe }}</span>
|
||||
</n-popover>
|
||||
</template>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
<!-- 选项按钮 -->
|
||||
<n-config-provider :theme="lightTheme">
|
||||
@ -55,7 +65,7 @@
|
||||
alt="" />
|
||||
|
||||
<n-flex vertical :size="16" justify="space-between" class="text-(14px #909090)">
|
||||
<span class="text-(16px [--text-color])">{{ item.accountName }}</span>
|
||||
<span class="text-(16px [--text-color])">{{ item.name }}</span>
|
||||
<span>群号:1235873897182</span>
|
||||
<span>创建时间:2021-01-01</span>
|
||||
</n-flex>
|
||||
@ -97,20 +107,25 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { MittEnum, RoomTypeEnum } from '@/enums'
|
||||
import { lightTheme } from 'naive-ui'
|
||||
import router from '@/router'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { useMessage } from '@/hooks/useMessage.ts'
|
||||
import { useBadgeInfo, useUserInfo } from '@/hooks/useCached.ts'
|
||||
import apis from '@/services/apis.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
|
||||
const { handleMsgClick } = useMessage()
|
||||
const globalStore = useGlobalStore()
|
||||
const chatStore = useChatStore()
|
||||
const props = defineProps<{
|
||||
content: any[]
|
||||
content: any
|
||||
}>()
|
||||
const { content } = toRefs(props)
|
||||
const item = computed<MockItem>(() => {
|
||||
return content.value[0]
|
||||
const item = computed(() => {
|
||||
return useUserInfo(content.value.uid).value
|
||||
})
|
||||
|
||||
const footerOptions = ref<OPT.Details[]>([
|
||||
@ -120,7 +135,12 @@ const footerOptions = ref<OPT.Details[]>([
|
||||
click: () => {
|
||||
// TODO 需要增加独立窗口功能 (nyh -> 2024-03-25 16:01:23)
|
||||
router.push('/message')
|
||||
handleMsgClick(item.value)
|
||||
apis.sessionDetailWithFriends({ uid: item.value.uid as number }).then((res) => {
|
||||
globalStore.currentSession.roomId = res.data.roomId
|
||||
globalStore.currentSession.type = RoomTypeEnum.SINGLE
|
||||
chatStore.updateSessionLastActiveTime(res.data.roomId, res.data)
|
||||
handleMsgClick(res.data as any)
|
||||
})
|
||||
Mitt.emit(MittEnum.TO_SEND_MSG, { url: 'message' })
|
||||
}
|
||||
},
|
||||
|
@ -15,20 +15,20 @@
|
||||
</ContextMenu>
|
||||
|
||||
<!-- @提及框 -->
|
||||
<div v-if="ait && activeItem.type === RoomTypeEnum.GROUP && filteredList.length > 0" class="ait">
|
||||
<div v-if="ait && activeItem.type === RoomTypeEnum.GROUP && personList.length > 0" class="ait">
|
||||
<n-virtual-list
|
||||
id="image-chat-msgInput"
|
||||
ref="virtualListInst"
|
||||
style="max-height: 180px"
|
||||
:item-size="36"
|
||||
:items="filteredList"
|
||||
:items="personList"
|
||||
v-model:selectedKey="selectedAitKey">
|
||||
<template #default="{ item }">
|
||||
<n-flex
|
||||
@mouseover="() => (selectedAitKey = item.key)"
|
||||
:class="{ active: selectedAitKey === item.key }"
|
||||
@mouseover="() => (selectedAitKey = item.uid)"
|
||||
:class="{ active: selectedAitKey === item.uid }"
|
||||
@click="handleAit(item)"
|
||||
:key="item.key"
|
||||
:key="item.uid"
|
||||
align="center"
|
||||
class="ait-item">
|
||||
<n-avatar
|
||||
@ -42,7 +42,7 @@
|
||||
:intersection-observer-options="{
|
||||
root: '#image-chat-msgInput'
|
||||
}" />
|
||||
<span> {{ item.accountName }}</span>
|
||||
<span> {{ item.name }}</span>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
@ -80,7 +80,7 @@
|
||||
import { lightTheme, darkTheme, VirtualListInst } from 'naive-ui'
|
||||
import { MittEnum, RoomTypeEnum, ThemeEnum } from '@/enums'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { CacheUserItem, MockItem } from '@/services/types.ts'
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@ -93,14 +93,14 @@ const settingStore = setting()
|
||||
const { themes } = storeToRefs(settingStore)
|
||||
/** 发送按钮旁的箭头 */
|
||||
const arrow = ref(false)
|
||||
// 输入框dom元素
|
||||
/** 输入框dom元素 */
|
||||
const messageInputDom = ref()
|
||||
const activeItem = ref(inject('activeItem') as MockItem)
|
||||
/** 虚拟列表 */
|
||||
const virtualListInst = ref<VirtualListInst>()
|
||||
const { handlePaste } = useCommon()
|
||||
/** 引入useMsgInput的相关方法 */
|
||||
const { inputKeyDown, handleAit, handleInput, send, filteredList, ait, msgInput, chatKey, menuList, selectedAitKey } =
|
||||
const { inputKeyDown, handleAit, handleInput, send, personList, ait, msgInput, chatKey, menuList, selectedAitKey } =
|
||||
useMsgInput(messageInputDom)
|
||||
|
||||
/** 当切换聊天对象时,重新获取焦点 */
|
||||
@ -112,19 +112,19 @@ watch(activeItem, () => {
|
||||
})
|
||||
|
||||
/** 当ait人员列表发生变化的时候始终select第一个 */
|
||||
watch(filteredList, (newList) => {
|
||||
watch(personList, (newList) => {
|
||||
if (newList.length > 0) {
|
||||
/** 先设置滚动条滚动到第一个 */
|
||||
virtualListInst.value?.scrollTo({ key: newList[0].key })
|
||||
selectedAitKey.value = newList[0].key
|
||||
virtualListInst.value?.scrollTo({ key: newList[0].uid })
|
||||
selectedAitKey.value = newList[0].uid
|
||||
}
|
||||
})
|
||||
|
||||
/** 处理键盘上下键切换提及项 */
|
||||
const handleAitKeyChange = (direction: 1 | -1) => {
|
||||
const currentIndex = filteredList.value.findIndex((item) => item.key === selectedAitKey.value)
|
||||
const newIndex = Math.max(0, Math.min(currentIndex + direction, filteredList.value.length - 1))
|
||||
selectedAitKey.value = filteredList.value[newIndex].key
|
||||
const currentIndex = personList.value.findIndex((item) => item.uid === selectedAitKey.value)
|
||||
const newIndex = Math.max(0, Math.min(currentIndex + direction, personList.value.length - 1))
|
||||
selectedAitKey.value = personList.value[newIndex].uid
|
||||
// 获取新选中项在列表中的索引,并滚动到该位置(使用key来进行定位)
|
||||
virtualListInst.value?.scrollTo({ key: selectedAitKey.value })
|
||||
}
|
||||
@ -140,7 +140,7 @@ onMounted(() => {
|
||||
onKeyStroke('Enter', (e) => {
|
||||
if (ait.value && selectedAitKey.value > -1) {
|
||||
e.preventDefault()
|
||||
const item = filteredList.value.find((item) => item.key === selectedAitKey.value) as MockItem
|
||||
const item = personList.value.find((item) => item.uid === selectedAitKey.value) as CacheUserItem
|
||||
handleAit(item)
|
||||
}
|
||||
})
|
||||
|
@ -3,7 +3,12 @@
|
||||
<main
|
||||
style="box-shadow: 0 4px 4px var(--box-shadow-color)"
|
||||
class="relative z-30 flex-y-center justify-between p-[8px_20px_12px] select-none">
|
||||
<div class="color-[--text-color]">{{ activeItem.accountName }}</div>
|
||||
<n-flex align="center">
|
||||
<span class="color-[--text-color]">{{ activeItem.name }}</span>
|
||||
<svg v-if="activeItem.hot_Flag === IsAllUserEnum.Yes" class="size-20px color-#13987f select-none outline-none">
|
||||
<use href="#auth"></use>
|
||||
</svg>
|
||||
</n-flex>
|
||||
<!-- 顶部右边选项栏 -->
|
||||
<nav class="options flex-y-center gap-20px color-[--icon-color]">
|
||||
<div class="options-box">
|
||||
@ -114,7 +119,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { IsAllUserEnum, SessionItem } from '@/services/types.ts'
|
||||
import { useDisplayMedia } from '@vueuse/core'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
@ -129,7 +134,7 @@ const modalShow = ref(false)
|
||||
const sidebarShow = ref(false)
|
||||
const masking = ref(false)
|
||||
const { activeItem } = defineProps<{
|
||||
activeItem: MockItem
|
||||
activeItem: SessionItem
|
||||
}>()
|
||||
|
||||
// 创建一个RTCPeerConnection实例
|
||||
|
@ -1,4 +1,12 @@
|
||||
<template>
|
||||
<div class="absolute-x-center top-20px" v-if="!messageOptions?.isLoading && chatMessageList?.length === 0">
|
||||
<span v-if="chatStore.isGroup" class="text-(14px #909090)">暂无消息,快来发送第一条消息吧~</span>
|
||||
<span v-else class="text-(14px #909090)">你们已成功添加为好友,现在可以开始聊天了!</span>
|
||||
</div>
|
||||
<n-flex justify="center" class="absolute-x-center top-10px" v-if="messageOptions?.isLoading">
|
||||
<img class="size-16px" src="@/assets/img/loading-one.svg" alt="" />
|
||||
<span class="text-(14px #909090)">加载中</span>
|
||||
</n-flex>
|
||||
<!-- 中间聊天内容(使用虚拟列表) -->
|
||||
<n-virtual-list
|
||||
id="image-chat-main"
|
||||
@ -7,35 +15,73 @@
|
||||
style="max-height: calc(100vh - 260px)"
|
||||
:class="{ 'right-1px': activeItem.type === RoomTypeEnum.SINGLE }"
|
||||
class="relative h-100vh"
|
||||
item-resizable
|
||||
:ignore-item-resize="true"
|
||||
:padding-top="10"
|
||||
:item-size="itemSize"
|
||||
:items="items">
|
||||
<template #default="{ item }">
|
||||
<main
|
||||
:key="item.key"
|
||||
class="flex-y-center min-h-58px"
|
||||
:items="chatMessageList">
|
||||
<template #default="{ item, index }">
|
||||
<n-flex
|
||||
vertical
|
||||
:key="index"
|
||||
class="flex-y-center"
|
||||
:class="[
|
||||
[isGroup ? 'p-[18px_20px]' : 'chat-single p-[4px_20px_10px_20px]'],
|
||||
{ 'active-reply': activeReply === item.key }
|
||||
item.message.type === MsgEnum.RECALL ? 'min-h-22px' : 'min-h-62px',
|
||||
chatStore.isGroup ? 'p-[14px_20px]' : 'chat-single p-[4px_20px_10px_20px]',
|
||||
{ 'active-reply': activeReply === item.message.id }
|
||||
]">
|
||||
<!-- 信息间隔时间 -->
|
||||
<span class="text-(12px #909090) select-none bg-[--time-color] p-4px rounded-6px" v-if="item.timeBlock">
|
||||
{{ item.timeBlock }}
|
||||
</span>
|
||||
<!-- 消息为撤回消息 -->
|
||||
<div v-if="item.message.type === MsgEnum.RECALL">
|
||||
<template v-if="chatStore.isGroup">
|
||||
<span v-if="item.fromUser.uid === userUid" class="text-12px color-#909090 select-none">
|
||||
你撤回了一条消息
|
||||
</span>
|
||||
<span v-else class="text-12px color-#909090 select-none" v-html="item.message.body"></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-12px color-#909090 select-none">
|
||||
{{ item.fromUser.uid === userUid ? '你撤回了一条消息' : '对方撤回了一条消息' }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 好友或者群聊的信息 -->
|
||||
<article
|
||||
class="flex flex-col w-full gap-18px"
|
||||
:class="{
|
||||
'items-end': item.accountId === userId
|
||||
}">
|
||||
<div class="flex items-start flex-1" :class="item.accountId === userId ? 'flex-row-reverse' : ''">
|
||||
v-else
|
||||
class="flex flex-col w-full"
|
||||
:class="[{ 'items-end': item.fromUser.uid === userUid }, chatStore.isGroup ? 'gap-18px' : 'gap-2px']">
|
||||
<!-- 信息时间(单聊) -->
|
||||
<div
|
||||
v-if="!chatStore.isGroup"
|
||||
class="text-(12px #909090) h-12px w-fit select-none"
|
||||
:class="
|
||||
item.fromUser.uid === userUid
|
||||
? activeReply === item.message.id
|
||||
? 'pr-68px'
|
||||
: 'pr-42px'
|
||||
: activeReply === item.message.id
|
||||
? 'pl-68px'
|
||||
: 'pl-42px'
|
||||
">
|
||||
<Transition name="fade">
|
||||
<span v-if="hoverBubble.key === item.message.id">
|
||||
{{ formatTimestamp(item.message.sendTime, true) }}
|
||||
</span>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="flex items-start flex-1" :class="item.fromUser.uid === userUid ? 'flex-row-reverse' : ''">
|
||||
<!-- 回复消息提示的箭头 -->
|
||||
<svg
|
||||
v-if="activeReply === item.key"
|
||||
v-if="activeReply === item.message.id"
|
||||
class="size-16px pt-4px color-#909090"
|
||||
:class="item.accountId === userId ? 'ml-8px' : 'mr-8px'">
|
||||
<use :href="item.accountId === userId ? `#corner-down-left` : `#corner-down-right`"></use>
|
||||
:class="item.fromUser.uid === userUid ? 'ml-8px' : 'mr-8px'">
|
||||
<use :href="item.fromUser.uid === userUid ? `#corner-down-left` : `#corner-down-right`"></use>
|
||||
</svg>
|
||||
<!-- 头像 -->
|
||||
<n-popover
|
||||
@update:show="handlePopoverUpdate(item.key)"
|
||||
@update:show="handlePopoverUpdate(item.message.id)"
|
||||
trigger="click"
|
||||
placement="right"
|
||||
:show-arrow="false"
|
||||
@ -44,45 +90,94 @@
|
||||
<template #trigger>
|
||||
<ContextMenu
|
||||
@select="$event.click(item, 'Main')"
|
||||
:menu="isGroup ? optionsList : void 0"
|
||||
:menu="chatStore.isGroup ? optionsList : void 0"
|
||||
:special-menu="report">
|
||||
<n-avatar
|
||||
lazy
|
||||
round
|
||||
:color="'#fff'"
|
||||
:size="34"
|
||||
@click="selectKey = item.key"
|
||||
@click="selectKey = item.message.id"
|
||||
class="select-none"
|
||||
:src="item.accountId === userId ? item.avatar : activeItem.avatar"
|
||||
:class="item.accountId === userId ? '' : 'mr-10px'"
|
||||
fallback-src="/logo.png"
|
||||
:render-placeholder="() => null"
|
||||
:intersection-observer-options="{
|
||||
root: '#image-chat-main'
|
||||
}"></n-avatar>
|
||||
:src="
|
||||
item.fromUser.uid === userUid
|
||||
? login.accountInfo.avatar
|
||||
: useUserInfo(item.fromUser.uid).value.avatar
|
||||
"
|
||||
:class="item.fromUser.uid === userUid ? '' : 'mr-10px'"></n-avatar>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<!-- 用户个人信息框 -->
|
||||
<InfoPopover v-if="selectKey === item.key" :info="item.accountId !== userId ? activeItemRef : void 0" />
|
||||
<InfoPopover v-if="selectKey === item.message.id" :uid="item.fromUser.uid" />
|
||||
</n-popover>
|
||||
<n-flex
|
||||
vertical
|
||||
justify="center"
|
||||
:size="8"
|
||||
:size="6"
|
||||
class="color-[--text-color] flex-1"
|
||||
:class="item.accountId === userId ? 'items-end mr-10px' : ''">
|
||||
<ContextMenu @select="$event.click(item)" :menu="isGroup ? optionsList : []" :special-menu="report">
|
||||
<span class="text-12px select-none color-#909090" v-if="isGroup">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</ContextMenu>
|
||||
:class="item.fromUser.uid === userUid ? 'items-end mr-10px' : ''">
|
||||
<n-flex
|
||||
:size="4"
|
||||
align="center"
|
||||
:style="item.fromUser.uid === userUid ? 'flex-direction: row-reverse' : ''">
|
||||
<ContextMenu
|
||||
@select="$event.click(item)"
|
||||
:menu="chatStore.isGroup ? optionsList : []"
|
||||
:special-menu="report">
|
||||
<n-flex
|
||||
:size="6"
|
||||
class="select-none"
|
||||
align="center"
|
||||
v-if="chatStore.isGroup"
|
||||
:style="item.fromUser.uid === userUid ? 'flex-direction: row-reverse' : ''">
|
||||
<!-- 用户徽章 -->
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<img
|
||||
v-if="useBadgeInfo(useUserInfo(item.fromUser.uid).value.wearingItemId).value.img"
|
||||
class="size-18px"
|
||||
:src="useBadgeInfo(useUserInfo(item.fromUser.uid).value.wearingItemId).value.img"
|
||||
alt="badge" />
|
||||
</template>
|
||||
<span>{{ useBadgeInfo(useUserInfo(item.fromUser.uid).value.wearingItemId).value.describe }}</span>
|
||||
</n-popover>
|
||||
<!-- 用户名 -->
|
||||
<span class="text-12px select-none color-#909090 inline-block align-top">
|
||||
{{ useUserInfo(item.fromUser.uid).value.name }}
|
||||
</span>
|
||||
<!-- 消息归属地 -->
|
||||
<span class="text-(12px #909090)">
|
||||
({{ useUserInfo(item.fromUser.uid).value.locPlace || '未知' }})
|
||||
</span>
|
||||
</n-flex>
|
||||
</ContextMenu>
|
||||
<!-- 群主 -->
|
||||
<div
|
||||
v-if="chatStore.isGroup && item.message.id === 1"
|
||||
class="flex p-4px rounded-4px bg-#f5dadf size-fit select-none">
|
||||
<span class="text-(10px #d5304f)">群主</span>
|
||||
</div>
|
||||
<!-- 管理员 -->
|
||||
<div
|
||||
v-if="chatStore.isGroup && item.message.id === 2"
|
||||
class="flex p-4px rounded-4px bg-#13987F66 size-fit select-none">
|
||||
<span class="text-(10px #13987f)">管理员</span>
|
||||
</div>
|
||||
<!-- 信息时间(群聊) -->
|
||||
<Transition name="fade">
|
||||
<span v-if="chatStore.isGroup && hoverBubble.key === item.message.id" class="text-(12px #909090)">
|
||||
{{ formatTimestamp(item.message.sendTime, true) }}
|
||||
</span>
|
||||
</Transition>
|
||||
</n-flex>
|
||||
<!-- 气泡样式 -->
|
||||
<ContextMenu
|
||||
class="size-fit"
|
||||
:data-key="item.accountId === userId ? `U${item.key}` : `Q${item.key}`"
|
||||
@mouseenter="handleMouseEnter(item.message.id)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
class="w-fit"
|
||||
:data-key="item.fromUser.uid === userUid ? `U${item.message.id}` : `Q${item.message.id}`"
|
||||
@select="$event.click(item)"
|
||||
:menu="handleItemType(item.type)"
|
||||
:emoji="isGroup ? emojiList : []"
|
||||
:menu="handleItemType(item.message.type)"
|
||||
:emoji="chatStore.isGroup ? emojiList : []"
|
||||
:special-menu="specialMenuList"
|
||||
@reply-emoji="handleEmojiSelect($event.label, item)"
|
||||
@click="handleMsgClick(item)">
|
||||
@ -90,13 +185,13 @@
|
||||
<!-- <RenderMessage :message="message" />-->
|
||||
<!-- 消息为文本类型或者回复消息 -->
|
||||
<div
|
||||
v-if="item.type === MsgEnum.TEXT || item.type === MsgEnum.REPLY"
|
||||
v-if="item.message.type === MsgEnum.TEXT"
|
||||
style="white-space: pre-wrap"
|
||||
:class="[
|
||||
{ active: activeBubble === item.key },
|
||||
item.accountId === userId ? 'bubble-oneself' : 'bubble'
|
||||
{ active: activeBubble === item.message.id },
|
||||
item.fromUser.uid === userUid ? 'bubble-oneself' : 'bubble'
|
||||
]">
|
||||
<span v-html="item.content"></span>
|
||||
<span v-html="item.message.body.content"></span>
|
||||
</div>
|
||||
|
||||
<!-- 消息为为图片类型(不固定宽度和高度), 多张图片时渲染 -->
|
||||
@ -106,7 +201,7 @@
|
||||
class="select-none"
|
||||
v-for="(src, index) in item.content"
|
||||
:key="index"
|
||||
:img-props="{ style: { maxWidth: '325px', maxHeight: '165px' } }"
|
||||
:img-props="{ style: { maxWidth: '325px', maxHeight: '165px', width: '100%', height: 'auto' } }"
|
||||
show-toolbar-tooltip
|
||||
style="border-radius: 8px"
|
||||
:fallback-src="'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'"
|
||||
@ -118,7 +213,7 @@
|
||||
<n-image
|
||||
class="select-none"
|
||||
v-if="typeof item.content === 'string' && item.type === MsgEnum.IMAGE"
|
||||
:img-props="{ style: { maxWidth: '325px', maxHeight: '165px' } }"
|
||||
:img-props="{ style: { maxWidth: '325px', maxHeight: '165px', width: '100%', height: 'auto' } }"
|
||||
show-toolbar-tooltip
|
||||
style="border-radius: 8px"
|
||||
:fallback-src="'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'"
|
||||
@ -139,32 +234,32 @@
|
||||
<n-flex
|
||||
align="center"
|
||||
:size="6"
|
||||
v-if="item.reply && item.type === MsgEnum.REPLY"
|
||||
@click="jumpToReplyMsg(item.reply.key)"
|
||||
class="reply-bubble relative">
|
||||
v-if="item.message.body.reply"
|
||||
@click="jumpToReplyMsg(item.message.body.reply.id)"
|
||||
class="reply-bubble relative w-fit">
|
||||
<svg class="size-14px"><use href="#to-top"></use></svg>
|
||||
<span>{{ `${item.reply.accountName}:` }}</span>
|
||||
<span>{{ `${item.message.body.reply.username}:` }}</span>
|
||||
<!-- 当回复消息为图片时渲染 -->
|
||||
<n-image
|
||||
v-if="item.reply.content.startsWith('data:image/')"
|
||||
:img-props="{ style: { maxWidth: '50px', maxHeight: '50px' } }"
|
||||
show-toolbar-tooltip
|
||||
style="border-radius: 4px"
|
||||
@click.stop
|
||||
:fallback-src="'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'"
|
||||
:src="item.reply.content" />
|
||||
<!-- <n-image-->
|
||||
<!-- v-if="item.reply.content.startsWith('data:image/')"-->
|
||||
<!-- :img-props="{ style: { maxWidth: '50px', maxHeight: '50px' } }"-->
|
||||
<!-- show-toolbar-tooltip-->
|
||||
<!-- style="border-radius: 4px"-->
|
||||
<!-- @click.stop-->
|
||||
<!-- :fallback-src="'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'"-->
|
||||
<!-- :src="item.reply.content" />-->
|
||||
<!-- 当回复消息为文本时渲染(判断是否有aitSpan标签) -->
|
||||
<span v-else class="content-span">
|
||||
{{ handleReply(item.reply.content) }}
|
||||
<span class="content-span">
|
||||
{{ handleReply(item.message.body.reply.body) }}
|
||||
</span>
|
||||
<!-- 多个图片时计数器样式 -->
|
||||
<div v-if="item.reply.imgCount" class="reply-img-sub">
|
||||
{{ item.reply.imgCount }}
|
||||
</div>
|
||||
<!-- <!– 多个图片时计数器样式 –>-->
|
||||
<!-- <div v-if="item.reply.imgCount" class="reply-img-sub">-->
|
||||
<!-- {{ item.reply.imgCount }}-->
|
||||
<!-- </div>-->
|
||||
</n-flex>
|
||||
|
||||
<!-- 群聊回复emoji表情 -->
|
||||
<n-flex :size="4" v-if="isGroup && item.emojiList">
|
||||
<n-flex :size="4" v-if="chatStore.isGroup && item.emojiList">
|
||||
<n-flex
|
||||
:size="2"
|
||||
align="center"
|
||||
@ -179,7 +274,7 @@
|
||||
</n-flex>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
|
||||
@ -201,19 +296,22 @@
|
||||
</n-modal>
|
||||
|
||||
<!-- 悬浮按钮提示(头部悬浮) // TODO 要结合已读未读功能来判断之前的信息有多少没有读,当现在的距离没有到最底部并且又有新消息来未读的时候显示下标的更多信息 (nyh -> 2024-03-07 01:27:22)-->
|
||||
<header class="float-header" :class="isGroup ? 'right-220px' : 'right-50px'">
|
||||
<div class="float-box">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-icon :color="'#13987f'">
|
||||
<svg><use href="#double-up"></use></svg>
|
||||
</n-icon>
|
||||
<span class="text-12px">xx条新信息</span>
|
||||
</n-flex>
|
||||
</div>
|
||||
</header>
|
||||
<!-- <header class="float-header" :class="isGroup ? 'right-220px' : 'right-50px'">-->
|
||||
<!-- <div class="float-box">-->
|
||||
<!-- <n-flex justify="space-between" align="center">-->
|
||||
<!-- <n-icon :color="'#13987f'">-->
|
||||
<!-- <svg><use href="#double-up"></use></svg>-->
|
||||
<!-- </n-icon>-->
|
||||
<!-- <span class="text-12px">xx条新信息</span>-->
|
||||
<!-- </n-flex>-->
|
||||
<!-- </div>-->
|
||||
<!-- </header>-->
|
||||
|
||||
<!-- 悬浮按钮提示(底部悬浮) -->
|
||||
<footer class="float-footer" v-if="floatFooter && newMsgNum > 0" :class="isGroup ? 'right-220px' : 'right-50px'">
|
||||
<footer
|
||||
class="float-footer"
|
||||
v-if="floatFooter && newMsgNum > 0"
|
||||
:class="chatStore.isGroup ? 'right-220px' : 'right-50px'">
|
||||
<div class="float-box" :class="{ max: newMsgNum > 99 }" @click="scrollBottom">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-icon :color="newMsgNum > 99 ? '#ce304f' : '#13987f'">
|
||||
@ -228,7 +326,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { EventEnum, MittEnum, MsgEnum, RoomTypeEnum } from '@/enums'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { type MessageType, SessionItem } from '@/services/types.ts'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { usePopover } from '@/hooks/usePopover.ts'
|
||||
@ -240,75 +338,100 @@ import { delay } from 'lodash-es'
|
||||
import { useCommon } from '@/hooks/useCommon.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { formatTimestamp } from '@/utils/ComputedTime.ts'
|
||||
import { useUserInfo, useBadgeInfo } from '@/hooks/useCached.ts'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
|
||||
const { activeItem } = defineProps<{
|
||||
activeItem: MockItem
|
||||
activeItem: SessionItem
|
||||
}>()
|
||||
const activeItemRef = ref({ ...activeItem })
|
||||
const activeItemRef = ref<SessionItem>({ ...activeItem })
|
||||
const settingStore = setting()
|
||||
const chatStore = useChatStore()
|
||||
// const userInfo = useUserStore()?.userInfo
|
||||
/** 消息列表 */
|
||||
const chatMessageList = computed(() => chatStore.chatMessageList)
|
||||
const messageOptions = computed(() => chatStore.currentMessageOptions)
|
||||
const { login } = storeToRefs(settingStore)
|
||||
const { createWebviewWindow } = useWindow()
|
||||
/** 是否是超级管理员 */
|
||||
// const isAdmin = computed(() => userInfo?.power === PowerEnum.ADMIN)
|
||||
/** 跳转回复消息后选中效果 */
|
||||
const activeReply = ref(-1)
|
||||
/** 当前信息是否是群聊信息 */
|
||||
const isGroup = computed(() => activeItem.type === RoomTypeEnum.GROUP)
|
||||
/** item最小高度,用于计算滚动大小和位置 */
|
||||
const itemSize = computed(() => (isGroup.value ? 98 : 70))
|
||||
const itemSize = computed(() => (chatStore.isGroup ? 90 : 76))
|
||||
/** 虚拟列表 */
|
||||
const virtualListInst = ref<VirtualListInst>()
|
||||
/** 手动触发Popover显示 */
|
||||
const infoPopover = ref(false)
|
||||
const { removeTag } = useCommon()
|
||||
/** 鼠标悬浮的气泡显示对应的时间 */
|
||||
const hoverBubble = ref<{
|
||||
key: number
|
||||
timer?: NodeJS.Timeout
|
||||
}>({
|
||||
key: -1
|
||||
})
|
||||
const { removeTag, userUid } = useCommon()
|
||||
const {
|
||||
handleScroll,
|
||||
handleMsgClick,
|
||||
handleConfirm,
|
||||
handleItemType,
|
||||
items,
|
||||
activeBubble,
|
||||
newMsgNum,
|
||||
floatFooter,
|
||||
historyIndex,
|
||||
tips,
|
||||
modalShow,
|
||||
userId,
|
||||
specialMenuList,
|
||||
itemComputed,
|
||||
optionsList,
|
||||
report,
|
||||
selectKey,
|
||||
emojiList
|
||||
emojiList,
|
||||
scrollTop
|
||||
} = useChatMain(activeItem)
|
||||
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-main')
|
||||
// // 创建一个符合 TextBody 类型的对象
|
||||
// const textBody = {
|
||||
// content: '123',
|
||||
// reply: {
|
||||
// /** 填充合适的回复对象 */
|
||||
// },
|
||||
// urlContentMap: {
|
||||
// // 填充合适的 URL 映射信息,如果没有,则可以是空对象
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 使用 ref 创建一个响应式的 MsgType 对象
|
||||
// const msg = ref({
|
||||
// id: 123456, // 假设的消息 ID
|
||||
// roomId: 123, // 假设的房间 ID
|
||||
// type: MsgEnum.TEXT, // 假设消息类型为文本
|
||||
// body: textBody, // 上面创建的 TextBody 对象
|
||||
// sendTime: Date.now(), // 发送消息的时间戳
|
||||
// messageMark: {
|
||||
// /** 填充合适的 MessageMarkType 对象 */
|
||||
// }
|
||||
// })
|
||||
// const message = computed(() => msg.value)
|
||||
|
||||
watchEffect(() => {
|
||||
newMsgNum.value = itemComputed.value - historyIndex.value
|
||||
activeItemRef.value = { ...activeItem }
|
||||
watch(activeItemRef, (value, oldValue) => {
|
||||
if (oldValue.roomId !== value.roomId) {
|
||||
nextTick(() => {
|
||||
console.log('切换房间')
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', debounce: true })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(chatMessageList, (value, oldValue) => {
|
||||
if (scrollTop.value === 0 && value.length > 20) {
|
||||
nextTick(() => {
|
||||
// 跳转的下标
|
||||
virtualListInst.value?.scrollTo({ index: value.length - oldValue.length, debounce: true })
|
||||
})
|
||||
} else {
|
||||
nextTick(() => {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', debounce: true })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 当鼠标进入时触发的处理函数
|
||||
const handleMouseEnter = (key: any) => {
|
||||
// 设置定时器,在1600毫秒后更新悬浮气泡的key值
|
||||
hoverBubble.value.timer = setTimeout(() => {
|
||||
hoverBubble.value.key = key
|
||||
}, 1600)
|
||||
}
|
||||
|
||||
// 当鼠标离开时触发的处理函数
|
||||
const handleMouseLeave = () => {
|
||||
// 如果定时器存在,则清除定时器并重置为undefined
|
||||
if (hoverBubble.value.timer) {
|
||||
clearTimeout(hoverBubble.value.timer)
|
||||
hoverBubble.value.timer = void 0
|
||||
}
|
||||
// 重置悬浮气泡的key值为-1
|
||||
hoverBubble.value.key = -1
|
||||
}
|
||||
|
||||
/** 取消回复emoji表情 */
|
||||
const cancelReplyEmoji = (item: any, index: number) => {
|
||||
// 判断item.emojiList数组中的count是否为1,如果为1则删除该元素,否则count-1
|
||||
@ -339,35 +462,6 @@ const handleReply = (content: string) => {
|
||||
return content.includes('id="aitSpan"') ? removeTag(content) : content
|
||||
}
|
||||
|
||||
/** 发送信息 */
|
||||
const handleSendMessage = (msg: any) => {
|
||||
nextTick(() => {
|
||||
// 检查是否为图片消息
|
||||
if (msg.type === MsgEnum.IMAGE || msg.type === MsgEnum.FILE) {
|
||||
// 查找所有的img标签并存入数组
|
||||
const imgSrcArray = [...msg.content.matchAll(/<img.*?src="(.*?)"/g)].map((match) => match[1])
|
||||
if (imgSrcArray.length > 1) {
|
||||
// 图片数量大于1,储存整个数组以便后面在模板中使用
|
||||
msg.content = imgSrcArray
|
||||
} else if (imgSrcArray.length === 1) {
|
||||
// 图片数量为1,只存储单张图片的src
|
||||
msg.content = imgSrcArray[0]
|
||||
}
|
||||
}
|
||||
const index = items.value.length > 0 ? items.value[items.value.length - 1].key : 0
|
||||
items.value.push({
|
||||
value: login.value.accountInfo.name,
|
||||
key: index + 1,
|
||||
accountId: login.value.accountInfo.uid,
|
||||
avatar: login.value.accountInfo.avatar,
|
||||
content: msg.content,
|
||||
type: msg.type,
|
||||
reply: msg.type === MsgEnum.REPLY ? msg.reply : null
|
||||
})
|
||||
addToDomUpdateQueue(index, userId.value)
|
||||
})
|
||||
}
|
||||
|
||||
/** 跳转到回复消息 */
|
||||
const jumpToReplyMsg = (key: number) => {
|
||||
nextTick(() => {
|
||||
@ -381,14 +475,14 @@ const jumpToReplyMsg = (key: number) => {
|
||||
* @param index 下标
|
||||
* @param id 用户ID
|
||||
*/
|
||||
const addToDomUpdateQueue = (index: number, id: string) => {
|
||||
const addToDomUpdateQueue = (index: number, id: number) => {
|
||||
// 使用 nextTick 确保虚拟列表渲染完最新的项目后进行滚动
|
||||
nextTick(() => {
|
||||
if (!floatFooter.value || id === userId.value) {
|
||||
if (!floatFooter.value || id === userUid.value) {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', debounce: true })
|
||||
}
|
||||
/** data-key标识的气泡,添加前缀用于区分用户消息,不然气泡动画会被覆盖 */
|
||||
const dataKey = id === userId.value ? `U${index + 1}` : `Q${index + 1}`
|
||||
const dataKey = id === userUid.value ? `U${index}` : `Q${index}`
|
||||
const lastMessageElement = document.querySelector(`[data-key="${dataKey}"]`) as HTMLElement
|
||||
if (lastMessageElement) {
|
||||
// 添加动画类
|
||||
@ -401,6 +495,7 @@ const addToDomUpdateQueue = (index: number, id: string) => {
|
||||
lastMessageElement.addEventListener('animationend', handleAnimationEnd)
|
||||
}
|
||||
})
|
||||
chatStore.clearNewMsgCount()
|
||||
}
|
||||
|
||||
/** 点击后滚动到底部 */
|
||||
@ -432,6 +527,10 @@ const closeMenu = (event: any) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 滚动到底部
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom', debounce: true })
|
||||
})
|
||||
/**! 启动图标闪烁 需要设置"resources": ["sec-tauri/图标放置的文件夹"]*/
|
||||
invoke('tray_blink', {
|
||||
isRun: true,
|
||||
@ -441,8 +540,10 @@ onMounted(() => {
|
||||
}).catch((error) => {
|
||||
console.error('设置图标失败:', error)
|
||||
})
|
||||
Mitt.on(MittEnum.SEND_MESSAGE, (event: any) => {
|
||||
handleSendMessage(event)
|
||||
Mitt.on(MittEnum.SEND_MESSAGE, (event: MessageType) => {
|
||||
nextTick(() => {
|
||||
addToDomUpdateQueue(event.message.id, event.fromUser.uid)
|
||||
})
|
||||
})
|
||||
Mitt.on(`${MittEnum.INFO_POPOVER}-Main`, (event: any) => {
|
||||
selectKey.value = event
|
||||
@ -475,6 +576,9 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(hoverBubble.value.timer)
|
||||
hoverBubble.value.timer = void 0
|
||||
hoverBubble.value.key = -1
|
||||
window.removeEventListener('click', closeMenu, true)
|
||||
})
|
||||
</script>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<!--! 这里最好不要使用n-flex,滚动高度会有问题 -->
|
||||
<main v-if="activeItem.type === RoomTypeEnum.GROUP" style="border-left: 1px solid var(--line-color)" class="item-box">
|
||||
<main v-if="isGroup" style="border-left: 1px solid var(--line-color)" class="item-box">
|
||||
<n-flex v-if="!isSearch" align="center" justify="space-between" class="pr-8px pl-8px h-42px">
|
||||
<span class="text-14px">群聊成员</span>
|
||||
<span class="text-14px">群聊成员 {{ userList.length }}</span>
|
||||
<svg @click="handleSearch" class="size-14px"><use href="#search"></use></svg>
|
||||
</n-flex>
|
||||
<!-- 搜索框 -->
|
||||
@ -30,10 +30,10 @@
|
||||
style="max-height: calc(100vh - 130px)"
|
||||
item-resizable
|
||||
:item-size="42"
|
||||
:items="MockList">
|
||||
:items="userList">
|
||||
<template #default="{ item }">
|
||||
<n-popover
|
||||
@update:show="handlePopoverUpdate(item.key)"
|
||||
@update:show="handlePopoverUpdate(item.uid)"
|
||||
trigger="click"
|
||||
placement="left"
|
||||
:show-arrow="false"
|
||||
@ -41,10 +41,12 @@
|
||||
style="padding: 0; background: var(--bg-info); backdrop-filter: blur(10px)">
|
||||
<template #trigger>
|
||||
<ContextMenu @select="$event.click(item, 'Sidebar')" :menu="optionsList" :special-menu="report">
|
||||
<n-flex @click="selectKey = item.key" :key="item.key" :size="10" align="center" class="item">
|
||||
<n-flex @click="selectKey = item.uid" :key="item.uid" :size="10" align="center" class="item">
|
||||
<n-avatar
|
||||
lazy
|
||||
round
|
||||
class="grayscale"
|
||||
:class="{ 'grayscale-0': item.activeStatus === OnlineEnum.ONLINE }"
|
||||
:color="'#fff'"
|
||||
:size="24"
|
||||
:src="item.avatar"
|
||||
@ -53,35 +55,54 @@
|
||||
:intersection-observer-options="{
|
||||
root: '#image-chat-sidebar'
|
||||
}"></n-avatar>
|
||||
<span class="text-12px">{{ item.accountName }}</span>
|
||||
<span class="text-12px truncate flex-1">{{ item.name }}</span>
|
||||
<div v-if="item.uid === 1" class="flex p-4px rounded-4px bg-#f5dadf size-fit select-none">
|
||||
<span class="text-(10px #d5304f)">群主</span>
|
||||
</div>
|
||||
<div v-if="item.uid === 2" class="flex p-4px rounded-4px bg-#13987F66 size-fit select-none">
|
||||
<span class="text-(10px #13987f)">管理员</span>
|
||||
</div>
|
||||
</n-flex>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<!-- 用户个人信息框 -->
|
||||
<InfoPopover v-if="selectKey === item.key" :info="item" />
|
||||
<InfoPopover v-if="selectKey === item.uid" :uid="item.uid" />
|
||||
</n-popover>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</main>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { MittEnum, RoomTypeEnum } from '@/enums'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { MockList } from '@/mock'
|
||||
import { MittEnum, OnlineEnum, RoomTypeEnum } from '@/enums'
|
||||
import { InputInst } from 'naive-ui'
|
||||
import { usePopover } from '@/hooks/usePopover.ts'
|
||||
import { useChatMain } from '@/hooks/useChatMain.ts'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { useGroupStore } from '@/stores/group.ts'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import type { UserItem } from '@/services/types.ts'
|
||||
|
||||
const { activeItem } = defineProps<{
|
||||
activeItem: MockItem
|
||||
}>()
|
||||
const groupStore = useGroupStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const groupUserList = computed(() => groupStore.userList)
|
||||
const userList = computed(() => {
|
||||
return groupUserList.value.map((item: UserItem) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { uid, ...userInfo } = item // 排除uid,获取剩余内容
|
||||
return {
|
||||
...userInfo,
|
||||
...useUserInfo(item.uid).value
|
||||
}
|
||||
})
|
||||
})
|
||||
const isGroup = computed(() => globalStore.currentSession?.type === RoomTypeEnum.GROUP)
|
||||
const isSearch = ref(false)
|
||||
const searchRef = ref('')
|
||||
/** 手动触发Popover显示 */
|
||||
const infoPopover = ref(false)
|
||||
const inputInstRef = ref<InputInst | null>(null)
|
||||
const { optionsList, report, selectKey } = useChatMain(activeItem)
|
||||
const { optionsList, report, selectKey } = useChatMain()
|
||||
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-sidebar')
|
||||
|
||||
const handleSearch = () => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<!-- 头部 -->
|
||||
<ChatHeader :active-item="activeItemRef" />
|
||||
<ChatHeader :active-item="activeItemRef as any" />
|
||||
<n-flex :size="0" class="h-full">
|
||||
<n-flex vertical :size="0" class="flex-1">
|
||||
<n-flex vertical :size="0" class="flex-1 relative">
|
||||
<!-- 中间聊天框内容 -->
|
||||
<ChatMain :active-item="activeItemRef" />
|
||||
<ChatMain :active-item="activeItemRef as any" />
|
||||
<!-- 输入框和操作列表 -->
|
||||
<ChatFooter class="flex-1" />
|
||||
</n-flex>
|
||||
|
@ -21,9 +21,13 @@ export enum RCodeEnum {
|
||||
/**URL*/
|
||||
export enum URLEnum {
|
||||
/**用户*/
|
||||
USER = '/user',
|
||||
/**文章*/
|
||||
ARTICLE = '/article'
|
||||
USER = '/api/user',
|
||||
/**聊天*/
|
||||
CHAT = '/api/chat',
|
||||
/**房间*/
|
||||
ROOM = '/api/room',
|
||||
/**oss*/
|
||||
OSS = '/api/oss'
|
||||
}
|
||||
|
||||
/** tauri原生跨窗口通信时传输的类型 */
|
||||
@ -83,33 +87,94 @@ export enum StoresEnum {
|
||||
/** 在线状态 */
|
||||
ONLINE_STATUS = 'onlineStatus',
|
||||
/** 历史内容 */
|
||||
HISTORY = 'history'
|
||||
HISTORY = 'history',
|
||||
/** 聊天列表 */
|
||||
CHAT_LIST = 'chatList'
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
* todo: 后续需要补充
|
||||
*/
|
||||
export enum MsgEnum {
|
||||
/** 未知 */
|
||||
UNKNOWN,
|
||||
/** 文本 */
|
||||
TEXT,
|
||||
/** 撤回 */
|
||||
RECALL,
|
||||
/** 图片 */
|
||||
IMAGE,
|
||||
/** 混合 */
|
||||
MIXED,
|
||||
/** 文件 */
|
||||
FILE,
|
||||
/** 语音 */
|
||||
VOICE,
|
||||
/** 视频 */
|
||||
VIDEO,
|
||||
/** 文件 */
|
||||
FILE,
|
||||
/** 表情 */
|
||||
EMOTICON,
|
||||
/** 表情包 */
|
||||
EMOJI,
|
||||
/** 系统消息 */
|
||||
SYSTEM,
|
||||
/** 混合 */
|
||||
MIXED,
|
||||
/** 艾特 */
|
||||
AIT,
|
||||
/** 回复 */
|
||||
REPLY
|
||||
}
|
||||
|
||||
/**
|
||||
* 在线状态
|
||||
*/
|
||||
export enum OnlineEnum {
|
||||
/** 在线 */
|
||||
ONLINE = 1,
|
||||
/** 离线 */
|
||||
OFFLINE
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
*/
|
||||
export enum ActEnum {
|
||||
/** 确认 */
|
||||
Confirm = 1,
|
||||
/** 取消 */
|
||||
Cancel
|
||||
}
|
||||
|
||||
export enum SexEnum {
|
||||
MAN = 1,
|
||||
REMALE
|
||||
}
|
||||
|
||||
export enum PowerEnum {
|
||||
USER,
|
||||
ADMIN
|
||||
}
|
||||
|
||||
export enum IsYetEnum {
|
||||
NO,
|
||||
YES
|
||||
}
|
||||
|
||||
export enum MarkEnum {
|
||||
LIKE = 1,
|
||||
DISLIKE
|
||||
}
|
||||
|
||||
// 成员角色 1群主 2管理员 3普通成员 4踢出群聊
|
||||
export enum RoleEnum {
|
||||
/** 1群主 */
|
||||
LORD = 1,
|
||||
/** 2管理员 */
|
||||
ADMIN,
|
||||
/** 3普通成员 */
|
||||
NORMAL,
|
||||
/** 4踢出群聊 */
|
||||
REMOVED
|
||||
}
|
||||
|
||||
/** 房间类型 1群聊 2单聊 */
|
||||
export enum RoomTypeEnum {
|
||||
/** 1群聊 */
|
||||
@ -118,6 +183,14 @@ export enum RoomTypeEnum {
|
||||
SINGLE = 2
|
||||
}
|
||||
|
||||
/** 变更类型 1 加入群组,2: 移除群组 */
|
||||
export enum ChangeTypeEnum {
|
||||
/** 1 加入群组 */
|
||||
JOIN = 1,
|
||||
/** 2 移除群组 */
|
||||
REMOVE
|
||||
}
|
||||
|
||||
/** 关闭窗口的行为 */
|
||||
export enum CloseBxEnum {
|
||||
/** 隐藏 */
|
||||
@ -147,5 +220,7 @@ export enum WsResEnum {
|
||||
/** 二维码登录 */
|
||||
QRCODE_LOGIN = 'qrcodeLogin',
|
||||
/** 登录成功 */
|
||||
LOGIN_SUCCESS = 'loginSuccess'
|
||||
LOGIN_SUCCESS = 'loginSuccess',
|
||||
/** ws连接错误 */
|
||||
WS_ERROR = 'wsError'
|
||||
}
|
||||
|
34
src/hooks/useCached.ts
Normal file
34
src/hooks/useCached.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { useCachedStore } from '@/stores/cached'
|
||||
|
||||
/**
|
||||
* 统一获取用户信息 hook
|
||||
* @param uid 用户 ID
|
||||
* @description 引入该Hook后,可响应式获取用户信息
|
||||
*/
|
||||
export const useUserInfo = (uid?: number | ComputedRef<number | undefined> | Ref<number>) => {
|
||||
const cachedStore = useCachedStore()
|
||||
const userInfo = computed(() => (uid && cachedStore.userCachedList[toValue(uid as number)]) || {})
|
||||
// 如果没有就请求
|
||||
const resultUid = toValue(uid as number)
|
||||
if (resultUid && Object.keys(userInfo.value).length === 0) {
|
||||
cachedStore.getBatchUserInfo([resultUid])
|
||||
}
|
||||
return userInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一获取用户徽章信息 hook
|
||||
* @param itemId 用户徽章ID
|
||||
* @description 引入该Hook后,可响应式获取用户徽章信息
|
||||
*/
|
||||
export const useBadgeInfo = (itemId?: number | ComputedRef<number | undefined>) => {
|
||||
const cachedStore = useCachedStore()
|
||||
const badgeInfo = computed(() => (itemId && cachedStore.badgeCachedList[toValue(itemId as number)]) || {})
|
||||
// 如果没有就请求
|
||||
const resultItemId = toValue(itemId as number)
|
||||
if (resultItemId && Object.keys(badgeInfo.value).length === 0) {
|
||||
cachedStore.getBatchBadgeInfo([resultItemId])
|
||||
}
|
||||
return badgeInfo
|
||||
}
|
@ -1,18 +1,22 @@
|
||||
import { useCommon } from '@/hooks/useCommon.ts'
|
||||
import { MittEnum, MsgEnum } from '@/enums'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { MessageType, SessionItem } from '@/services/types.ts'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import apis from '@/services/apis.ts'
|
||||
|
||||
export const useChatMain = (activeItem: MockItem) => {
|
||||
const { removeTag } = useCommon()
|
||||
const settingStore = setting()
|
||||
const { login } = storeToRefs(settingStore)
|
||||
export const useChatMain = (activeItem?: SessionItem) => {
|
||||
const { removeTag, userUid } = useCommon()
|
||||
const chatStore = useChatStore()
|
||||
// const userInfo = useUserStore()?.userInfo
|
||||
// const chatMessageList = computed(() => chatStore.chatMessageList)
|
||||
const messageOptions = computed(() => chatStore.currentMessageOptions)
|
||||
/** 滚动条位置 */
|
||||
const scrollTop = ref(-1)
|
||||
/** 是否是超级管理员 */
|
||||
// const isAdmin = computed(() => userInfo?.power === PowerEnum.ADMIN)
|
||||
/** 选中的气泡消息 */
|
||||
const activeBubble = ref(-1)
|
||||
/** 当前登录的用户id */
|
||||
const userId = ref(login.value.accountInfo.uid)
|
||||
/** 提醒框标题 */
|
||||
const tips = ref()
|
||||
/** 是否显示删除信息的弹窗 */
|
||||
@ -29,7 +33,7 @@ export const useChatMain = (activeItem: MockItem) => {
|
||||
const selectKey = ref()
|
||||
/** 计算出触发页脚后的历史消息下标 */
|
||||
const itemComputed = computed(() => {
|
||||
return items.value.filter((item) => item.accountId !== userId.value).length
|
||||
return items.value.filter((item) => item.accountId !== userUid.value).length
|
||||
})
|
||||
|
||||
/**! 模拟信息列表 */
|
||||
@ -37,11 +41,11 @@ export const useChatMain = (activeItem: MockItem) => {
|
||||
Array.from({ length: 5 }, (_, i) => ({
|
||||
value: `${i}安老师`,
|
||||
key: i,
|
||||
accountId: activeItem.accountId,
|
||||
avatar: activeItem.avatar,
|
||||
accountId: activeItem?.roomId,
|
||||
avatar: activeItem?.avatar,
|
||||
content: '123',
|
||||
type: MsgEnum.TEXT,
|
||||
reply: MsgEnum.REPLY
|
||||
reply: MsgEnum.RECALL
|
||||
? {
|
||||
accountName: '',
|
||||
content: '',
|
||||
@ -65,6 +69,14 @@ export const useChatMain = (activeItem: MockItem) => {
|
||||
click: (item: any) => {
|
||||
Mitt.emit(MittEnum.REPLY_MEG, item)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '撤回',
|
||||
icon: 'corner-down-left',
|
||||
click: async (item: MessageType) => {
|
||||
await apis.recallMsg({ roomId: 1, msgId: item.message.id })
|
||||
chatStore.updateRecallStatus({ msgId: item.message.id })
|
||||
}
|
||||
}
|
||||
])
|
||||
/** 右键消息菜单列表 */
|
||||
@ -72,9 +84,8 @@ export const useChatMain = (activeItem: MockItem) => {
|
||||
{
|
||||
label: '复制',
|
||||
icon: 'copy',
|
||||
click: (item: any) => {
|
||||
const content = items.value[item.key].content
|
||||
handleCopy(content)
|
||||
click: (item: MessageType) => {
|
||||
handleCopy(item.message.body.content)
|
||||
}
|
||||
},
|
||||
...commonMenuList.value
|
||||
@ -175,16 +186,20 @@ export const useChatMain = (activeItem: MockItem) => {
|
||||
/** emoji表情菜单 */
|
||||
const emojiList = ref([
|
||||
{
|
||||
label: '👍'
|
||||
label: '👍',
|
||||
title: '好赞'
|
||||
},
|
||||
{
|
||||
label: '😆'
|
||||
label: '😆',
|
||||
title: '开心'
|
||||
},
|
||||
{
|
||||
label: '🥳'
|
||||
label: '🥳',
|
||||
title: '恭喜'
|
||||
},
|
||||
{
|
||||
label: '🤯'
|
||||
label: '🤯',
|
||||
title: '惊呆了'
|
||||
}
|
||||
])
|
||||
|
||||
@ -222,23 +237,30 @@ export const useChatMain = (activeItem: MockItem) => {
|
||||
const handleScroll = (e: Event) => {
|
||||
const target = e.target as HTMLElement
|
||||
// 获取已滚动的距离,即从顶部到当前滚动位置的距离
|
||||
const scrollTop = target.scrollTop
|
||||
scrollTop.value = target.scrollTop
|
||||
// 获取整个滚动容器的高度
|
||||
const scrollHeight = target.scrollHeight
|
||||
// 获取容器的可视区域高度
|
||||
const clientHeight = target.clientHeight
|
||||
// const scrollHeight = target.scrollHeight
|
||||
// // 获取容器的可视区域高度
|
||||
// const clientHeight = target.clientHeight
|
||||
// 计算距离底部的距离
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
|
||||
// 判断是否大于100
|
||||
if (distanceFromBottom > 100) {
|
||||
floatFooter.value = true
|
||||
// 更新历史消息下标
|
||||
historyIndex.value = itemComputed.value
|
||||
} else {
|
||||
floatFooter.value = false
|
||||
historyIndex.value = 0
|
||||
newMsgNum.value = 0
|
||||
// const distanceFromBottom = scrollHeight - scrollTop.value - clientHeight
|
||||
// 判断是否滚动到顶部
|
||||
if (scrollTop.value === 0) {
|
||||
// 记录顶部最后一条消息的下标
|
||||
// historyIndex.value = chatMessageList.value[0].message.id
|
||||
if (messageOptions.value?.isLoading) return
|
||||
chatStore.loadMore()
|
||||
}
|
||||
// // 判断是否大于100
|
||||
// if (distanceFromBottom > 100) {
|
||||
// floatFooter.value = true
|
||||
// // 更新历史消息下标
|
||||
// historyIndex.value = itemComputed.value
|
||||
// } else {
|
||||
// floatFooter.value = false
|
||||
// historyIndex.value = 0
|
||||
// newMsgNum.value = 0
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -258,12 +280,12 @@ export const useChatMain = (activeItem: MockItem) => {
|
||||
}
|
||||
|
||||
/** 点击气泡消息时候监听用户是否按下ctrl+c来复制内容 */
|
||||
const handleMsgClick = (item: any) => {
|
||||
activeBubble.value = item.key
|
||||
const handleMsgClick = (item: MessageType) => {
|
||||
activeBubble.value = item.message.id
|
||||
// 启用键盘监听
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === 'c') {
|
||||
const content = items.value[item.key].content
|
||||
const content = items.value[item.message.id].content
|
||||
handleCopy(content)
|
||||
// 取消监听键盘事件,以免多次绑定
|
||||
document.removeEventListener('keydown', handleKeyPress)
|
||||
@ -285,12 +307,13 @@ export const useChatMain = (activeItem: MockItem) => {
|
||||
historyIndex,
|
||||
tips,
|
||||
modalShow,
|
||||
userId,
|
||||
specialMenuList,
|
||||
itemComputed,
|
||||
optionsList,
|
||||
report,
|
||||
selectKey,
|
||||
emojiList
|
||||
emojiList,
|
||||
commonMenuList,
|
||||
scrollTop
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,15 @@ import { LimitEnum, MsgEnum } from '@/enums'
|
||||
import { Ref } from 'vue'
|
||||
import { createFileOrVideoDom } from '@/utils/CreateDom.ts'
|
||||
import { RegExp } from '@/utils/RegExp.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
/** 常用工具类 */
|
||||
export const useCommon = () => {
|
||||
const settingStore = setting()
|
||||
const { login } = storeToRefs(settingStore)
|
||||
/** 当前登录用户的uid */
|
||||
const userUid = computed(() => login.value.accountInfo.uid)
|
||||
/** 回复消息 */
|
||||
const reply = ref({
|
||||
accountName: '',
|
||||
@ -34,7 +40,6 @@ export const useCommon = () => {
|
||||
let hasImage = false
|
||||
let hasVideo = false
|
||||
let hasFile = false
|
||||
let hasReply = false
|
||||
|
||||
const elements = messageInputDom.value.childNodes
|
||||
for (const element of elements) {
|
||||
@ -48,8 +53,6 @@ export const useCommon = () => {
|
||||
}
|
||||
} else if (element.tagName === 'VIDEO' || (element.tagName === 'A' && element.href.match(/\.(mp4|webm)$/i))) {
|
||||
hasVideo = true
|
||||
} else if (element.id === 'replyDiv') {
|
||||
hasReply = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,8 +64,6 @@ export const useCommon = () => {
|
||||
return MsgEnum.MIXED
|
||||
} else if (hasImage) {
|
||||
return MsgEnum.IMAGE
|
||||
} else if (hasReply) {
|
||||
return MsgEnum.REPLY
|
||||
} else {
|
||||
return MsgEnum.TEXT
|
||||
}
|
||||
@ -153,15 +154,6 @@ export const useCommon = () => {
|
||||
min-width: 0;
|
||||
`
|
||||
let contentBox
|
||||
const { hyperlinkRegex, foundHyperlinks } = RegExp.isHyperlink(content)
|
||||
// 判断是否包含超链接
|
||||
if (foundHyperlinks && foundHyperlinks.length > 0) {
|
||||
content.replace(hyperlinkRegex, (match: string) => {
|
||||
reply.value.content = match.startsWith('www.') ? 'https://' + match : match
|
||||
})
|
||||
// 去掉content中的标签
|
||||
content = removeTag(content)
|
||||
}
|
||||
// 判断content内容是否是data:image/开头的数组
|
||||
if (Array.isArray(content)) {
|
||||
// 获取总共有多少张图片
|
||||
@ -192,6 +184,15 @@ export const useCommon = () => {
|
||||
// 去掉content中的标签
|
||||
content = removeTag(content)
|
||||
}
|
||||
const { hyperlinkRegex, foundHyperlinks } = RegExp.isHyperlink(content)
|
||||
// 判断是否包含超链接
|
||||
if (foundHyperlinks && foundHyperlinks.length > 0) {
|
||||
content.replace(hyperlinkRegex, (match: string) => {
|
||||
reply.value.content = match.startsWith('www.') ? 'https://' + match : match
|
||||
})
|
||||
// 去掉content中的标签
|
||||
content = removeTag(content)
|
||||
}
|
||||
// 把正文放到span标签中,并设置span标签的样式
|
||||
contentBox = document.createElement('span')
|
||||
contentBox.style.cssText = `
|
||||
@ -208,6 +209,7 @@ export const useCommon = () => {
|
||||
}
|
||||
// 在回复信息的右边添加一个关闭信息的按钮
|
||||
const closeBtn = document.createElement('span')
|
||||
closeBtn.id = 'closeBtn'
|
||||
closeBtn.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -352,6 +354,7 @@ export const useCommon = () => {
|
||||
handlePaste,
|
||||
removeTag,
|
||||
FileOrVideoPaste,
|
||||
reply
|
||||
reply,
|
||||
userUid
|
||||
}
|
||||
}
|
||||
|
70
src/hooks/useDownload.ts
Normal file
70
src/hooks/useDownload.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { ref } from 'vue'
|
||||
import { createEventHook } from '@vueuse/core'
|
||||
|
||||
export const useDownload = () => {
|
||||
const process = ref(0)
|
||||
const isDownloading = ref(false)
|
||||
|
||||
const { on: onLoaded, trigger } = createEventHook()
|
||||
|
||||
const getFileExtension = (url: string) => {
|
||||
const pathname = new URL(url).pathname
|
||||
const lastDotIndex = pathname.lastIndexOf('.')
|
||||
if (lastDotIndex === -1) {
|
||||
return ''
|
||||
}
|
||||
return pathname.slice(lastDotIndex + 1)
|
||||
}
|
||||
|
||||
const getFileName = (url: string) => {
|
||||
const pathname = new URL(url).pathname
|
||||
const lastDotIndex = pathname.lastIndexOf('/')
|
||||
if (lastDotIndex === -1) {
|
||||
return '未知文件'
|
||||
}
|
||||
return pathname.slice(lastDotIndex + 1)
|
||||
}
|
||||
|
||||
const downloadFile = (url: string, filename?: string, extension?: string) => {
|
||||
isDownloading.value = true
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', url, true)
|
||||
xhr.responseType = 'blob'
|
||||
xhr.onprogress = (event) => {
|
||||
if (event.lengthComputable) {
|
||||
process.value = Math.floor((event.loaded / event.total) * 100)
|
||||
}
|
||||
}
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
const blob = new Blob([xhr.response], { type: 'application/octet-stream' })
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
const urlExtension = getFileExtension(url)
|
||||
const ext = extension || urlExtension
|
||||
if (filename) {
|
||||
a.download = `${filename}${ext ? '.' : ''}${ext}`
|
||||
} else {
|
||||
a.download = getFileName(url)
|
||||
}
|
||||
a.click()
|
||||
// 调用 URL.revokeObjectURL() 方法来释放该内存
|
||||
URL.revokeObjectURL(a.href)
|
||||
trigger('success')
|
||||
} else {
|
||||
trigger('fail')
|
||||
}
|
||||
// 清空进度
|
||||
process.value = 0
|
||||
isDownloading.value = false
|
||||
}
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
return {
|
||||
onLoaded,
|
||||
downloadFile,
|
||||
process,
|
||||
isDownloading
|
||||
}
|
||||
}
|
@ -2,10 +2,9 @@ import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
import { EventEnum, MittEnum } from '@/enums'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { MockItem, SessionItem } from '@/services/types.ts'
|
||||
import { delay } from 'lodash-es'
|
||||
import { MockList } from '@/mock'
|
||||
import { WebviewWindow } from '@tauri-apps/api/window'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
@ -16,7 +15,7 @@ const msgBoxShow = ref(false)
|
||||
/** 独立窗口的集合 */
|
||||
const aloneWin = ref(new Set())
|
||||
const shrinkStatus = ref(false)
|
||||
const itemRef = ref({} as MockItem)
|
||||
const itemRef = ref<SessionItem>()
|
||||
export const useMessage = () => {
|
||||
const settingStore = setting()
|
||||
const { chat } = storeToRefs(settingStore)
|
||||
@ -28,14 +27,14 @@ export const useMessage = () => {
|
||||
})
|
||||
|
||||
/** 处理点击选中消息 */
|
||||
const handleMsgClick = (item: MockItem) => {
|
||||
const handleMsgClick = (item: SessionItem) => {
|
||||
msgBoxShow.value = true
|
||||
activeIndex.value = item.key
|
||||
activeIndex.value = item.roomId
|
||||
const data = { msgBoxShow, item }
|
||||
Mitt.emit(MittEnum.MSG_BOX_SHOW, data)
|
||||
// 判断是否打开了独立的窗口
|
||||
if (aloneWin.value.has(EventEnum.ALONE + item.key)) {
|
||||
checkWinExist(EventEnum.ALONE + item.key).then()
|
||||
if (aloneWin.value.has(EventEnum.ALONE + item.roomId)) {
|
||||
checkWinExist(EventEnum.ALONE + item.roomId).then()
|
||||
activeIndex.value = -1
|
||||
Mitt.emit(MittEnum.MSG_BOX_SHOW, { item: -1 })
|
||||
}
|
||||
@ -46,7 +45,7 @@ export const useMessage = () => {
|
||||
}
|
||||
|
||||
/** 处理双击事件 */
|
||||
const handleMsgDblclick = (item: MockItem) => {
|
||||
const handleMsgDblclick = (item: SessionItem) => {
|
||||
if (!chat.value.isDouble) return
|
||||
delay(async () => {
|
||||
await openAloneWin(item)
|
||||
@ -54,17 +53,16 @@ export const useMessage = () => {
|
||||
}
|
||||
|
||||
/** 打开独立窗口 */
|
||||
const openAloneWin = async (item: MockItem) => {
|
||||
const openAloneWin = async (item: SessionItem) => {
|
||||
itemRef.value = { ...item }
|
||||
if (activeIndex.value === item.key) {
|
||||
if (activeIndex.value === item.roomId) {
|
||||
activeIndex.value = -1
|
||||
Mitt.emit(MittEnum.MSG_BOX_SHOW, { item: -1 })
|
||||
await listen('aloneWin', () => {
|
||||
emit('aloneData', { item: { ...item } })
|
||||
})
|
||||
}
|
||||
// TODO 传递用户信息(这里的label最好使用用户唯一的id来代替) (nyh -> 2024-03-18 12:18:10)
|
||||
await createWebviewWindow(item.accountName, EventEnum.ALONE + item.key, 720, 800, '', true, true, 580)
|
||||
await createWebviewWindow(item.name, EventEnum.ALONE + item.roomId, 720, 800, '', true, true, 580)
|
||||
}
|
||||
|
||||
const menuList = ref<OPT.RightMenu[]>([
|
||||
@ -93,7 +91,7 @@ export const useMessage = () => {
|
||||
{
|
||||
label: '打开独立聊天窗口',
|
||||
icon: 'freezing-line-column',
|
||||
click: async (item: MockItem) => {
|
||||
click: async (item: SessionItem) => {
|
||||
await openAloneWin(item)
|
||||
}
|
||||
},
|
||||
@ -104,30 +102,31 @@ export const useMessage = () => {
|
||||
{
|
||||
label: '从消息列表中移除',
|
||||
icon: 'delete',
|
||||
click: (item: MockItem) => {
|
||||
// 根据key找到items中对应的下标
|
||||
const index = MockList.value.findIndex((e) => e.key === item.key)
|
||||
// 删除消息的时候判断是否已经打开了独立窗口
|
||||
if (aloneWin.value.has(`alone${index}`)) {
|
||||
const win = WebviewWindow.getByLabel(`alone${index}`)
|
||||
win?.close()
|
||||
}
|
||||
// 如果找到了对应的元素,则移除
|
||||
if (index !== -1) {
|
||||
const removeItem = MockList.value.splice(index, 1)[0]
|
||||
if (activeIndex.value === removeItem.key) {
|
||||
if (index < MockList.value.length) {
|
||||
// 需要使用新的索引位置找到key更新activeItem.value
|
||||
activeIndex.value = MockList.value[index].key
|
||||
handleMsgClick(MockList.value[index])
|
||||
} else {
|
||||
if (MockList.value.length === 0) return
|
||||
// 如果我们删除的是最后一个元素,则需要选中前一个元素
|
||||
activeIndex.value = MockList.value[MockList.value.length - 1].key
|
||||
handleMsgClick(MockList.value[MockList.value.length - 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
click: (item: SessionItem) => {
|
||||
console.log(item)
|
||||
// // 根据key找到items中对应的下标
|
||||
// const index = MockList.value.findIndex((e) => e.key === item.key)
|
||||
// // 删除消息的时候判断是否已经打开了独立窗口
|
||||
// if (aloneWin.value.has(`alone${index}`)) {
|
||||
// const win = WebviewWindow.getByLabel(`alone${index}`)
|
||||
// win?.close()
|
||||
// }
|
||||
// // 如果找到了对应的元素,则移除
|
||||
// if (index !== -1) {
|
||||
// const removeItem = MockList.value.splice(index, 1)[0]
|
||||
// if (activeIndex.value === removeItem.key) {
|
||||
// if (index < MockList.value.length) {
|
||||
// // 需要使用新的索引位置找到key更新activeItem.value
|
||||
// activeIndex.value = MockList.value[index].key
|
||||
// handleMsgClick(MockList.value[index])
|
||||
// } else {
|
||||
// if (MockList.value.length === 0) return
|
||||
// // 如果我们删除的是最后一个元素,则需要选中前一个元素
|
||||
// activeIndex.value = MockList.value[MockList.value.length - 1].key
|
||||
// handleMsgClick(MockList.value[MockList.value.length - 1])
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
},
|
||||
{ label: '屏蔽此人消息', icon: 'forbid' }
|
||||
@ -135,9 +134,9 @@ export const useMessage = () => {
|
||||
|
||||
onMounted(async () => {
|
||||
await listen(EventEnum.ALONE, () => {
|
||||
emit(EventEnum.ALONE + itemRef.value.key, itemRef.value)
|
||||
if (aloneWin.value.has(EventEnum.ALONE + itemRef.value.key)) return
|
||||
aloneWin.value.add(EventEnum.ALONE + itemRef.value.key)
|
||||
emit(EventEnum.ALONE + itemRef.value?.roomId, itemRef.value)
|
||||
if (aloneWin.value.has(EventEnum.ALONE + itemRef.value?.roomId)) return
|
||||
aloneWin.value.add(EventEnum.ALONE + itemRef.value?.roomId)
|
||||
})
|
||||
await listen(EventEnum.WIN_CLOSE, (e) => {
|
||||
aloneWin.value.delete(e.payload)
|
||||
|
@ -1,38 +1,50 @@
|
||||
import { LimitEnum, MittEnum, MsgEnum } from '@/enums'
|
||||
import { Ref } from 'vue'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { CacheUserItem } from '@/services/types.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { MockList } from '@/mock'
|
||||
import { useCommon } from './useCommon.ts'
|
||||
import { RegExp } from '@/utils/RegExp.ts'
|
||||
import apis from '@/services/apis.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
import { useCachedStore } from '@/stores/cached.ts'
|
||||
|
||||
export const useMsgInput = (messageInputDom: Ref) => {
|
||||
const { triggerInputEvent, insertNode, getMessageContentType, getEditorRange, imgPaste, removeTag, reply } =
|
||||
const chatStore = useChatStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const cachedStore = useCachedStore()
|
||||
const { triggerInputEvent, insertNode, getMessageContentType, getEditorRange, imgPaste, removeTag, reply, userUid } =
|
||||
useCommon()
|
||||
const settingStore = setting()
|
||||
const { chat } = storeToRefs(settingStore)
|
||||
const chatKey = ref(chat.value.sendKey)
|
||||
const msgInput = ref('')
|
||||
const ait = ref(false)
|
||||
/** 临时消息id */
|
||||
const tempMessageId = ref(0)
|
||||
/** 艾特后的关键字的key */
|
||||
const aitKey = ref('')
|
||||
/** 是否正在输入拼音 */
|
||||
const isChinese = ref(false)
|
||||
// 记录编辑器光标的位置
|
||||
const editorRange = ref<{ range: Range; selection: Selection } | null>(null)
|
||||
// 过滤MockList
|
||||
const filteredList = computed(() => {
|
||||
/** @候选人列表 */
|
||||
const personList = computed(() => {
|
||||
if (aitKey.value && !isChinese.value) {
|
||||
return MockList.value.filter((item) => item.accountName.includes(aitKey.value))
|
||||
return cachedStore.currentAtUsersList.filter(
|
||||
(user) => user.name?.startsWith(aitKey.value) && user.uid !== userUid.value
|
||||
)
|
||||
} else {
|
||||
return MockList.value
|
||||
// 过滤当前登录的用户
|
||||
return cachedStore.currentAtUsersList.filter((user) => user.uid !== userUid.value)
|
||||
}
|
||||
})
|
||||
/** 记录当前选中的提及项 key */
|
||||
const selectedAitKey = ref(filteredList.value[0]?.key ?? null)
|
||||
const selectedAitKey = ref(personList.value[0]?.uid ?? null)
|
||||
/** 右键菜单列表 */
|
||||
const menuList = ref([
|
||||
{ label: '剪切', icon: 'screenshot', disabled: true },
|
||||
@ -66,8 +78,8 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
|
||||
watchEffect(() => {
|
||||
chatKey.value = chat.value.sendKey
|
||||
if (!ait.value && filteredList.value.length > 0) {
|
||||
selectedAitKey.value = 0
|
||||
if (!ait.value && personList.value.length > 0) {
|
||||
selectedAitKey.value = personList.value[0]?.uid
|
||||
}
|
||||
// 如果输入框没有值就把回复内容清空
|
||||
if (msgInput.value === '') {
|
||||
@ -91,15 +103,21 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
})
|
||||
/** 监听回复信息的传递 */
|
||||
Mitt.on(MittEnum.REPLY_MEG, (event: any) => {
|
||||
const accountName = useUserInfo(event.fromUser.uid).value.name!
|
||||
// 如果已经有回复消息,则替换掉原来的回复消息
|
||||
if (reply.value.content) {
|
||||
// TODO 如果已经有就替换原来的内容 (nyh -> 2024-04-18 23:10:56)
|
||||
return
|
||||
// 触发id为closeBtn的按钮点击事件,从而关闭第一个回复框,实现回复消息的替换
|
||||
document.getElementById('closeBtn')?.dispatchEvent(new Event('click'))
|
||||
}
|
||||
reply.value = { imgCount: 0, accountName: event.value, content: event.content, key: event.key }
|
||||
if (!Array.isArray(event.message.body.content)) {
|
||||
// 回复前把包含 的字符替换成空格
|
||||
event.message.body.content = event.message.body.content.replace(/ /g, ' ')
|
||||
}
|
||||
reply.value = { imgCount: 0, accountName: accountName, content: event.message.body.content, key: event.key }
|
||||
if (messageInputDom.value) {
|
||||
nextTick().then(() => {
|
||||
messageInputDom.value.focus()
|
||||
insertNode(MsgEnum.REPLY, { accountName: event.value, content: event.content })
|
||||
insertNode(MsgEnum.REPLY, { accountName: accountName, content: event.message.body.content })
|
||||
triggerInputEvent(messageInputDom.value)
|
||||
})
|
||||
}
|
||||
@ -114,27 +132,49 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
window.$message.warning(`一次性只能上传${LimitEnum.COM_COUNT}个文件或图片`)
|
||||
return
|
||||
}
|
||||
// 排除id="replyDiv"的元素的内容
|
||||
const replyDiv = messageInputDom.value.querySelector('#replyDiv')
|
||||
if (replyDiv) {
|
||||
replyDiv.parentNode?.removeChild(replyDiv)
|
||||
// 然后重新赋值给msgInput
|
||||
msgInput.value = messageInputDom.value.innerHTML.replace(replyDiv.outerHTML, '')
|
||||
}
|
||||
ait.value = false
|
||||
const contentType = getMessageContentType(messageInputDom)
|
||||
const msg = {
|
||||
type: contentType,
|
||||
content: msgInput.value,
|
||||
reply: contentType === MsgEnum.REPLY ? reply.value : null
|
||||
content: removeTag(msgInput.value),
|
||||
reply: reply.value
|
||||
}
|
||||
/** 如果是Reply消息,需要将消息的样式修改 */
|
||||
if (msg.type === MsgEnum.REPLY) {
|
||||
// 先去掉原来的标签
|
||||
msg.content = removeTag(msg.content)
|
||||
// 截取空格后的内容
|
||||
// TODO 不允许用户删除回复消息中最前面的空格或者标志符号 (nyh -> 2024-04-17 06:39:22)
|
||||
msg.content = msg.content.replace(/^[\S\s]*\u00A0/, '')
|
||||
// TODO 当输入的类型是艾特类型的时候需要处理 (nyh -> 2024-05-30 19:52:20)
|
||||
// TODO 当输入的内容换行后会有div包裹,这样会有xxr攻击风险 (nyh -> 2024-05-30 20:19:27)
|
||||
/** 如果reply.value.content中有内容,需要将消息的样式修改 */
|
||||
if (reply.value.content) {
|
||||
if (msg.type === MsgEnum.TEXT) {
|
||||
// 创建一个虚拟div元素以便对HTML进行操作
|
||||
const tempDiv = document.createElement('div')
|
||||
// 将msg.content赋值给虚拟div的innerHTML
|
||||
tempDiv.innerHTML = msg.content
|
||||
// 查找id为"replyDiv"的元素
|
||||
const replyDiv = tempDiv.querySelector('#replyDiv')
|
||||
// 如果找到了元素,则删除它
|
||||
if (replyDiv) {
|
||||
replyDiv.parentNode?.removeChild(replyDiv)
|
||||
}
|
||||
// 先去掉原来的标签
|
||||
tempDiv.innerHTML = removeTag(tempDiv.innerHTML)
|
||||
// 只截取tempDiv.innerHTML开头中的
|
||||
tempDiv.innerHTML = tempDiv.innerHTML.replace(/^\s* /, '')
|
||||
// 处理后的内容可以传给实际发送消息的方法
|
||||
msg.content = tempDiv.innerHTML
|
||||
}
|
||||
}
|
||||
const { hyperlinkRegex, foundHyperlinks } = RegExp.isHyperlink(msg.content)
|
||||
/** 判断是否有超链接 */
|
||||
if (foundHyperlinks && foundHyperlinks.length > 0) {
|
||||
msg.content = msg.content.replace(hyperlinkRegex, (match) => {
|
||||
const href = match.startsWith('www.') ? 'https://' + match : match
|
||||
return `<a style="color: inherit" href="${href}" target="_blank" rel="noopener noreferrer">${match}</a>`
|
||||
return `<a style="color: inherit;text-underline-offset: 4px" href="${href}" target="_blank" rel="noopener noreferrer">${match}</a>`
|
||||
})
|
||||
}
|
||||
// 判断文本信息是否超过限制
|
||||
@ -147,7 +187,19 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
window.$message.error('暂不支持混合类型消息发送')
|
||||
return
|
||||
}
|
||||
Mitt.emit(MittEnum.SEND_MESSAGE, msg)
|
||||
apis
|
||||
.sendMsg({ roomId: globalStore.currentSession.roomId, msgType: msg.type, body: { content: msg.content } })
|
||||
.then((res) => {
|
||||
if (res.data.message.type === MsgEnum.TEXT) {
|
||||
chatStore.pushMsg(res.data)
|
||||
// 发完消息就要刷新会话列表,
|
||||
// FIXME 如果当前会话已经置顶了,可以不用刷新
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
} else {
|
||||
// 更新上传状态下的消息
|
||||
chatStore.updateMsg(tempMessageId.value, res.data)
|
||||
}
|
||||
})
|
||||
msgInput.value = ''
|
||||
messageInputDom.value.innerHTML = ''
|
||||
reply.value = { imgCount: 0, accountName: '', content: '', key: '' }
|
||||
@ -184,7 +236,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
aitKey.value = keyWord
|
||||
editorRange.value = { range, selection }
|
||||
|
||||
if (ait.value && filteredList.value.length > 0) {
|
||||
if (ait.value && personList.value.length > 0) {
|
||||
const res = range.getBoundingClientRect()
|
||||
await nextTick(() => {
|
||||
const dom = document.querySelector('.ait') as HTMLElement
|
||||
@ -213,7 +265,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
}
|
||||
|
||||
/** 处理点击@提及框事件 */
|
||||
const handleAit = (item: MockItem) => {
|
||||
const handleAit = (item: CacheUserItem) => {
|
||||
const myEditorRange = editorRange?.value?.range
|
||||
/** 获取光标所在位置的文本节点 */
|
||||
const textNode = myEditorRange?.endContainer
|
||||
@ -231,7 +283,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
range?.setStart(textNode, <number>expRes?.index)
|
||||
/** 设置范围的结束位置为光标的位置 */
|
||||
range?.setEnd(textNode, endOffset!)
|
||||
insertNode(MsgEnum.AIT, item.accountName)
|
||||
insertNode(MsgEnum.AIT, item.name)
|
||||
triggerInputEvent(messageInputDom.value)
|
||||
ait.value = false
|
||||
}
|
||||
@ -242,7 +294,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
handleAit,
|
||||
handleInput,
|
||||
send,
|
||||
filteredList,
|
||||
personList,
|
||||
ait,
|
||||
msgInput,
|
||||
chatKey,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { LogicalSize, WebviewWindow } from '@tauri-apps/api/window'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { EventEnum } from '@/enums'
|
||||
|
||||
export const useWindow = () => {
|
||||
/**
|
||||
@ -27,7 +28,7 @@ export const useWindow = () => {
|
||||
) => {
|
||||
const checkLabel = computed(() => {
|
||||
/** 如果是打开独立窗口就截取label中的固定label名称 */
|
||||
if (label.includes('alone')) {
|
||||
if (label.includes(EventEnum.ALONE)) {
|
||||
return label.replace(/\d/g, '')
|
||||
} else {
|
||||
return label
|
||||
|
@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<main data-tauri-drag-region class="resizable select-none" :style="{ width: width + 'px' }">
|
||||
<main
|
||||
data-tauri-drag-region
|
||||
id="center"
|
||||
class="resizable select-none flex flex-col"
|
||||
:style="{ width: `${initWidth}px` }">
|
||||
<div class="resize-handle" @mousedown="initDrag"></div>
|
||||
<ActionBar
|
||||
class="absolute right-0"
|
||||
v-if="shrinkStatus"
|
||||
@ -47,19 +52,41 @@ import Mitt from '@/utils/Bus.ts'
|
||||
import router from '@/router'
|
||||
import { MittEnum } from '@/enums'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
// const minWidth = 160 // 设置最小宽度
|
||||
// const maxWidth = 320 // 设置最大宽度
|
||||
const width = ref(250) // 初始化宽度
|
||||
/** 设置最小宽度 */
|
||||
const minWidth = 160
|
||||
/** 设置最大宽度 */
|
||||
const maxWidth = 300
|
||||
/** 初始化宽度 */
|
||||
const initWidth = ref(250)
|
||||
/**! 使用(vueUse函数获取)视口宽度 */
|
||||
const { width } = useWindowSize()
|
||||
/** 是否拖拽 */
|
||||
const isDrag = ref(true)
|
||||
/** 当前消息 */
|
||||
const currentMsg = ref()
|
||||
|
||||
// const startX = ref()
|
||||
// const startWidth = ref()
|
||||
const startX = ref()
|
||||
const startWidth = ref()
|
||||
const shrinkStatus = ref(false)
|
||||
|
||||
// todo 1.了解这里是怎么实现的 2.修改拖拽放大缩小的事件
|
||||
Mitt.on(MittEnum.SHRINK_WINDOW, (event) => {
|
||||
shrinkStatus.value = event as boolean
|
||||
width.value = 250
|
||||
watchEffect(() => {
|
||||
if (width.value >= 310 && width.value < 800) {
|
||||
Mitt.emit(MittEnum.SHRINK_WINDOW, true)
|
||||
const center = document.querySelector('#center')
|
||||
center?.classList.add('flex-1')
|
||||
isDrag.value = false
|
||||
}
|
||||
if (width.value >= 800) {
|
||||
Mitt.emit(MittEnum.SHRINK_WINDOW, false)
|
||||
if (currentMsg.value) {
|
||||
Mitt.emit(MittEnum.MSG_BOX_SHOW, { msgBoxShow: true, ...currentMsg.value })
|
||||
}
|
||||
const center = document.querySelector('#center')
|
||||
center?.classList.remove('flex-1')
|
||||
isDrag.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const closeMenu = (event: Event) => {
|
||||
@ -71,42 +98,51 @@ const closeMenu = (event: Event) => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
/** 定义一个函数,在鼠标拖动时调用 */
|
||||
const doDrag = (e: MouseEvent) => {
|
||||
// 使用 requestAnimationFrame 来处理动画,确保动画在下一帧渲染前执行
|
||||
requestAnimationFrame(() => {
|
||||
// 计算新的宽度
|
||||
const newWidth = startWidth.value + e.clientX - startX.value
|
||||
// 如果新宽度不等于最大宽度,则更新宽度值
|
||||
if (newWidth !== maxWidth) {
|
||||
initWidth.value = clamp(newWidth, minWidth, maxWidth) // 使用 clamp 函数限制宽度值在最小值和最大值之间
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 定义一个函数,用于将数值限制在指定的最小值和最大值之间 */
|
||||
const clamp = (value: number, min: number, max: number) => {
|
||||
return Math.min(Math.max(value, min), max) // 使用 Math.min 和 Math.max 函数来限制数值范围
|
||||
}
|
||||
|
||||
const initDrag = (e: MouseEvent) => {
|
||||
if (!isDrag.value) return
|
||||
startX.value = e.clientX
|
||||
startWidth.value = initWidth.value
|
||||
document.addEventListener('mousemove', doDrag, false)
|
||||
document.addEventListener('mouseup', stopDrag, false)
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
document.removeEventListener('mousemove', doDrag, false)
|
||||
document.removeEventListener('mouseup', stopDrag, false)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
Mitt.on(MittEnum.SHRINK_WINDOW, (event) => {
|
||||
shrinkStatus.value = event as boolean
|
||||
})
|
||||
Mitt.on(MittEnum.MSG_BOX_SHOW, (event: any) => {
|
||||
if (!event) return
|
||||
currentMsg.value = event
|
||||
})
|
||||
window.addEventListener('click', closeMenu, true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', closeMenu, true)
|
||||
})
|
||||
// watchEffect(() => {
|
||||
// if (width.value === maxWidth) {
|
||||
// Mitt.emit('shrinkWindow', false)
|
||||
// }
|
||||
// })
|
||||
|
||||
// const initDrag = (e: MouseEvent) => {
|
||||
// startX.value = e.clientX
|
||||
// startWidth.value = width.value
|
||||
//
|
||||
// document.addEventListener('mousemove', doDrag, false)
|
||||
// document.addEventListener('mouseup', stopDrag, false)
|
||||
// }
|
||||
//
|
||||
// const doDrag = (e: MouseEvent) => {
|
||||
// const newWidth = startWidth.value + e.clientX - startX.value
|
||||
// if (newWidth <= maxWidth && newWidth >= minWidth) {
|
||||
// width.value = newWidth
|
||||
// } else if (newWidth > maxWidth) {
|
||||
// width.value = maxWidth
|
||||
// } else if (newWidth < minWidth) {
|
||||
// width.value = minWidth
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// const stopDrag = () => {
|
||||
// document.removeEventListener('mousemove', doDrag, false)
|
||||
// document.removeEventListener('mouseup', stopDrag, false)
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -10,7 +10,8 @@
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
width: 1px;
|
||||
cursor: ew-resize;
|
||||
background-color: #ccc; /** 可以根据需要更改颜色 */
|
||||
z-index: 9999;
|
||||
background-color: var(--split-color);
|
||||
}
|
@ -13,7 +13,6 @@ import Right from './right/index.vue'
|
||||
import Mitt from '@/utils/Bus'
|
||||
import { MittEnum } from '@/enums'
|
||||
|
||||
/** todo home窗口创建的时候已经设置了resizable: true,可以调整大小了,但是还是不可以调整大小 */
|
||||
const shrinkStatus = ref(false)
|
||||
/**
|
||||
* event默认如果没有传递值就为true,所以shrinkStatus的值为false就会发生值的变化
|
||||
|
@ -3,6 +3,7 @@ import { emit } from '@tauri-apps/api/event'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { delay } from 'lodash-es'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import axios from 'axios'
|
||||
|
||||
const { createWebviewWindow } = useWindow()
|
||||
const itemsTop = ref<OPT.L.Top[]>([
|
||||
@ -75,6 +76,11 @@ const moreList = ref<OPT.L.MoreList[]>([
|
||||
label: '退出账号',
|
||||
icon: 'power',
|
||||
click: async () => {
|
||||
localStorage.removeItem('USER_INFO')
|
||||
localStorage.removeItem('TOKEN')
|
||||
// 清空axios请求头
|
||||
const instance = axios.create()
|
||||
instance.defaults.headers.common.Authorization = ''
|
||||
// todo 退出账号 需要关闭其他的全部窗口
|
||||
await createWebviewWindow('登录', 'login', 320, 448, 'home', true, false, 320, 448).then(() => {
|
||||
/** 给一点延迟,不然创建登录窗口后还没有来得及设置阴影和圆角效果 */
|
||||
|
@ -1,50 +1,50 @@
|
||||
<template>
|
||||
<main data-tauri-drag-region class="left w-60px h-full p-[30px_6px_15px] box-border flex-col-center select-none">
|
||||
<main class="left min-w-60px h-full p-[30px_6px_15px] box-border flex-col-center select-none" data-tauri-drag-region>
|
||||
<!-- 点击时头像内容框 -->
|
||||
<n-popover
|
||||
v-model:show="infoShow"
|
||||
trigger="click"
|
||||
:show-arrow="false"
|
||||
:placement="shrinkStatus ? 'bottom-start' : 'right-start'"
|
||||
style="padding: 0; background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px)">
|
||||
:show-arrow="false"
|
||||
style="padding: 0; background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px)"
|
||||
trigger="click">
|
||||
<template #trigger>
|
||||
<!-- 头像 -->
|
||||
<div class="relative size-36px rounded-50% cursor-pointer">
|
||||
<n-avatar round :color="'#fff'" :size="36" :src="login.accountInfo.avatar" fallback-src="/logo.png" />
|
||||
<n-avatar :color="'#fff'" :size="36" :src="login.accountInfo.avatar" fallback-src="/logo.png" round />
|
||||
|
||||
<div
|
||||
@click.stop="openContent('在线状态', 'onlineStatus', 320, 480)"
|
||||
class="bg-[--bg-avatar] text-10px rounded-50% size-12px absolute bottom--2px right--2px border-(2px solid [--bg-avatar])">
|
||||
<img class="rounded-50% size-full" :src="url" alt="" />
|
||||
class="bg-[--bg-avatar] text-10px rounded-50% size-12px absolute bottom--2px right--2px border-(2px solid [--bg-avatar])"
|
||||
@click.stop="openContent('在线状态', 'onlineStatus', 320, 480)">
|
||||
<img :src="url" alt="" class="rounded-50% size-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 用户个人信息框 -->
|
||||
<n-flex
|
||||
vertical
|
||||
:size="26"
|
||||
:style="`background: linear-gradient(to bottom, ${bgColor} 0%, ${themeColor} 100%)`"
|
||||
class="size-full p-15px box-border rounded-8px"
|
||||
:style="`background: linear-gradient(to bottom, ${bgColor} 0%, ${themeColor} 100%)`">
|
||||
vertical>
|
||||
<!-- 头像以及信息区域 -->
|
||||
<n-flex justify="space-between" align="center" :size="25">
|
||||
<n-flex :size="25" align="center" justify="space-between">
|
||||
<n-flex>
|
||||
<img class="size-68px rounded-50% select-none" :src="login.accountInfo.avatar" alt="" />
|
||||
<img :src="login.accountInfo.avatar" alt="" class="size-68px rounded-50% select-none" />
|
||||
|
||||
<n-flex vertical justify="center" :size="10" class="text-[--text-color]">
|
||||
<n-flex :size="10" class="text-[--text-color]" justify="center" vertical>
|
||||
<span class="text-18px">{{ login.accountInfo.name }}</span>
|
||||
<span class="text-(12px [--info-text-color])">账号 {{ login.accountInfo.uid }}</span>
|
||||
<n-flex
|
||||
@click="openContent('在线状态', 'onlineStatus', 320, 480)"
|
||||
:size="5"
|
||||
align="center"
|
||||
class="item-hover ml--4px">
|
||||
<img class="rounded-50% size-18px" :src="url" alt="" />
|
||||
class="item-hover ml--4px"
|
||||
@click="openContent('在线状态', 'onlineStatus', 320, 480)">
|
||||
<img :src="url" alt="" class="rounded-50% size-18px" />
|
||||
<span>{{ title }}</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<n-flex vertical align="center" :size="5" class="item-hover">
|
||||
<n-flex :size="5" align="center" class="item-hover" vertical>
|
||||
<svg class="size-20px"><use href="#thumbs-up"></use></svg>
|
||||
<span class="text-12px">9999+</span>
|
||||
</n-flex>
|
||||
@ -58,36 +58,36 @@
|
||||
<n-flex :size="40" class="select-none">
|
||||
<span class="text-[--info-text-color]">动态</span>
|
||||
<n-image-group>
|
||||
<n-flex :size="6" :class="shrinkStatus ? 'overflow-hidden w-180px' : ''" :wrap="false">
|
||||
<n-flex :class="shrinkStatus ? 'overflow-hidden w-180px' : ''" :size="6" :wrap="false">
|
||||
<n-image
|
||||
v-for="n in 4"
|
||||
:key="n"
|
||||
preview-disabled
|
||||
class="rounded-8px"
|
||||
width="50"
|
||||
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" />
|
||||
preview-disabled
|
||||
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
|
||||
width="50" />
|
||||
</n-flex>
|
||||
</n-image-group>
|
||||
</n-flex>
|
||||
|
||||
<n-flex justify="center" align="center" :size="40">
|
||||
<n-button secondary> 编辑资料 </n-button>
|
||||
<n-flex :size="40" align="center" justify="center">
|
||||
<n-button secondary @click="handleEditing"> 编辑资料 </n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-popover>
|
||||
|
||||
<div data-tauri-drag-region class="flex-1 mt-20px flex-col-x-center justify-between">
|
||||
<div class="flex-1 mt-20px flex-col-x-center justify-between" data-tauri-drag-region>
|
||||
<!-- 上部分操作栏 -->
|
||||
<header class="flex-col-x-center gap-10px color-[--icon-color]">
|
||||
<div
|
||||
v-for="(item, index) in itemsTop"
|
||||
:key="index"
|
||||
@click="pageJumps(item.url)"
|
||||
:class="[
|
||||
{ active: activeUrl === item.url && item.url !== 'dynamic' },
|
||||
openWindowsList.has(item.url) ? 'p-[6px_8px] color-#13987f' : 'top-action'
|
||||
]">
|
||||
<n-badge :value="item.badge" :max="99">
|
||||
]"
|
||||
@click="pageJumps(item.url)">
|
||||
<n-badge :max="99" :value="item.badge">
|
||||
<svg class="size-22px">
|
||||
<use
|
||||
:href="`#${activeUrl === item.url || openWindowsList.has(item.url) ? item.iconAction : item.icon}`"></use>
|
||||
@ -101,17 +101,17 @@
|
||||
<div
|
||||
v-for="(item, index) in itemsBottom"
|
||||
:key="index"
|
||||
@click="openContent(item.title, item.label)"
|
||||
:class="openWindowsList.has(item.url.substring(1)) ? 'p-[6px_8px] color-#13987f' : 'bottom-action'">
|
||||
:class="openWindowsList.has(item.url.substring(1)) ? 'p-[6px_8px] color-#13987f' : 'bottom-action'"
|
||||
@click="openContent(item.title, item.label)">
|
||||
<svg class="size-22px">
|
||||
<use :href="`#${openWindowsList.has(item.url.substring(1)) ? item.iconAction : item.icon}`"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<svg
|
||||
@click="settingShow = !settingShow"
|
||||
:class="{ 'color-#13987f': settingShow }"
|
||||
class="more size-22px relative"
|
||||
:class="{ 'color-#13987f': settingShow }">
|
||||
@click="settingShow = !settingShow">
|
||||
<use :href="settingShow ? '#hamburger-button-action' : '#hamburger-button'"></use>
|
||||
</svg>
|
||||
|
||||
@ -128,23 +128,121 @@
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 编辑资料弹窗 -->
|
||||
<n-modal v-model:show="editInfo.show" :mask-closable="false" class="rounded-8px" transform-origin="center">
|
||||
<div class="bg-[--bg-edit] w-480px h-fit box-border flex flex-col">
|
||||
<n-flex :size="6" vertical>
|
||||
<n-flex class="text-(14px --text-color) select-none pt-6px" justify="center">编辑资料</n-flex>
|
||||
<svg
|
||||
class="size-14px ml-a cursor-pointer pt-6px select-none absolute right-6px"
|
||||
@click="editInfo.show = false">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
<span class="h-1px w-full bg-[--line-color]"></span>
|
||||
</n-flex>
|
||||
<n-flex :size="20" class="p-22px select-none" vertical>
|
||||
<!-- 头像 -->
|
||||
<n-flex justify="center">
|
||||
<n-avatar :size="80" :src="editInfo.content.avatar" round style="border: 3px solid #fff" />
|
||||
</n-flex>
|
||||
<n-flex v-if="currentBadge" align="center" justify="center">
|
||||
<span class="text-(14px #707070)">当前佩戴的徽章:</span>
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<img :src="currentBadge?.img" alt="" class="size-22px" />
|
||||
</template>
|
||||
<span>{{ currentBadge?.describe }}</span>
|
||||
</n-popover>
|
||||
</n-flex>
|
||||
<!-- 昵称编辑输入框 -->
|
||||
<n-popover placement="top-start" trigger="click">
|
||||
<template #trigger>
|
||||
<n-input
|
||||
ref="inputInstRef"
|
||||
v-model:value="editInfo.content.name"
|
||||
:count-graphemes="countGraphemes"
|
||||
:default-value="editInfo.content.name"
|
||||
:maxlength="8"
|
||||
:passively-activated="true"
|
||||
class="rounded-6px"
|
||||
clearable
|
||||
placeholder="请输入你的昵称"
|
||||
show-count
|
||||
type="text">
|
||||
<template #prefix>
|
||||
<span class="pr-6px text-#909090">昵称</span>
|
||||
</template>
|
||||
</n-input>
|
||||
</template>
|
||||
<span>剩余改名次数: {{ editInfo.content.modifyNameChance || 0 }}</span>
|
||||
</n-popover>
|
||||
|
||||
<!-- 徽章列表 -->
|
||||
<n-flex :size="[56, 20]" align="center">
|
||||
<template v-for="item in editInfo.badgeList" :key="item.id">
|
||||
<div class="badge-item">
|
||||
<n-image
|
||||
:class="{ 'grayscale-0': item.obtain === IsYetEnum.YES }"
|
||||
:src="item.img"
|
||||
alt="badge"
|
||||
class="flex-center grayscale"
|
||||
width="100"
|
||||
height="100"
|
||||
preview-disabled
|
||||
round />
|
||||
<div class="tip">
|
||||
<template v-if="item.obtain === IsYetEnum.YES">
|
||||
<n-button v-if="item.wearing === IsYetEnum.NO" color="#13987f" @click="toggleWarningBadge(item)">
|
||||
佩戴
|
||||
</n-button>
|
||||
</template>
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<svg class="size-24px outline-none"><use href="#tips"></use></svg>
|
||||
</template>
|
||||
<span>{{ item.describe }}</span>
|
||||
</n-popover>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
<n-flex class="p-12px" align="center" justify="center">
|
||||
<n-button :disabled="editInfo.content.name === login.accountInfo.name" color="#13987f" @click="saveEditInfo">
|
||||
保存
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</n-modal>
|
||||
</main>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { delay } from 'lodash-es'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import router from '@/router'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { EventEnum, MittEnum, ThemeEnum } from '@/enums'
|
||||
import { EventEnum, IsYetEnum, MittEnum, MsgEnum, ThemeEnum } from '@/enums'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { itemsTop, itemsBottom, moreList } from './config.ts'
|
||||
import { itemsBottom, itemsTop, moreList } from './config.ts'
|
||||
import { onlineStatus } from '@/stores/onlineStatus.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import apis from '@/services/apis.ts'
|
||||
import GraphemeSplitter from 'grapheme-splitter'
|
||||
import { BadgeType, UserInfoType } from '@/services/types.ts'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
import { renderReplyContent } from '@/utils/RenderReplyContent.ts'
|
||||
import { formatTimestamp } from '@/utils/ComputedTime.ts'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { useCachedStore } from '@/stores/cached.ts'
|
||||
|
||||
const prefers = matchMedia('(prefers-color-scheme: dark)')
|
||||
const { createWebviewWindow } = useWindow()
|
||||
const settingStore = setting()
|
||||
const userStore = useUserStore()
|
||||
const cachedStore = useCachedStore()
|
||||
const { themes, login } = storeToRefs(settingStore)
|
||||
const OLStatusStore = onlineStatus()
|
||||
const { url, title, bgColor } = storeToRefs(OLStatusStore)
|
||||
@ -156,12 +254,53 @@ const infoShow = ref(false)
|
||||
const themeColor = ref(themes.value.content === ThemeEnum.DARK ? 'rgba(63,63,63, 0.2)' : 'rgba(241,241,241, 0.2)')
|
||||
/** 已打开窗口的列表 */
|
||||
const openWindowsList = ref(new Set())
|
||||
/** 编辑资料弹窗 */
|
||||
// TODO 这里考虑是否查接口查实时的用户信息还是直接查本地存储的用户信息 (nyh -> 2024-05-05 01:12:36)
|
||||
const editInfo = ref<{
|
||||
show: boolean
|
||||
content: Partial<UserInfoType>
|
||||
badgeList: BadgeType[]
|
||||
}>({
|
||||
show: false,
|
||||
content: {},
|
||||
badgeList: []
|
||||
})
|
||||
/** 当前用户佩戴的徽章 */
|
||||
const currentBadge = computed(() =>
|
||||
editInfo.value.badgeList.find((item) => item.obtain === IsYetEnum.YES && item.wearing === IsYetEnum.YES)
|
||||
)
|
||||
|
||||
/** 跟随系统主题模式切换主题 */
|
||||
const followOS = () => {
|
||||
themeColor.value = prefers.matches ? 'rgba(63,63,63, 0.2)' : 'rgba(241,241,241, 0.2)'
|
||||
}
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const sessionList = computed(() =>
|
||||
chatStore.sessionList.map((item) => {
|
||||
// 最后一条消息内容
|
||||
const lastMsg = Array.from(chatStore.messageMap.get(item.roomId)?.values() || [])?.slice(-1)?.[0]
|
||||
let LastUserMsg = ''
|
||||
if (lastMsg) {
|
||||
const lastMsgUserName = useUserInfo(lastMsg.fromUser.uid)
|
||||
LastUserMsg =
|
||||
lastMsg.message?.type === MsgEnum.RECALL
|
||||
? `${lastMsgUserName.value.name}:'撤回了一条消息'`
|
||||
: renderReplyContent(
|
||||
lastMsgUserName.value.name,
|
||||
lastMsg.message?.type,
|
||||
lastMsg.message?.body?.content || lastMsg.message?.body
|
||||
)!
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
lastMsg: LastUserMsg || item.text || '欢迎使用HuLa',
|
||||
lastMsgTime: formatTimestamp(item?.activeTime)
|
||||
}
|
||||
})
|
||||
)
|
||||
const msgTotal = computed(() => {
|
||||
return sessionList.value.reduce((total, item) => total + item.unreadCount, 0)
|
||||
})
|
||||
watchEffect(() => {
|
||||
Mitt.on(MittEnum.UPDATE_MSG_TOTAL, (event) => {
|
||||
itemsTop.value.find((item) => {
|
||||
@ -170,6 +309,11 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
})
|
||||
itemsTop.value.find((item) => {
|
||||
if (item.url === 'message') {
|
||||
item.badge = msgTotal.value
|
||||
}
|
||||
})
|
||||
Mitt.on(MittEnum.TO_SEND_MSG, (event: any) => {
|
||||
activeUrl.value = event.url
|
||||
})
|
||||
@ -182,6 +326,64 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
/** 更新缓存里面的用户信息 */
|
||||
const updateCurrentUserCache = (key: 'name' | 'wearingItemId', value: any) => {
|
||||
const currentUser = userStore.userInfo.uid && cachedStore.userCachedList[userStore.userInfo.uid]
|
||||
if (currentUser) {
|
||||
currentUser[key] = value // 更新缓存里面的用户信息
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存用户信息 */
|
||||
const saveEditInfo = () => {
|
||||
if (!editInfo.value.content.name || editInfo.value.content.name.trim() === '') {
|
||||
window.$message.error('昵称不能为空')
|
||||
return
|
||||
}
|
||||
if (editInfo.value.content.modifyNameChance === 0) {
|
||||
window.$message.error('改名次数不足')
|
||||
return
|
||||
}
|
||||
apis.modifyUserName(editInfo.value.content.name).then((res) => {
|
||||
if (!res.success) {
|
||||
window.$message.error(res.errMsg)
|
||||
return
|
||||
}
|
||||
// 更新本地缓存的用户信息
|
||||
login.value.accountInfo.name = editInfo.value.content.name!
|
||||
updateCurrentUserCache('name', editInfo.value.content.name) // 更新缓存里面的用户信息
|
||||
if (!editInfo.value.content.modifyNameChance) return
|
||||
editInfo.value.content.modifyNameChance -= 1
|
||||
window.$message.success('保存成功')
|
||||
})
|
||||
}
|
||||
|
||||
/** 佩戴卸下徽章 */
|
||||
const toggleWarningBadge = async (badge: BadgeType) => {
|
||||
if (!badge?.id) return
|
||||
await apis.setUserBadge(badge.id)
|
||||
}
|
||||
|
||||
/** 计算字符长度 */
|
||||
const countGraphemes = (value: string) => {
|
||||
const splitter = new GraphemeSplitter()
|
||||
return splitter.countGraphemes(value)
|
||||
}
|
||||
|
||||
/* 打开并且创建modal */
|
||||
const handleEditing = () => {
|
||||
/** 获取用户的徽章列表 */
|
||||
apis.getBadgeList().then((res) => {
|
||||
editInfo.value.badgeList = res.data
|
||||
})
|
||||
/** 获取用户信息 */
|
||||
apis.getUserInfo().then((res) => {
|
||||
editInfo.value.content = res.data
|
||||
})
|
||||
infoShow.value = false
|
||||
editInfo.value.show = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一跳转路由方法
|
||||
* @param url 跳转的路由
|
||||
@ -241,6 +443,16 @@ onUnmounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import 'style';
|
||||
.badge-item {
|
||||
.tip {
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
@apply absolute top-0 left-0 w-full h-full flex-center z-999 opacity-0;
|
||||
}
|
||||
@apply bg-#ccc relative rounded-50% size-fit p-4px cursor-pointer;
|
||||
&:hover .tip {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<main class="flex-1 bg-[--right-bg-color] h-full w-100vw">
|
||||
<main class="flex-1 bg-[--right-bg-color] h-full w-100vw min-w-600px">
|
||||
<ActionBar :current-label="appWindow.label" />
|
||||
<!-- 需要判断当前路由是否是信息详情界面 -->
|
||||
<ChatBox :active-item="activeItem" v-if="msgBoxShow && isChat && activeItem !== -1" />
|
||||
@ -67,7 +67,7 @@ onMounted(() => {
|
||||
|
||||
if (isDetails) {
|
||||
Mitt.on(MittEnum.DETAILS_SHOW, (event: any) => {
|
||||
DetailsContent.value = event.data
|
||||
DetailsContent.value = event.context
|
||||
detailsShow.value = event.detailsShow as boolean
|
||||
})
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import 'uno.css'
|
||||
import '@unocss/reset/eric-meyer.css' // unocss提供的浏览器默认样式重置
|
||||
import '@/services/webSocket'
|
||||
import { pinia } from '@/stores'
|
||||
import vResize from '@/directives/v-resize'
|
||||
import vSlide from '@/directives/v-slide.ts'
|
||||
@ -9,3 +10,9 @@ import App from '@/App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router).use(pinia).directive('resize', vResize).directive('slide', vSlide).mount('#app')
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
import('@/utils/console.ts').then((module) => {
|
||||
/**! 控制台打印项目版本信息(不需要可手动关闭)*/
|
||||
module.consolePrint()
|
||||
})
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ export const MockList = ref<MockItem[]>(
|
||||
key: i,
|
||||
avatar: `${avatars}?${i}`,
|
||||
type: type,
|
||||
accountId: `${i}`,
|
||||
accountId: i,
|
||||
accountName: generateRandomString(Math.floor(Math.random() * 10) + 1, type)
|
||||
}
|
||||
})
|
||||
|
@ -1,20 +1,122 @@
|
||||
import { createAxios } from '@/services/request'
|
||||
import urls from '@/services/urls'
|
||||
import type { login, Response } from '@/services/types'
|
||||
import { parameter } from '@/services/types'
|
||||
import type {
|
||||
Response,
|
||||
BadgeType,
|
||||
CacheBadgeItem,
|
||||
CacheBadgeReq,
|
||||
CacheUserItem,
|
||||
CacheUserReq,
|
||||
ContactItem,
|
||||
EmojiItem,
|
||||
GroupDetailReq,
|
||||
GroupStatisticType,
|
||||
ListResponse,
|
||||
MarkMsgReq,
|
||||
MessageReq,
|
||||
MessageType,
|
||||
MsgReadUnReadCountType,
|
||||
RequestFriendItem,
|
||||
SessionItem,
|
||||
UserInfoType,
|
||||
UserItem
|
||||
} from '@/services/types'
|
||||
|
||||
const request = createAxios()
|
||||
|
||||
const GET = <T>(url: string, params?: any) => request.get<T, Response>(url, params)
|
||||
const POST = <T>(url: string, params?: any) => request.post<T, Response>(url, params)
|
||||
// const PUT = <T>(url: string, params?: any) => request.put<T, Response>(url, params)
|
||||
// const DELETE = <T>(url: string, params?: any) => request.delete<T, Response>(url, params)
|
||||
const PUT = <T>(url: string, params?: any) => request.put<T, Response>(url, params)
|
||||
const DELETE = <T>(url: string, params?: any) => request.delete<T, Response>(url, params)
|
||||
|
||||
export default {
|
||||
/**登录 请求*/
|
||||
login: (form: login): Promise<Response> => POST(urls.login, form),
|
||||
/**退出 请求*/
|
||||
logout: (): Promise<Response> => GET(urls.logout),
|
||||
/**获取文章列表*/
|
||||
getArticlePage: (params: parameter): Promise<Response> => GET(urls.articlePage, { params })
|
||||
/** 获取用户信息 */
|
||||
getUserInfo: (): Promise<Response> => GET(urls.getUserInfo),
|
||||
/** 获取群成员列表 */
|
||||
getGroupList: (params?: any) => GET<ListResponse<UserItem>>(urls.getGroupUserList, params),
|
||||
/** 获取群成员统计 */
|
||||
getMemberStatistic: () => GET<GroupStatisticType>(urls.getMemberStatistic),
|
||||
/** 房间内的所有群成员列表-@专用 */
|
||||
getAllUserBaseInfo: (params?: any) =>
|
||||
GET<Pick<CacheUserItem, 'avatar' | 'name' | 'uid'>[]>(urls.getAllUserBaseInfo, params),
|
||||
/** 批量获取成员详细信息 */
|
||||
getUserInfoBatch: (users: CacheUserReq[]) => POST<CacheUserItem[]>(urls.getUserInfoBatch, { reqList: users }),
|
||||
/** 批量获取徽章信息 */
|
||||
getBadgesBatch: (badges: CacheBadgeReq[]) => POST<CacheBadgeItem[]>(urls.getBadgesBatch, { reqList: badges }),
|
||||
/** 获取消息列表 */
|
||||
getMsgList: (params?: any) => GET<ListResponse<MessageType>>(urls.getMsgList, params),
|
||||
/** 发送消息 */
|
||||
sendMsg: (data?: MessageReq) => POST<MessageType>(urls.sendMsg, data),
|
||||
/** 标记消息,点赞等 */
|
||||
markMsg: (data?: MarkMsgReq) => PUT<void>(urls.markMsg, data),
|
||||
/** 获取用户详细信息 */
|
||||
getUserDetail: () => GET<UserInfoType>(urls.getUserInfoDetail, {}),
|
||||
/** 获取徽章列表 */
|
||||
getBadgeList: (): Promise<Response> => GET<BadgeType[]>(urls.getBadgeList),
|
||||
/** 设置用户勋章 */
|
||||
setUserBadge: (badgeId: number) => PUT<void>(urls.setUserBadge, { badgeId }),
|
||||
/** 修改用户名 */
|
||||
modifyUserName: (name: string) => PUT<void>(urls.modifyUserName, { name }),
|
||||
/** 撤回消息 */
|
||||
recallMsg: (data: { msgId: number; roomId: number }) => PUT<void>(urls.recallMsg, data),
|
||||
/** 拉黑用户 */
|
||||
blockUser: (data: { uid: number }) => PUT<void>(urls.blockUser, data),
|
||||
/** 获取临时上传链接 */
|
||||
getUploadUrl: (params: any) => GET<{ downloadUrl: string; uploadUrl: string }>(urls.fileUpload, { params }),
|
||||
/** 新增表情包 */
|
||||
addEmoji: (data: { uid: number; expressionUrl: string }) => POST<MessageType>(urls.addEmoji, data),
|
||||
/** 获取表情 **/
|
||||
getEmoji: (params: { uid: number }) => GET<EmojiItem[]>(urls.getEmoji, { params }),
|
||||
/** 删除id */
|
||||
deleteEmoji: (params: { id: number }) => DELETE<EmojiItem[]>(urls.deleteEmoji, params),
|
||||
/** 获取联系人列表 */
|
||||
getContactList: (params?: any) => GET<ListResponse<ContactItem>>(urls.getContactList, { params }),
|
||||
/** 获取好友申请列表 */
|
||||
requestFriendList: (params?: any) => GET<ListResponse<RequestFriendItem>>(urls.requestFriendList, { params }),
|
||||
/** 发送添加好友请求 */
|
||||
sendAddFriendRequest: (params: { targetUid: number; msg: string }) =>
|
||||
POST<EmojiItem[]>(urls.sendAddFriendRequest, params),
|
||||
/** 同意好友申请 */
|
||||
applyFriendRequest: (params: { applyId: number }) => PUT(urls.sendAddFriendRequest, params),
|
||||
/** 同意好友申请 */
|
||||
deleteFriend: (params: { targetUid: number }) => DELETE(urls.deleteFriend, params),
|
||||
/** 好友申请未读数 */
|
||||
newFriendCount: () => GET<{ unReadCount: number }>(urls.newFriendCount),
|
||||
/** 会话列表 */
|
||||
getSessionList: (params?: any) => GET<ListResponse<SessionItem>>(urls.getSessionList, params),
|
||||
/** 消息的已读未读列表 */
|
||||
getMsgReadList: (params?: any) => GET<ListResponse<{ uid: number }>>(urls.getMsgReadList, params),
|
||||
/** 消息已读未读数 */
|
||||
getMsgReadCount: (params?: any) => GET<MsgReadUnReadCountType[]>(urls.getMsgReadCount, params),
|
||||
/** 消息阅读上报 */
|
||||
markMsgRead: (params?: any) => PUT<MsgReadUnReadCountType[]>(urls.getMsgReadCount, params),
|
||||
/** 新增群组 */
|
||||
createGroup: (params: { uidList: number[] }) => POST<{ id: number }>(urls.createGroup, params),
|
||||
/** 邀请群成员 */
|
||||
inviteGroupMember: (params: { roomId: number; uidList: number[] }) => POST(urls.inviteGroupMember, params),
|
||||
/** 删除群成员 */
|
||||
removeGroupMember: (params: { roomId: number; uid: number }) => DELETE(urls.inviteGroupMember, params),
|
||||
/** 群组详情 */
|
||||
groupDetail: (params: { id: number }) => GET<GroupDetailReq>(urls.groupDetail, { params }),
|
||||
/** 会话详情 */
|
||||
sessionDetail: (params: { id: number }) => GET<SessionItem>(urls.sessionDetail, { params }),
|
||||
/** 会话详情(联系人列表发消息用) */
|
||||
sessionDetailWithFriends: (params: { uid: number }) => GET<SessionItem>(urls.sessionDetailWithFriends, { params }),
|
||||
/** 添加群管理 */
|
||||
addAdmin: ({ roomId, uidList }: { roomId: number; uidList: number[] }) =>
|
||||
PUT<boolean>(urls.addAdmin, {
|
||||
roomId,
|
||||
uidList
|
||||
}),
|
||||
/** 撤销群管理 */
|
||||
revokeAdmin: ({ roomId, uidList }: { roomId: number; uidList: number[] }) =>
|
||||
DELETE<boolean>(urls.revokeAdmin, {
|
||||
roomId,
|
||||
uidList
|
||||
}),
|
||||
/** 退群 */
|
||||
exitGroup: ({ roomId }: { roomId: number }) =>
|
||||
DELETE<boolean>(urls.exitGroup, {
|
||||
roomId
|
||||
})
|
||||
}
|
||||
|
@ -1,5 +1,29 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||
import { userStore } from '@/stores/user.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
|
||||
/** 是否是测试环境 */
|
||||
const isTest = computed(() => {
|
||||
return setting().login.accountInfo.token === 'test'
|
||||
})
|
||||
|
||||
function getToken() {
|
||||
let tempToken = ''
|
||||
return {
|
||||
get() {
|
||||
if (tempToken) return tempToken
|
||||
const token = localStorage.getItem('TOKEN')
|
||||
if (token) {
|
||||
tempToken = token
|
||||
}
|
||||
return tempToken
|
||||
},
|
||||
clear() {
|
||||
tempToken = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const computedToken = getToken()
|
||||
|
||||
//请求配置
|
||||
export const createAxios = (config?: AxiosRequestConfig): AxiosInstance => {
|
||||
@ -19,15 +43,25 @@ export const createAxios = (config?: AxiosRequestConfig): AxiosInstance => {
|
||||
instance.interceptors.request.use(
|
||||
function (config: any) {
|
||||
//判断是否有token 根据自己的需求判断
|
||||
const token = userStore().getBearerToken
|
||||
if (token != undefined) {
|
||||
//如果要求携带在参数中
|
||||
const token = setting().login.accountInfo.token
|
||||
if (isTest.value) {
|
||||
// 如果token为'test',阻止请求并返回一个错误对象
|
||||
return Promise.reject(
|
||||
window.$message.create('当前为测试环境,请注意辨别', {
|
||||
type: 'warning',
|
||||
closable: true,
|
||||
duration: 0
|
||||
})
|
||||
)
|
||||
}
|
||||
if (token != void 0) {
|
||||
// //如果要求携带在参数中
|
||||
// config.params = Object.assign({}, config.params, token)
|
||||
// 如果要求携带在请求头中
|
||||
// config.headers = Object.assign({}, config.headers, operate.uploadParameters())
|
||||
config.headers['Content-Type'] = 'application/json;charset=utf-8'
|
||||
// 设置请求头
|
||||
config.headers['token'] = token
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
@ -98,14 +132,8 @@ export const createAxios = (config?: AxiosRequestConfig): AxiosInstance => {
|
||||
default:
|
||||
error.message = `连接错误${error.response.status}`
|
||||
}
|
||||
} else {
|
||||
// 超时处理
|
||||
if (JSON.stringify(error).includes('timeout')) {
|
||||
error.message = '服务器响应超时,请刷新当前页'
|
||||
} else {
|
||||
error.message = '连接服务器失败'
|
||||
}
|
||||
}
|
||||
if (isTest) return Promise.resolve(error.response)
|
||||
window.$message.error(error.message)
|
||||
/***** 处理结束 *****/
|
||||
return Promise.resolve(error.response)
|
||||
|
@ -3,50 +3,233 @@
|
||||
* 注意:请使用TSDoc规范进行注释,以便在使用时能够获得良好提示。
|
||||
* @see TSDoc规范https://tsdoc.org/
|
||||
**/
|
||||
import { MsgEnum, RCodeEnum, RoomTypeEnum } from '@/enums'
|
||||
import { ActEnum, IsYetEnum, MarkEnum, MsgEnum, OnlineEnum, RoomTypeEnum, SexEnum } from '@/enums'
|
||||
|
||||
/**响应请求体*/
|
||||
export type Response = {
|
||||
code: RCodeEnum
|
||||
msg: string
|
||||
data: {
|
||||
records: any
|
||||
total: number
|
||||
}
|
||||
fail: boolean
|
||||
/** 成功标识true or false */
|
||||
success: boolean
|
||||
version: string
|
||||
/** 错误码 */
|
||||
errCode: number
|
||||
/** 错误消息 */
|
||||
errMsg: string
|
||||
/** 数据 */
|
||||
data: any
|
||||
}
|
||||
/**分页搜索*/
|
||||
export type parameter = {
|
||||
pageNum: number
|
||||
pageSize: number
|
||||
|
||||
/* ======================================================== */
|
||||
export type ListResponse<T> = {
|
||||
/** 游标(下次翻页带上这参数)*/
|
||||
cursor: string
|
||||
/** 是否最后一页 */
|
||||
isLast: boolean
|
||||
list: T[]
|
||||
}
|
||||
|
||||
export type CacheBadgeReq = {
|
||||
/** 最后更新时间 更新超过 10 分钟异步去更新。 */
|
||||
lastModifyTime?: number
|
||||
/** 徽章 ID */
|
||||
itemId: number
|
||||
}
|
||||
|
||||
export type GroupDetailReq = {
|
||||
/** 群头像 */
|
||||
avatar: string
|
||||
/** 群名称 */
|
||||
groupName: string
|
||||
/** 在线人数 */
|
||||
onlineNum: number
|
||||
/** 成员角色 1群主 2管理员 3普通成员 4踢出群聊 */
|
||||
role: number
|
||||
/** 房间id */
|
||||
roomId: number
|
||||
}
|
||||
|
||||
export type CacheBadgeItem = {
|
||||
/** 是否需要更新数据源。 */
|
||||
needRefresh?: boolean
|
||||
/** 最后更新时间 更新超过 10 分钟异步去更新。 */
|
||||
lastModifyTime: number
|
||||
/** 徽章说明 */
|
||||
describe: string
|
||||
/** 徽章图标 */
|
||||
img: string
|
||||
/** 徽章 ID */
|
||||
itemId: number
|
||||
}
|
||||
|
||||
export type CacheUserReq = {
|
||||
/** 最后更新时间 更新超过 10 分钟异步去更新。 */
|
||||
lastModifyTime?: number
|
||||
/** uid */
|
||||
uid: number
|
||||
}
|
||||
|
||||
export type CacheUserItem = {
|
||||
/** 是否需要更新数据源。 */
|
||||
needRefresh?: boolean
|
||||
/** 最后更新时间 更新超过 10 分钟异步去更新。 */
|
||||
lastModifyTime: number
|
||||
/** 获得的徽章 */
|
||||
itemIds: number[]
|
||||
/** 佩戴的徽章 */
|
||||
wearingItemId: number
|
||||
/** 归属地 */
|
||||
locPlace: string
|
||||
/** 头像 */
|
||||
avatar: string
|
||||
/** 最后一次上下线时间 */
|
||||
lastOptTime: number
|
||||
/** 用户名称 */
|
||||
name: string
|
||||
}
|
||||
/**登录类型*/
|
||||
export type login = {
|
||||
username: string
|
||||
password: string
|
||||
/** uid */
|
||||
uid: number
|
||||
}
|
||||
|
||||
/** ===================================================== */
|
||||
/** 回复类型 */
|
||||
export type ReplyType = {
|
||||
id: number
|
||||
username: string
|
||||
type: MsgEnum
|
||||
/** 根据不同类型回复的消息展示也不同-`过渡版` */
|
||||
body: any
|
||||
/**
|
||||
* 是否可消息跳转
|
||||
* @enum {number} `0`否 `1`是
|
||||
*/
|
||||
export type UserItem = {
|
||||
/** 在线状态 */
|
||||
activeStatus: OnlineEnum
|
||||
/** 头像 */
|
||||
avatar: string
|
||||
/** 最后一次上下线时间 */
|
||||
lastOptTime: number
|
||||
/** 用户名称 */
|
||||
name: string
|
||||
/** 角色ID */
|
||||
roleId?: number
|
||||
/** uid */
|
||||
uid: number
|
||||
}
|
||||
|
||||
export type GroupStatisticType = {
|
||||
/** 在线人数 */
|
||||
onlineNum: number
|
||||
/** 总人数 */
|
||||
totalNum: number
|
||||
}
|
||||
|
||||
export type MessageReplyType = {
|
||||
/** 是否可消息跳转 0否 1是 */
|
||||
canCallback: number
|
||||
/** 跳转间隔的消息条数 */
|
||||
/** 是否可消息跳转 0否 1是 */
|
||||
content: string
|
||||
/** 跳转间隔的消息条数 */
|
||||
gapCount: number
|
||||
/** 消息id */
|
||||
id: number
|
||||
/** 用户名称 */
|
||||
username: string
|
||||
}
|
||||
|
||||
/** 消息互动信息 */
|
||||
export type MarkMsgReq = {
|
||||
// actType 动作类型 1确认 2取消
|
||||
actType: ActEnum
|
||||
// 标记类型 1点赞 2举报
|
||||
markType: MarkEnum
|
||||
// 消息 ID
|
||||
msgId: number
|
||||
}
|
||||
|
||||
export type UserInfoType = {
|
||||
/** 用户唯一标识 */
|
||||
uid: number
|
||||
/** 用户头像 */
|
||||
avatar: string
|
||||
/** 用户名 */
|
||||
name: string
|
||||
/** 剩余改名次数 */
|
||||
modifyNameChance: number
|
||||
/** 性别 1为男性,2为女性 */
|
||||
sex: SexEnum
|
||||
/** 徽章,本地字段,有值用本地,无值用远端 */
|
||||
badge?: string
|
||||
/** 权限 */
|
||||
power?: number
|
||||
}
|
||||
|
||||
export type BadgeType = {
|
||||
// 徽章描述
|
||||
describe: string
|
||||
// 徽章id
|
||||
id: number
|
||||
// 徽章图标
|
||||
img: string
|
||||
// 是否拥有 0否 1是
|
||||
obtain: IsYetEnum
|
||||
// 是否佩戴 0否 1是
|
||||
wearing: IsYetEnum
|
||||
}
|
||||
|
||||
export type MarkItemType = {
|
||||
/** 操作用户 */
|
||||
uid: number
|
||||
/** 消息id */
|
||||
msgId: number
|
||||
/** 操作类型 1点赞 2举报 */
|
||||
markType: MarkEnum
|
||||
/** 数量 */
|
||||
markCount: number
|
||||
/** 动作类型 1确认 2取消 */
|
||||
actType: ActEnum
|
||||
}
|
||||
|
||||
export type RevokedMsgType = {
|
||||
/** 消息ID */
|
||||
msgId: number
|
||||
/** 会话ID */
|
||||
roomId?: number
|
||||
/** 撤回人ID */
|
||||
recallUid?: number
|
||||
}
|
||||
|
||||
export type EmojiItem = {
|
||||
expressionUrl: string
|
||||
id: number
|
||||
}
|
||||
|
||||
// -------------------- ⬇消息体类型定义⬇ ----------------
|
||||
|
||||
/**
|
||||
* 消息返回体
|
||||
*/
|
||||
export type MessageType = {
|
||||
/** 发送者信息 */
|
||||
fromUser: MsgUserType
|
||||
/** 消息主体 */
|
||||
message: MsgType
|
||||
/** 发送时间 */
|
||||
sendTime: string
|
||||
/** 时间段(可选) */
|
||||
timeBlock?: string
|
||||
/** 是否加载中 */
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息中用户信息
|
||||
*/
|
||||
export type MsgUserType = {
|
||||
/** 用户ID */
|
||||
uid: number
|
||||
/** 用户名 */
|
||||
username: string
|
||||
/** 头像 */
|
||||
avatar: string
|
||||
/** 归属地 */
|
||||
locPlace: string
|
||||
/** 徽章 */
|
||||
badge?: {
|
||||
/** 徽章地址 */
|
||||
img: string
|
||||
/** 描述 */
|
||||
describe: string // 描述
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息互动信息
|
||||
*/
|
||||
export type MessageMarkType = {
|
||||
/** 点赞 */
|
||||
userLike: number
|
||||
@ -109,7 +292,9 @@ export type EmojiBody = {
|
||||
url: string
|
||||
}
|
||||
|
||||
/** 消息内容 */
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
export type MsgType = {
|
||||
/** 消息ID */
|
||||
id: number
|
||||
@ -125,32 +310,115 @@ export type MsgType = {
|
||||
messageMark: MessageMarkType
|
||||
}
|
||||
|
||||
/** 缓存用户项 */
|
||||
export type CacheUserItem = {
|
||||
/** 是否需要更新数据源。 */
|
||||
needRefresh?: boolean
|
||||
/** 最后更新时间 更新超过 10 分钟异步去更新。 */
|
||||
lastModifyTime: number
|
||||
/** 获得的徽章 */
|
||||
itemIds: number[]
|
||||
/** 佩戴的徽章 */
|
||||
wearingItemId: number
|
||||
/** 归属地 */
|
||||
locPlace: string
|
||||
/** 头像 */
|
||||
avatar: string
|
||||
export type ReplyType = {
|
||||
id: number
|
||||
username: string
|
||||
type: MsgEnum
|
||||
/** 根据不同类型回复的消息展示也不同-`过渡版` */
|
||||
body: any
|
||||
/**
|
||||
* 是否可消息跳转
|
||||
* @enum {number} `0`否 `1`是
|
||||
*/
|
||||
canCallback: number
|
||||
/** 跳转间隔的消息条数 */
|
||||
gapCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息载体
|
||||
*/
|
||||
export type MessageReq = {
|
||||
/** 会话id */
|
||||
roomId: number
|
||||
/** 消息类型 */
|
||||
msgType: MsgEnum
|
||||
/** 消息体 */
|
||||
body: {
|
||||
/** 文本消息内容 */
|
||||
content?: string
|
||||
/** 回复的消息id */
|
||||
replyMsgId?: number
|
||||
/** 任意 */
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
/** 申请状态 */
|
||||
export enum RequestFriendAgreeStatus {
|
||||
/** 1待审批 */
|
||||
Waiting = 1,
|
||||
/** 2同意 */
|
||||
Agree
|
||||
}
|
||||
|
||||
/** 请求添加好友的列表项 */
|
||||
export type RequestFriendItem = {
|
||||
/** 申请id */
|
||||
applyId: number
|
||||
/** 申请信息 */
|
||||
msg: string
|
||||
/** 申请状态 1待审批 2同意 */
|
||||
status: RequestFriendAgreeStatus
|
||||
/** 申请类型 1加好友 */
|
||||
type: number
|
||||
/** 申请人uid */
|
||||
uid: number
|
||||
/** 会话 ID */
|
||||
roomId: number
|
||||
}
|
||||
/** 联系人的列表项 */
|
||||
export type ContactItem = {
|
||||
/** 在线状态 1在线 2离线 */
|
||||
activeStatus: OnlineEnum
|
||||
/** 最后一次上下线时间 */
|
||||
lastOptTime: number
|
||||
/** 用户名称 */
|
||||
name: string
|
||||
/** uid */
|
||||
uid: number
|
||||
}
|
||||
|
||||
/** 是否全员展示的会话 0否 1是 */
|
||||
export enum IsAllUserEnum {
|
||||
/** 0否 */
|
||||
Not,
|
||||
/** 1是 */
|
||||
Yes
|
||||
}
|
||||
|
||||
/** 会话列表项 */
|
||||
export type SessionItem = {
|
||||
/** 房间最后活跃时间(用来排序) */
|
||||
activeTime: number
|
||||
/** 会话头像 */
|
||||
avatar: string
|
||||
/** 是否全员展示的会话 0否 1是 */
|
||||
hot_Flag: IsAllUserEnum
|
||||
/** 会话名称 */
|
||||
name: string
|
||||
/** 房间id */
|
||||
roomId: number
|
||||
/** 最新消息 */
|
||||
text: string
|
||||
/** 房间类型 1群聊 2单聊 */
|
||||
type: RoomTypeEnum
|
||||
/** 未读数 */
|
||||
unreadCount: number
|
||||
}
|
||||
|
||||
/** 消息已读未读数列表项 */
|
||||
export type MsgReadUnReadCountType = {
|
||||
/** 消息 ID */
|
||||
msgId: number
|
||||
/** 已读数 */
|
||||
readCount: number
|
||||
/** 未读数 */
|
||||
unReadCount: number | null
|
||||
}
|
||||
/* ======================================================== */
|
||||
/**! 模拟信息数据的类型 */
|
||||
export type MockItem = {
|
||||
key: number
|
||||
type: RoomTypeEnum
|
||||
avatar: string
|
||||
accountId: string
|
||||
accountId: number
|
||||
accountName: string
|
||||
}
|
||||
|
@ -5,7 +5,43 @@ const { PROD, VITE_SERVICE_URL } = import.meta.env
|
||||
const prefix = PROD ? VITE_SERVICE_URL : ''
|
||||
|
||||
export default {
|
||||
login: `${prefix + URLEnum.USER}/login`,
|
||||
logout: `${prefix + URLEnum.USER}/logout`,
|
||||
articlePage: `${prefix + URLEnum.ARTICLE}/page`
|
||||
getUserInfo: `${prefix + URLEnum.USER}/userInfo`, // 获取用户信息
|
||||
getBadgeList: `${prefix + URLEnum.USER}/badges`, // 获取徽章列表
|
||||
getMemberStatistic: `${prefix + URLEnum.CHAT}/public/member/statistic`,
|
||||
getUserInfoBatch: `${prefix + URLEnum.USER}/public/summary/userInfo/batch`,
|
||||
getBadgesBatch: `${prefix + URLEnum.USER}/public/badges/batch`,
|
||||
getAllUserBaseInfo: `${prefix + URLEnum.ROOM}/group/member/list`, // 房间内的所有群成员列表-@专用
|
||||
getMsgList: `${prefix + URLEnum.CHAT}/public/msg/page`,
|
||||
sendMsg: `${prefix + URLEnum.CHAT}/msg`,
|
||||
getUserInfoDetail: `${prefix + URLEnum.USER}/userInfo`, // 获取用户信息详情
|
||||
modifyUserName: `${prefix + URLEnum.USER}/name`, // 修改用户名
|
||||
setUserBadge: `${prefix + URLEnum.USER}/badge`, // 设置用户徽章
|
||||
markMsg: `${prefix + URLEnum.CHAT}/msg/mark`, // 消息标记
|
||||
blockUser: `${prefix + URLEnum.USER}/black`, // 拉黑用户
|
||||
recallMsg: `${prefix + URLEnum.CHAT}/msg/recall`, // 撤回消息
|
||||
fileUpload: `${prefix + URLEnum.OSS}/upload/url`, // 文件上传
|
||||
addEmoji: `${prefix + URLEnum.USER}/emoji`, // 增加表情
|
||||
deleteEmoji: `${prefix + URLEnum.USER}/emoji`, // 删除表情
|
||||
getEmoji: `${prefix + URLEnum.USER}/emoji/list`, // 查询表情包
|
||||
|
||||
// -------------- 好友相关 ---------------
|
||||
getContactList: `${prefix + URLEnum.USER}/friend/page`, // 联系人列表
|
||||
requestFriendList: `${prefix + URLEnum.USER}/friend/apply/page`, // 好友申请列表
|
||||
sendAddFriendRequest: `${prefix + URLEnum.USER}/friend/apply`, // 申请好友
|
||||
deleteFriend: `${prefix + URLEnum.USER}/friend`, // 删除好友
|
||||
newFriendCount: `${prefix + URLEnum.USER}/friend/apply/unread`, // 申请未读数
|
||||
|
||||
// -------------- 聊天室相关 ---------------
|
||||
getSessionList: `${prefix + URLEnum.CHAT}/public/contact/page`, // 会话列表
|
||||
getMsgReadList: `${prefix + URLEnum.CHAT}/msg/read/page`, // 消息的已读未读列表
|
||||
getMsgReadCount: `${prefix + URLEnum.CHAT}/msg/read`, // 消息已读未读数
|
||||
createGroup: `${prefix + URLEnum.ROOM}/group`, // 新增群组
|
||||
getGroupUserList: `${prefix + URLEnum.ROOM}/public/group/member/page`,
|
||||
inviteGroupMember: `${prefix + URLEnum.ROOM}/group/member`, // 邀请群成员
|
||||
exitGroup: `${prefix + URLEnum.ROOM}/group/member/exit`, // 退群
|
||||
addAdmin: `${prefix + URLEnum.ROOM}/group/admin`, // 添加管理员
|
||||
revokeAdmin: `${prefix + URLEnum.ROOM}/group/admin`, // 添加管理员
|
||||
groupDetail: `${prefix + URLEnum.ROOM}/public/group`, // 群组详情
|
||||
sessionDetail: `${prefix + URLEnum.CHAT}/public/contact/detail`, // 会话详情
|
||||
sessionDetailWithFriends: `${prefix + URLEnum.CHAT}/public/contact/detail/friend` // 会话详情(联系人列表发消息用)
|
||||
}
|
||||
|
@ -1,51 +1,246 @@
|
||||
import { WsReqEnum, WsResEnum } from '@/enums'
|
||||
import { useWsLoginStore, LoginStatus } from '@/stores/ws'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useEmojiStore } from '@/stores/emoji'
|
||||
import { WsResponseMessageType } from '@/utils/wsType'
|
||||
import type { LoginSuccessResType, LoginInitResType, WsReqMsgContentType, OnStatusChangeType } from '@/utils/wsType'
|
||||
import type { MessageType, MarkItemType, RevokedMsgType } from '@/services/types'
|
||||
import { OnlineEnum, ChangeTypeEnum, RoomTypeEnum, WsResEnum, MittEnum } from '@/enums'
|
||||
import { computedToken } from '@/services/request'
|
||||
import { worker } from '@/utils/InitWorker.ts'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
|
||||
const { VITE_WEBSOCKET_URL } = import.meta.env
|
||||
/** websocket连接对象 */
|
||||
let ws: WebSocket
|
||||
/** 初始化websocket连接 */
|
||||
const initWebSocket = () => {
|
||||
ws = new WebSocket(`${VITE_WEBSOCKET_URL}/`)
|
||||
ws.onopen = () => {
|
||||
// 发送心跳
|
||||
setInterval(() => {
|
||||
sendToServer({
|
||||
type: WsReqEnum.HEARTBEAT
|
||||
})
|
||||
}, 1000 * 60)
|
||||
class WS {
|
||||
#tasks: WsReqMsgContentType[] = []
|
||||
// 重连🔐
|
||||
#connectReady = false
|
||||
|
||||
constructor() {
|
||||
this.initConnect()
|
||||
// 收到消息
|
||||
worker.addEventListener('message', this.onWorkerMsg)
|
||||
|
||||
// 后台重试次数达到上限之后,tab 获取焦点再重试
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden && !this.#connectReady) {
|
||||
this.initConnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听服务器返回的消息
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
const data: Record<string, any> = JSON.parse(event.data)
|
||||
switch (data.type) {
|
||||
case WsReqEnum.LOGIN:
|
||||
Mitt.emit(WsResEnum.QRCODE_LOGIN, data)
|
||||
initConnect = () => {
|
||||
const token = localStorage.getItem('TOKEN')
|
||||
// 如果token 是 null, 而且 localStorage 的用户信息有值,需要清空用户信息
|
||||
if (token === null && localStorage.getItem('USER_INFO')) {
|
||||
localStorage.removeItem('USER_INFO')
|
||||
}
|
||||
// 初始化 ws
|
||||
worker.postMessage(`{"type":"initWS","value":${token ? `"${token}"` : null}}`)
|
||||
}
|
||||
|
||||
onWorkerMsg = (e: MessageEvent<any>) => {
|
||||
const params: { type: string; value: unknown } = JSON.parse(e.data)
|
||||
switch (params.type) {
|
||||
case 'message': {
|
||||
this.onMessage(params.value as string)
|
||||
break
|
||||
case WsReqEnum.HEARTBEAT:
|
||||
}
|
||||
case 'open': {
|
||||
this.#dealTasks()
|
||||
break
|
||||
case WsReqEnum.AUTHORIZE:
|
||||
Mitt.emit(WsResEnum.LOGIN_SUCCESS, data)
|
||||
}
|
||||
case 'close':
|
||||
case 'error': {
|
||||
this.#onClose()
|
||||
break
|
||||
}
|
||||
case WsResEnum.WS_ERROR:
|
||||
Mitt.emit(WsResEnum.WS_ERROR, params.value)
|
||||
localStorage.removeItem('wsLogin')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// websocket出错重连
|
||||
ws.onerror = () => {
|
||||
// websocket出错重连
|
||||
initWebSocket()
|
||||
// 重置一些属性
|
||||
#onClose = () => {
|
||||
this.#connectReady = false
|
||||
}
|
||||
|
||||
#dealTasks = () => {
|
||||
this.#connectReady = true
|
||||
// 先探测登录态
|
||||
// this.#detectionLoginStatus()
|
||||
|
||||
setTimeout(() => {
|
||||
const userStore = useUserStore()
|
||||
if (userStore.isSign) {
|
||||
// 处理堆积的任务
|
||||
this.#tasks.forEach((task) => {
|
||||
this.send(task)
|
||||
})
|
||||
// 清空缓存的消息
|
||||
this.#tasks = []
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
#send(msg: WsReqMsgContentType) {
|
||||
worker.postMessage(`{"type":"message","value":${typeof msg === 'string' ? msg : JSON.stringify(msg)}}`)
|
||||
}
|
||||
|
||||
send = (params: WsReqMsgContentType) => {
|
||||
if (this.#connectReady) {
|
||||
this.#send(params)
|
||||
} else {
|
||||
// 放到队列
|
||||
this.#tasks.push(params)
|
||||
}
|
||||
}
|
||||
|
||||
// 收到消息回调
|
||||
onMessage = (value: string) => {
|
||||
// FIXME 可能需要 try catch,
|
||||
const params: { type: WsResponseMessageType; data: unknown } = JSON.parse(value)
|
||||
const loginStore = useWsLoginStore()
|
||||
const userStore = useUserStore()
|
||||
const chatStore = useChatStore()
|
||||
const groupStore = useGroupStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const emojiStore = useEmojiStore()
|
||||
switch (params.type) {
|
||||
// 获取登录二维码
|
||||
case WsResponseMessageType.LoginQrCode: {
|
||||
const data = params.data as LoginInitResType
|
||||
loginStore.loginQrCode = data.loginUrl
|
||||
Mitt.emit(WsResEnum.QRCODE_LOGIN)
|
||||
break
|
||||
}
|
||||
// 等待授权
|
||||
case WsResponseMessageType.WaitingAuthorize: {
|
||||
loginStore.loginStatus = LoginStatus.Waiting
|
||||
break
|
||||
}
|
||||
// 登录成功
|
||||
case WsResponseMessageType.LoginSuccess: {
|
||||
userStore.isSign = true
|
||||
const { token, ...rest } = params.data as LoginSuccessResType
|
||||
Mitt.emit(WsResEnum.LOGIN_SUCCESS, params.data)
|
||||
// FIXME 可以不需要赋值了,单独请求了接口。
|
||||
userStore.userInfo = { ...userStore.userInfo, ...rest }
|
||||
localStorage.setItem('USER_INFO', JSON.stringify(rest))
|
||||
localStorage.setItem('TOKEN', token)
|
||||
localStorage.removeItem('wsLogin')
|
||||
// 更新一下请求里面的 token.
|
||||
computedToken.clear()
|
||||
computedToken.get()
|
||||
// 获取用户详情
|
||||
userStore.getUserDetailAction()
|
||||
// 自己更新自己上线
|
||||
groupStore.batchUpdateUserStatus([
|
||||
{
|
||||
activeStatus: OnlineEnum.ONLINE,
|
||||
avatar: rest.avatar,
|
||||
lastOptTime: Date.now(),
|
||||
name: rest.name,
|
||||
uid: rest.uid
|
||||
}
|
||||
])
|
||||
// 获取用户详情
|
||||
chatStore.getSessionList(true)
|
||||
// 自定义表情列表
|
||||
emojiStore.getEmojiList()
|
||||
break
|
||||
}
|
||||
// 收到消息
|
||||
case WsResponseMessageType.ReceiveMessage: {
|
||||
chatStore.pushMsg(params.data as MessageType)
|
||||
Mitt.emit(MittEnum.SEND_MESSAGE, params.data)
|
||||
break
|
||||
}
|
||||
// 用户下线
|
||||
case WsResponseMessageType.OnOffLine: {
|
||||
const data = params.data as OnStatusChangeType
|
||||
groupStore.countInfo.onlineNum = data.onlineNum
|
||||
// groupStore.countInfo.totalNum = data.totalNum
|
||||
groupStore.batchUpdateUserStatus(data.changeList)
|
||||
break
|
||||
}
|
||||
// 用户 token 过期
|
||||
case WsResponseMessageType.TokenExpired: {
|
||||
userStore.isSign = false
|
||||
userStore.userInfo = {}
|
||||
localStorage.removeItem('USER_INFO')
|
||||
localStorage.removeItem('TOKEN')
|
||||
loginStore.loginStatus = LoginStatus.Init
|
||||
break
|
||||
}
|
||||
// 小黑子的发言在禁用后,要删除他的发言
|
||||
case WsResponseMessageType.InValidUser: {
|
||||
const data = params.data as { uid: number }
|
||||
// 消息列表删掉小黑子发言
|
||||
chatStore.filterUser(data.uid)
|
||||
// 群成员列表删掉小黑子
|
||||
groupStore.filterUser(data.uid)
|
||||
break
|
||||
}
|
||||
// 点赞、倒赞消息通知
|
||||
case WsResponseMessageType.WSMsgMarkItem: {
|
||||
const data = params.data as { markList: MarkItemType[] }
|
||||
chatStore.updateMarkCount(data.markList)
|
||||
break
|
||||
}
|
||||
// 消息撤回通知
|
||||
case WsResponseMessageType.WSMsgRecall: {
|
||||
const { data } = params as { data: RevokedMsgType }
|
||||
chatStore.updateRecallStatus(data)
|
||||
break
|
||||
}
|
||||
// 新好友申请
|
||||
case WsResponseMessageType.RequestNewFriend: {
|
||||
const data = params.data as { uid: number; unreadCount: number }
|
||||
globalStore.unReadMark.newFriendUnreadCount += data.unreadCount
|
||||
// notify({
|
||||
// name: '新好友',
|
||||
// text: '您有一个新好友, 快来看看~',
|
||||
// onClick: () => {
|
||||
// Router.push('/contact')
|
||||
// }
|
||||
// })
|
||||
break
|
||||
}
|
||||
// 新好友申请
|
||||
case WsResponseMessageType.NewFriendSession: {
|
||||
// changeType 1 加入群组,2: 移除群组
|
||||
const data = params.data as {
|
||||
roomId: number
|
||||
uid: number
|
||||
changeType: ChangeTypeEnum
|
||||
activeStatus: OnlineEnum
|
||||
lastOptTime: number
|
||||
}
|
||||
if (
|
||||
data.roomId === globalStore.currentSession.roomId &&
|
||||
globalStore.currentSession.type === RoomTypeEnum.GROUP
|
||||
) {
|
||||
if (data.changeType === ChangeTypeEnum.REMOVE) {
|
||||
// 移除群成员
|
||||
groupStore.filterUser(data.uid)
|
||||
// TODO 添加一条退出群聊的消息
|
||||
} else {
|
||||
// TODO 添加群成员
|
||||
// TODO 添加一条入群的消息
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
console.log('接收到未处理类型的消息:', params)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送json数据至服务器
|
||||
*
|
||||
* @param data 传输的json数据对象
|
||||
*/
|
||||
const sendToServer = (data: Record<string, any>) => {
|
||||
const json = JSON.stringify(data)
|
||||
ws.send(json)
|
||||
}
|
||||
|
||||
export { initWebSocket, sendToServer }
|
||||
export default new WS()
|
||||
|
127
src/stores/cached.ts
Normal file
127
src/stores/cached.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import apis from '@/services/apis'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import type { CacheBadgeItem, CacheUserItem } from '@/services/types'
|
||||
import { isDiffNow10Min } from '@/utils/ComputedTime.ts'
|
||||
|
||||
export type BaseUserItem = Pick<CacheUserItem, 'uid' | 'avatar' | 'name'>
|
||||
|
||||
export const useCachedStore = defineStore('cached', () => {
|
||||
const globalStore = useGlobalStore()
|
||||
const userCachedList = reactive<Record<number, Partial<CacheUserItem>>>({})
|
||||
const badgeCachedList = reactive<Record<number, Partial<CacheBadgeItem>>>({})
|
||||
|
||||
const currentRoomId = computed(() => globalStore.currentSession?.roomId)
|
||||
|
||||
const atUsersMap = reactive<Record<number, BaseUserItem[]>>({ [currentRoomId.value]: [] }) // 消息Map
|
||||
|
||||
const currentAtUsersList = computed({
|
||||
get: () => {
|
||||
const current = atUsersMap[currentRoomId.value]
|
||||
if (current === undefined) {
|
||||
atUsersMap[currentRoomId.value] = []
|
||||
}
|
||||
if (currentRoomId.value === 1) {
|
||||
return Object.values(userCachedList as BaseUserItem[])
|
||||
}
|
||||
return atUsersMap[currentRoomId.value]
|
||||
},
|
||||
set: (val) => {
|
||||
atUsersMap[currentRoomId.value] = val
|
||||
}
|
||||
})
|
||||
|
||||
/** 批量获取用户详细信息 */
|
||||
const getBatchUserInfo = async (uids: number[]) => {
|
||||
// 没有 lastModifyTime 的要更新,lastModifyTime 距离现在 10 分钟已上的也要更新
|
||||
const result = uids
|
||||
.map((uid) => {
|
||||
const cacheUser = userCachedList[uid]
|
||||
return { uid, lastModifyTime: cacheUser?.lastModifyTime }
|
||||
})
|
||||
.filter((item) => !item.lastModifyTime || isDiffNow10Min(item.lastModifyTime))
|
||||
if (!result.length) return
|
||||
const itemIdSet: Set<number> = new Set()
|
||||
const { data } = await apis.getUserInfoBatch(result)
|
||||
data?.forEach((item: CacheUserItem) => {
|
||||
// 更新最后更新时间。
|
||||
userCachedList[item.uid] = {
|
||||
...(item?.needRefresh ? item : userCachedList[item.uid]),
|
||||
needRefresh: undefined,
|
||||
lastModifyTime: Date.now()
|
||||
}
|
||||
|
||||
// 收集徽章 id并缓存
|
||||
// 可以改成 itemIds,可以更快收集完成。
|
||||
const wearingItemId = item.wearingItemId
|
||||
wearingItemId && itemIdSet.add(wearingItemId)
|
||||
})
|
||||
// 批量请求徽章详情
|
||||
await getBatchBadgeInfo([...itemIdSet])
|
||||
}
|
||||
|
||||
/** 批量获取用户徽章详细信息 */
|
||||
const getBatchBadgeInfo = async (itemIds: number[]) => {
|
||||
// 没有 lastModifyTime 的要更新,lastModifyTime 距离现在 10 分钟已上的也要更新
|
||||
const result = itemIds
|
||||
.map((itemId) => {
|
||||
const cacheBadge = badgeCachedList[itemId]
|
||||
return { itemId, lastModifyTime: cacheBadge?.lastModifyTime }
|
||||
})
|
||||
.filter((item) => !item.lastModifyTime || isDiffNow10Min(item.lastModifyTime))
|
||||
if (!result.length) return
|
||||
const { data } = await apis.getBadgesBatch(result)
|
||||
data?.forEach(
|
||||
(item: CacheBadgeItem) =>
|
||||
// 更新最后更新时间。
|
||||
(badgeCachedList[item.itemId] = {
|
||||
...(item?.needRefresh ? item : badgeCachedList[item.itemId]),
|
||||
needRefresh: undefined,
|
||||
lastModifyTime: Date.now()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/** 房间内的所有群成员列表-@专用 */
|
||||
const initAllUserBaseInfo = async () => {
|
||||
if (localStorage.getItem('IS_INIT_USER_BASE') === null) {
|
||||
const { data } = await apis.getAllUserBaseInfo({ params: { roomId: currentRoomId.value } })
|
||||
data?.forEach((item: CacheUserItem) => (userCachedList[item.uid] = item))
|
||||
localStorage.setItem('IS_INIT_USER_BASE', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
const getGroupAtUserBaseInfo = async () => {
|
||||
if (currentRoomId.value === 1) return
|
||||
const { data } = await apis.getAllUserBaseInfo({ params: { roomId: currentRoomId.value } })
|
||||
currentAtUsersList.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名关键字过滤用户
|
||||
* @param searchKey 检索的关键字
|
||||
*/
|
||||
// const filterUsers = (searchKey: string) => {
|
||||
// // 需要过滤自己
|
||||
// return currentAtUsersList.value?.filter((item) => item.name?.startsWith(searchKey))
|
||||
// }
|
||||
|
||||
/**
|
||||
* 通过用户ID列表获取用户基本信息
|
||||
* @param uidList
|
||||
*/
|
||||
const filterUsersByUidList = (uidList: number[]) => {
|
||||
return currentAtUsersList.value.filter((user) => uidList.includes(user.uid))
|
||||
}
|
||||
|
||||
return {
|
||||
userCachedList,
|
||||
badgeCachedList,
|
||||
getBatchUserInfo,
|
||||
getBatchBadgeInfo,
|
||||
initAllUserBaseInfo,
|
||||
getGroupAtUserBaseInfo,
|
||||
currentAtUsersList,
|
||||
filterUsersByUidList
|
||||
}
|
||||
})
|
464
src/stores/chat.ts
Normal file
464
src/stores/chat.ts
Normal file
@ -0,0 +1,464 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useRoute } from 'vue-router'
|
||||
import apis from '@/services/apis'
|
||||
import type { MarkItemType, MessageType, RevokedMsgType, SessionItem } from '@/services/types'
|
||||
import { MarkEnum, MsgEnum, RoomTypeEnum } from '@/enums'
|
||||
import { computedTimeBlock } from '@/utils/ComputedTime.ts'
|
||||
import { useCachedStore } from '@/stores/cached'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { useContactStore } from '@/stores/contacts'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
export const pageSize = 20
|
||||
// 标识是否第一次请求
|
||||
let isFirstInit = false
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const route = useRoute()
|
||||
const cachedStore = useCachedStore()
|
||||
const userStore = useUserStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const groupStore = useGroupStore()
|
||||
const contactStore = useContactStore()
|
||||
const sessionList = reactive<SessionItem[]>([]) // 会话列表
|
||||
const sessionOptions = reactive({ isLast: false, isLoading: false, cursor: '' })
|
||||
|
||||
const currentRoomId = computed(() => globalStore.currentSession?.roomId)
|
||||
const currentRoomType = computed(() => globalStore.currentSession?.type)
|
||||
|
||||
const messageMap = reactive<Map<number, Map<number, MessageType>>>(new Map([[currentRoomId.value, new Map()]])) // 消息Map
|
||||
const messageOptions = reactive<Map<number, { isLast: boolean; isLoading: boolean; cursor: string }>>(
|
||||
new Map([[currentRoomId.value, { isLast: false, isLoading: false, cursor: '' }]])
|
||||
)
|
||||
const replyMapping = reactive<Map<number, Map<number, number[]>>>(new Map([[currentRoomId.value, new Map()]])) // 回复消息映射
|
||||
|
||||
const currentMessageMap = computed({
|
||||
get: () => {
|
||||
const current = messageMap.get(currentRoomId.value as number)
|
||||
if (current === undefined) {
|
||||
messageMap.set(currentRoomId.value, new Map())
|
||||
}
|
||||
return messageMap.get(currentRoomId.value as number)
|
||||
},
|
||||
set: (val) => {
|
||||
messageMap.set(currentRoomId.value, val as Map<number, MessageType>)
|
||||
}
|
||||
})
|
||||
const currentMessageOptions = computed({
|
||||
get: () => {
|
||||
const current = messageOptions.get(currentRoomId.value as number)
|
||||
if (current === undefined) {
|
||||
messageOptions.set(currentRoomId.value, { isLast: false, isLoading: true, cursor: '' })
|
||||
}
|
||||
return messageOptions.get(currentRoomId.value as number)
|
||||
},
|
||||
set: (val) => {
|
||||
messageOptions.set(currentRoomId.value, val as { isLast: boolean; isLoading: boolean; cursor: string })
|
||||
}
|
||||
})
|
||||
const currentReplyMap = computed({
|
||||
get: () => {
|
||||
const current = replyMapping.get(currentRoomId.value as number)
|
||||
if (current === undefined) {
|
||||
replyMapping.set(currentRoomId.value, new Map())
|
||||
}
|
||||
return replyMapping.get(currentRoomId.value as number)
|
||||
},
|
||||
set: (val) => {
|
||||
replyMapping.set(currentRoomId.value, val as Map<number, number[]>)
|
||||
}
|
||||
})
|
||||
const isGroup = computed(() => currentRoomType.value === RoomTypeEnum.GROUP)
|
||||
/**
|
||||
* 获取当前会话信息
|
||||
*/
|
||||
const currentSessionInfo = computed(() =>
|
||||
sessionList.find((session) => session.roomId === globalStore.currentSession.roomId)
|
||||
)
|
||||
|
||||
const chatListToBottomAction = ref<() => void>() // 外部提供消息列表滚动到底部事件
|
||||
|
||||
// const isStartCount = ref(false) // 是否开始计数
|
||||
// const newMsgCount = ref(0) // 新消息计数
|
||||
|
||||
const newMsgCount = reactive<Map<number, { count: number; isStart: boolean }>>(
|
||||
new Map([
|
||||
[
|
||||
currentRoomId.value,
|
||||
{
|
||||
// 新消息计数
|
||||
count: 0,
|
||||
// 是否开始计数
|
||||
isStart: false
|
||||
}
|
||||
]
|
||||
])
|
||||
)
|
||||
const currentNewMsgCount = computed({
|
||||
get: () => {
|
||||
const current = newMsgCount.get(currentRoomId.value as number)
|
||||
if (current === undefined) {
|
||||
newMsgCount.set(currentRoomId.value, { count: 0, isStart: false })
|
||||
}
|
||||
return newMsgCount.get(currentRoomId.value as number)
|
||||
},
|
||||
set: (val) => {
|
||||
newMsgCount.set(currentRoomId.value, val as { count: number; isStart: boolean })
|
||||
}
|
||||
})
|
||||
|
||||
watch(currentRoomId, (val, oldVal) => {
|
||||
if (oldVal !== undefined && val !== oldVal) {
|
||||
// // 切换会话,滚动到底部
|
||||
chatListToBottomAction.value?.()
|
||||
// 切换的 rooms是空数据的话就请求消息列表
|
||||
if (!currentMessageMap.value || currentMessageMap.value.size === 0) {
|
||||
if (!currentMessageMap.value) {
|
||||
messageMap.set(currentRoomId.value as number, new Map())
|
||||
}
|
||||
getMsgList()
|
||||
}
|
||||
|
||||
// 群组的时候去请求
|
||||
if (currentRoomType.value === RoomTypeEnum.GROUP) {
|
||||
groupStore.getGroupUserList(true)
|
||||
groupStore.getCountStatistic()
|
||||
cachedStore.getGroupAtUserBaseInfo()
|
||||
}
|
||||
}
|
||||
|
||||
// 重置当前回复的消息
|
||||
currentMsgReply.value = {}
|
||||
})
|
||||
|
||||
// 当前消息回复
|
||||
const currentMsgReply = ref<Partial<MessageType>>({})
|
||||
|
||||
// 将消息列表转换为数组
|
||||
const chatMessageList = computed(() => [...(currentMessageMap.value?.values() || [])])
|
||||
|
||||
const getMsgList = async (size = pageSize) => {
|
||||
currentMessageOptions.value && (currentMessageOptions.value.isLoading = true)
|
||||
const { data } = await apis
|
||||
.getMsgList({
|
||||
params: {
|
||||
pageSize: size,
|
||||
cursor: currentMessageOptions.value?.cursor,
|
||||
roomId: currentRoomId.value
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
currentMessageOptions.value && (currentMessageOptions.value.isLoading = false)
|
||||
})
|
||||
if (!data) return
|
||||
const computedList = computedTimeBlock(data.list)
|
||||
|
||||
/** 收集需要请求用户详情的 uid */
|
||||
const uidCollectYet: Set<number> = new Set() // 去重用
|
||||
computedList.forEach((msg) => {
|
||||
const replyItem = msg.message.body?.reply
|
||||
if (replyItem?.id) {
|
||||
const messageIds = currentReplyMap.value?.get(replyItem.id) || []
|
||||
messageIds.push(msg.message.id)
|
||||
currentReplyMap.value?.set(replyItem.id, messageIds)
|
||||
|
||||
// 查询被回复用户的信息,被回复的用户信息里暂时无 uid
|
||||
// collectUidItem(replyItem.uid)
|
||||
}
|
||||
// 查询消息发送者的信息
|
||||
uidCollectYet.add(msg.fromUser.uid)
|
||||
})
|
||||
// 获取用户信息缓存
|
||||
await cachedStore.getBatchUserInfo([...uidCollectYet])
|
||||
// 为保证获取的历史消息在前面
|
||||
const newList = [...computedList, ...chatMessageList.value]
|
||||
currentMessageMap.value?.clear() // 清空Map
|
||||
newList.forEach((msg) => {
|
||||
currentMessageMap.value?.set(msg.message.id, msg)
|
||||
})
|
||||
|
||||
if (currentMessageOptions.value) {
|
||||
currentMessageOptions.value.cursor = data.cursor
|
||||
currentMessageOptions.value.isLast = data.isLast
|
||||
currentMessageOptions.value.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const getSessionList = async (isFresh = false) => {
|
||||
if (!isFresh && (sessionOptions.isLast || sessionOptions.isLoading)) return
|
||||
sessionOptions.isLoading = true
|
||||
const response = await apis
|
||||
.getSessionList({
|
||||
params: {
|
||||
pageSize: sessionList.length > pageSize ? sessionList.length : pageSize,
|
||||
cursor: isFresh || !sessionOptions.cursor ? undefined : sessionOptions.cursor
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
sessionOptions.isLoading = false
|
||||
})
|
||||
if (!response) return
|
||||
const { data } = response
|
||||
isFresh ? sessionList.splice(0, sessionList.length, ...data.list) : sessionList.push(...data.list)
|
||||
sessionOptions.cursor = data.cursor
|
||||
sessionOptions.isLast = data.isLast
|
||||
sessionOptions.isLoading = false
|
||||
|
||||
sortAndUniqueSessionList()
|
||||
|
||||
sessionList[0].unreadCount = 0
|
||||
if (!isFirstInit) {
|
||||
isFirstInit = true
|
||||
globalStore.currentSession.roomId = data.list[0].roomId
|
||||
globalStore.currentSession.type = data.list[0].type
|
||||
// 用会话列表第一个去请求消息列表
|
||||
await getMsgList()
|
||||
// 请求第一个群成员列表
|
||||
currentRoomType.value === RoomTypeEnum.GROUP && (await groupStore.getGroupUserList(true))
|
||||
// 初始化所有用户基本信息
|
||||
userStore.isSign && (await cachedStore.initAllUserBaseInfo())
|
||||
// 联系人列表
|
||||
await contactStore.getContactList(true)
|
||||
}
|
||||
}
|
||||
|
||||
/** 会话列表去重并排序 */
|
||||
const sortAndUniqueSessionList = () => {
|
||||
const temp: Record<string, SessionItem> = {}
|
||||
sessionList.forEach((item) => (temp[item.roomId] = item))
|
||||
sessionList.splice(0, sessionList.length, ...Object.values(temp))
|
||||
sessionList.sort((pre, cur) => cur.activeTime - pre.activeTime)
|
||||
}
|
||||
|
||||
const updateSession = (roomId: number, roomProps: Partial<SessionItem>) => {
|
||||
const session = sessionList.find((item) => item.roomId === roomId)
|
||||
session && roomProps && Object.assign(session, roomProps)
|
||||
sortAndUniqueSessionList()
|
||||
}
|
||||
|
||||
const updateSessionLastActiveTime = (roomId: number, room?: SessionItem) => {
|
||||
const session = sessionList.find((item) => item.roomId === roomId)
|
||||
if (session) {
|
||||
Object.assign(session, { activeTime: Date.now() })
|
||||
} else if (room) {
|
||||
const newItem = cloneDeep(room)
|
||||
newItem.activeTime = Date.now()
|
||||
sessionList.unshift(newItem)
|
||||
}
|
||||
sortAndUniqueSessionList()
|
||||
}
|
||||
|
||||
// 通过房间ID获取会话信息
|
||||
const getSession = (roomId: number): SessionItem => {
|
||||
return sessionList.find((item) => item.roomId === roomId) as SessionItem
|
||||
}
|
||||
|
||||
const pushMsg = async (msg: MessageType) => {
|
||||
const current = messageMap.get(msg.message.roomId)
|
||||
// TODO 操过五分钟发送信息的时候没有显示时间差的时间戳 (nyh -> 2024-05-21 00:17:15)
|
||||
current?.set(msg.message.id, msg)
|
||||
|
||||
// 获取用户信息缓存
|
||||
// 尝试取缓存user, 如果有 lastModifyTime 说明缓存过了,没有就一定是要缓存的用户了
|
||||
const uid = msg.fromUser.uid
|
||||
// const cacheUser = cachedStore.userCachedList[uid]
|
||||
await cachedStore.getBatchUserInfo([uid])
|
||||
|
||||
// 发完消息就要刷新会话列表,
|
||||
// 如果当前会话已经置顶了,可以不用刷新
|
||||
if (globalStore.currentSession && globalStore.currentSession.roomId !== msg.message.roomId) {
|
||||
let detailResponse = undefined
|
||||
// 如果当前路由不是聊天,就开始拿会话详情,并手动新增一条会话记录
|
||||
// if (route?.path && route?.path !== '/') {
|
||||
// globalStore.currentSession.roomId = msg.message.roomId
|
||||
// globalStore.currentSession.type = RoomTypeEnum.Single
|
||||
if (!current) {
|
||||
detailResponse = await apis.sessionDetail({ id: msg.message.roomId })
|
||||
}
|
||||
if (detailResponse) {
|
||||
const { data } = detailResponse
|
||||
updateSessionLastActiveTime(msg.message.roomId, data)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果收到的消息里面是艾特自己的就发送系统通知
|
||||
// if (msg.message.body.atUidList?.includes(userStore.userInfo.uid) && cacheUser) {
|
||||
// notify({
|
||||
// name: cacheUser.name as string,
|
||||
// text: msg.message.body.content,
|
||||
// icon: cacheUser.avatar as string
|
||||
// })
|
||||
// }
|
||||
|
||||
// tab 在后台获得新消息,就开始闪烁!
|
||||
// if (document.hidden && !shakeTitle.isShaking) {
|
||||
// shakeTitle.start()
|
||||
// }
|
||||
|
||||
if (
|
||||
currentNewMsgCount.value &&
|
||||
currentNewMsgCount.value?.isStart &&
|
||||
typeof currentNewMsgCount.value.count === 'number'
|
||||
) {
|
||||
currentNewMsgCount.value.count++
|
||||
return
|
||||
}
|
||||
|
||||
// 聊天记录计数
|
||||
if (currentRoomId.value !== msg.message.roomId) {
|
||||
const item = sessionList.find((item) => item.roomId === msg.message.roomId)
|
||||
if (item) {
|
||||
item.unreadCount += 1
|
||||
}
|
||||
// 如果新消息的 roomId 和 当前显示的 room 的 Id 一致,就更新已读
|
||||
} else {
|
||||
// 且当前路由在 聊天 内
|
||||
if (route?.path && route?.path === '/message') {
|
||||
await apis.markMsgRead({ roomId: currentRoomId.value })
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当前路由不是聊天,就开始计数
|
||||
if (route?.path && route?.path !== '/message') {
|
||||
globalStore.unReadMark.newMsgUnreadCount++
|
||||
}
|
||||
|
||||
// 聊天列表滚动到底部
|
||||
setTimeout(() => {
|
||||
// 如果超过一屏了,不自动滚动到最新消息。
|
||||
chatListToBottomAction.value?.()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 过滤掉小黑子的发言
|
||||
const filterUser = (uid: number) => {
|
||||
if (typeof uid !== 'number') return
|
||||
for (const messages of messageMap.values()) {
|
||||
messages?.forEach((msg) => {
|
||||
if (msg.fromUser.uid === uid) {
|
||||
messages.delete(msg.message.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const loadMore = async (size?: number) => {
|
||||
if (currentMessageOptions.value?.isLast || currentMessageOptions.value?.isLoading) return
|
||||
await getMsgList(size)
|
||||
}
|
||||
|
||||
const clearNewMsgCount = () => {
|
||||
currentNewMsgCount.value && (currentNewMsgCount.value.count = 0)
|
||||
}
|
||||
|
||||
// 查找消息在列表里面的索引
|
||||
const getMsgIndex = (msgId: number) => {
|
||||
if (!msgId || isNaN(Number(msgId))) return -1
|
||||
const keys = currentMessageMap.value ? Array.from(currentMessageMap.value.keys()) : []
|
||||
return keys.findIndex((key) => key === msgId)
|
||||
}
|
||||
|
||||
// 更新点赞、举报数
|
||||
const updateMarkCount = (markList: MarkItemType[]) => {
|
||||
markList.forEach((mark: MarkItemType) => {
|
||||
const { msgId, markType, markCount } = mark
|
||||
|
||||
const msgItem = currentMessageMap.value?.get(msgId)
|
||||
if (msgItem) {
|
||||
if (markType === MarkEnum.LIKE) {
|
||||
msgItem.message.messageMark.likeCount = markCount
|
||||
} else if (markType === MarkEnum.DISLIKE) {
|
||||
msgItem.message.messageMark.dislikeCount = markCount
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// 更新消息撤回状态
|
||||
const updateRecallStatus = (data: RevokedMsgType) => {
|
||||
const { msgId } = data
|
||||
const message = currentMessageMap.value?.get(msgId)
|
||||
if (message && typeof data.recallUid === 'number') {
|
||||
message.message.type = MsgEnum.RECALL
|
||||
const cacheUser = cachedStore.userCachedList[data.recallUid]
|
||||
// 如果撤回者的 id 不等于消息发送人的 id, 或者你本人就是管理员,那么显示管理员撤回的。
|
||||
if (data.recallUid !== message.fromUser.uid) {
|
||||
message.message.body = `管理员"${cacheUser.name}"撤回了一条成员消息` // 后期根据本地用户数据修改
|
||||
} else {
|
||||
// 如果被撤回的消息是消息发送者撤回,正常显示
|
||||
message.message.body = `"${cacheUser.name}"撤回了一条消息` // 后期根据本地用户数据修改
|
||||
}
|
||||
}
|
||||
// 更新与这条撤回消息有关的消息
|
||||
const messageList = currentReplyMap.value?.get(msgId)
|
||||
messageList?.forEach((id) => {
|
||||
const msg = currentMessageMap.value?.get(id)
|
||||
if (msg) {
|
||||
msg.message.body.reply.body = '原消息已被撤回'
|
||||
}
|
||||
})
|
||||
}
|
||||
// 删除消息
|
||||
const deleteMsg = (msgId: number) => {
|
||||
currentMessageMap.value?.delete(msgId)
|
||||
}
|
||||
// 更新消息
|
||||
const updateMsg = (msgId: number, newMessage: MessageType) => {
|
||||
deleteMsg(msgId)
|
||||
pushMsg(newMessage)
|
||||
}
|
||||
|
||||
// 标记已读数为 0
|
||||
const markSessionRead = (roomId: number) => {
|
||||
const session = sessionList.find((item) => item.roomId === roomId)
|
||||
const unreadCount = session?.unreadCount || 0
|
||||
if (session) {
|
||||
session.unreadCount = 0
|
||||
}
|
||||
return unreadCount
|
||||
}
|
||||
|
||||
// 根据消息id获取消息体
|
||||
const getMessage = (messageId: number) => {
|
||||
return currentMessageMap.value?.get(messageId)
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
const removeContact = (roomId: number) => {
|
||||
const index = sessionList.findIndex((session) => session.roomId === roomId)
|
||||
sessionList.splice(index, 1)
|
||||
}
|
||||
|
||||
return {
|
||||
getMsgIndex,
|
||||
chatMessageList,
|
||||
pushMsg,
|
||||
deleteMsg,
|
||||
clearNewMsgCount,
|
||||
updateMarkCount,
|
||||
updateRecallStatus,
|
||||
updateMsg,
|
||||
chatListToBottomAction,
|
||||
newMsgCount,
|
||||
messageMap,
|
||||
currentMessageMap,
|
||||
currentMessageOptions,
|
||||
currentReplyMap,
|
||||
currentNewMsgCount,
|
||||
loadMore,
|
||||
currentMsgReply,
|
||||
filterUser,
|
||||
sessionList,
|
||||
sessionOptions,
|
||||
getSessionList,
|
||||
updateSession,
|
||||
updateSessionLastActiveTime,
|
||||
markSessionRead,
|
||||
getSession,
|
||||
isGroup,
|
||||
currentSessionInfo,
|
||||
getMessage,
|
||||
removeContact
|
||||
}
|
||||
})
|
111
src/stores/contacts.ts
Normal file
111
src/stores/contacts.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import apis from '@/services/apis'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import type { ContactItem, RequestFriendItem } from '@/services/types'
|
||||
import { RequestFriendAgreeStatus } from '@/services/types'
|
||||
|
||||
export const pageSize = 20
|
||||
export const useContactStore = defineStore('contact', () => {
|
||||
const globalStore = useGlobalStore()
|
||||
const contactsList = reactive<ContactItem[]>([])
|
||||
const requestFriendsList = reactive<RequestFriendItem[]>([])
|
||||
const contactsOptions = reactive({ isLast: false, isLoading: false, cursor: '' })
|
||||
const requestFriendsOptions = reactive({ isLast: false, isLoading: false, cursor: '' })
|
||||
const getContactList = async (isFresh = false) => {
|
||||
if (!isFresh) {
|
||||
if (contactsOptions.isLast || contactsOptions.isLoading) return
|
||||
}
|
||||
contactsOptions.isLoading = true
|
||||
const res = await apis
|
||||
.getContactList({
|
||||
// TODO 先写 100,稍后优化
|
||||
pageSize: 100,
|
||||
cursor: isFresh || !contactsOptions.cursor ? undefined : contactsOptions.cursor
|
||||
})
|
||||
.catch(() => {
|
||||
contactsOptions.isLoading = false
|
||||
})
|
||||
if (!res) return
|
||||
const { data } = res
|
||||
isFresh ? contactsList.splice(0, contactsList.length, ...data.list) : contactsList.push(...data.list)
|
||||
contactsOptions.cursor = data.cursor
|
||||
contactsOptions.isLast = data.isLast
|
||||
contactsOptions.isLoading = false
|
||||
}
|
||||
|
||||
/** 好友申请未读数 */
|
||||
const getNewFriendCount = async () => {
|
||||
const res = await apis.newFriendCount().catch(() => {
|
||||
//
|
||||
})
|
||||
if (!res) return
|
||||
const { data } = res
|
||||
if (typeof data?.unReadCount === 'number') {
|
||||
globalStore.unReadMark.newFriendUnreadCount = data.unReadCount
|
||||
}
|
||||
}
|
||||
|
||||
const getRequestFriendsList = async (isFresh = false) => {
|
||||
if (!isFresh) {
|
||||
if (requestFriendsOptions.isLast || requestFriendsOptions.isLoading) return
|
||||
}
|
||||
requestFriendsOptions.isLoading = true
|
||||
const res = await apis
|
||||
.requestFriendList({
|
||||
pageSize,
|
||||
cursor: isFresh || !requestFriendsOptions.cursor ? undefined : requestFriendsOptions.cursor
|
||||
})
|
||||
.catch(() => {
|
||||
requestFriendsOptions.isLoading = false
|
||||
})
|
||||
// 每次加载完新的好友邀请列表都要更新申请未读数
|
||||
await getNewFriendCount()
|
||||
if (!res) return
|
||||
const { data } = res
|
||||
isFresh
|
||||
? requestFriendsList.splice(0, requestFriendsList.length, ...data.list)
|
||||
: requestFriendsList.push(...data.list)
|
||||
requestFriendsOptions.cursor = data.cursor
|
||||
requestFriendsOptions.isLast = data.isLast
|
||||
requestFriendsOptions.isLoading = false
|
||||
}
|
||||
// 默认执行一次
|
||||
// getContactList()
|
||||
// getRequestFriendsList()
|
||||
/** 接受好友请求 */
|
||||
const onAcceptFriend = (applyId: number) => {
|
||||
// 同意好友申请
|
||||
apis.applyFriendRequest({ applyId }).then(() => {
|
||||
// 刷新好友申请列表
|
||||
getRequestFriendsList(true)
|
||||
// 刷新好友列表
|
||||
getContactList(true)
|
||||
// 标识为可以发消息的人
|
||||
if (globalStore.currentSelectedContact) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
globalStore.currentSelectedContact.status = RequestFriendAgreeStatus.Agree
|
||||
}
|
||||
})
|
||||
}
|
||||
/** 删除好友 */
|
||||
const onDeleteContact = async (uid: number) => {
|
||||
if (!uid) return
|
||||
// 同意好友申请
|
||||
await apis.deleteFriend({ targetUid: uid })
|
||||
// 刷新好友申请列表
|
||||
// getRequestFriendsList(true)
|
||||
// 刷新好友列表
|
||||
await getContactList(true)
|
||||
}
|
||||
return {
|
||||
getContactList,
|
||||
getRequestFriendsList,
|
||||
contactsList,
|
||||
requestFriendsList,
|
||||
contactsOptions,
|
||||
requestFriendsOptions,
|
||||
onAcceptFriend,
|
||||
onDeleteContact
|
||||
}
|
||||
})
|
81
src/stores/downloadQuenu.ts
Normal file
81
src/stores/downloadQuenu.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useDownload } from '@/hooks/useDownload.ts'
|
||||
|
||||
type DownloadObjType = {
|
||||
url: string
|
||||
isDownloading: boolean
|
||||
process: number | undefined
|
||||
}
|
||||
|
||||
// 定义一个下载队列的 store
|
||||
export const useDownloadQuenuStore = defineStore('downloadQuenu', () => {
|
||||
// 最多可同时执行下载的任务数量
|
||||
const maxDownloadCount = 1
|
||||
// 下载队列
|
||||
const quenu = reactive<string[]>([])
|
||||
// 下载对象
|
||||
const downloadObjMap = reactive<Map<string, DownloadObjType>>(new Map())
|
||||
|
||||
// 添加到下载队列
|
||||
const addQuenuAction = (url: string) => {
|
||||
quenu.push(url)
|
||||
}
|
||||
|
||||
// 从下载队列中移除
|
||||
const removeQuenuAction = (url: string) => {
|
||||
const index = quenu.indexOf(url)
|
||||
if (index > -1) {
|
||||
quenu.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 出队列
|
||||
const dequeue = () => {
|
||||
if (!quenu.length || downloadObjMap.size >= maxDownloadCount) {
|
||||
return
|
||||
}
|
||||
const url = quenu.shift()
|
||||
if (url) {
|
||||
downloadAction(url)
|
||||
}
|
||||
}
|
||||
|
||||
// 下载
|
||||
const downloadAction = (url: string) => {
|
||||
const { downloadFile, isDownloading, process, onLoaded } = useDownload()
|
||||
const stopWatcher = watch(process, () => {
|
||||
// 更新下载进度
|
||||
downloadObjMap.set(url, { url, isDownloading: isDownloading.value, process: process.value })
|
||||
})
|
||||
onLoaded(() => {
|
||||
stopWatcher() // 清除watcher
|
||||
downloadObjMap.delete(url) // 下载完成后 删除下载对象
|
||||
dequeue()
|
||||
})
|
||||
if (url) {
|
||||
downloadFile(url)
|
||||
}
|
||||
}
|
||||
|
||||
const download = (url: string) => {
|
||||
addQuenuAction(url)
|
||||
dequeue()
|
||||
}
|
||||
|
||||
// 取消下载
|
||||
const cancelDownload = (url: string) => {
|
||||
if (quenu.includes(url)) {
|
||||
removeQuenuAction(url)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
quenu,
|
||||
addQuenuAction,
|
||||
removeQuenuAction,
|
||||
dequeue,
|
||||
downloadObjMap,
|
||||
download,
|
||||
cancelDownload
|
||||
}
|
||||
})
|
55
src/stores/emoji.ts
Normal file
55
src/stores/emoji.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import apis from '@/services/apis'
|
||||
import type { EmojiItem } from '@/services/types'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
export const useEmojiStore = defineStore('emoji', () => {
|
||||
const isLoading = ref(false) // 是否正在加载
|
||||
const userStore = useUserStore()
|
||||
const emojiList = ref<EmojiItem[]>([])
|
||||
|
||||
/**
|
||||
* 获取我的全部表情
|
||||
*/
|
||||
const getEmojiList = async () => {
|
||||
isLoading.value = true
|
||||
const res = await apis.getEmoji({ uid: userStore.userInfo.uid! }).catch(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
if (!res) return
|
||||
const {
|
||||
data: { data }
|
||||
} = res
|
||||
emojiList.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加表情
|
||||
*/
|
||||
const addEmoji = async (emojiUrl: string) => {
|
||||
const { uid } = userStore.userInfo
|
||||
if (!uid || !emojiUrl) return
|
||||
apis.addEmoji({ uid, expressionUrl: emojiUrl }).then((res) => {
|
||||
if (res.success) {
|
||||
window.$message.success('添加成功')
|
||||
}
|
||||
})
|
||||
await getEmojiList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除表情
|
||||
*/
|
||||
const deleteEmoji = async (id: number) => {
|
||||
if (!id) return
|
||||
await apis.deleteEmoji({ id })
|
||||
await getEmojiList()
|
||||
}
|
||||
|
||||
return {
|
||||
emojiList,
|
||||
addEmoji,
|
||||
getEmojiList,
|
||||
deleteEmoji
|
||||
}
|
||||
})
|
65
src/stores/global.ts
Normal file
65
src/stores/global.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { RoomTypeEnum } from '@/enums'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import type { ContactItem, RequestFriendItem } from '@/services/types'
|
||||
import { clearQueue, readCountQueue } from '@/utils/ReadCountQueue.ts'
|
||||
import apis from '@/services/apis'
|
||||
|
||||
export const useGlobalStore = defineStore('global', () => {
|
||||
const chatStore = useChatStore()
|
||||
const unReadMark = reactive<{ newFriendUnreadCount: number; newMsgUnreadCount: number }>({
|
||||
newFriendUnreadCount: 0,
|
||||
newMsgUnreadCount: 0
|
||||
})
|
||||
const currentReadUnreadList = reactive<{ show: boolean; msgId: number | null }>({
|
||||
show: false,
|
||||
msgId: null
|
||||
})
|
||||
const currentSession = reactive<{ roomId: number; type: RoomTypeEnum }>({
|
||||
roomId: 1,
|
||||
type: RoomTypeEnum.GROUP
|
||||
})
|
||||
/** 点击联系人选中的联系人项 */
|
||||
const currentSelectedContact = ref<ContactItem | RequestFriendItem>()
|
||||
const addFriendModalInfo = reactive<{ show: boolean; uid?: number }>({
|
||||
show: false,
|
||||
uid: undefined
|
||||
})
|
||||
// 创建群聊
|
||||
const createGroupModalInfo = reactive<{
|
||||
show: boolean
|
||||
isInvite: boolean
|
||||
selectedUid: number[]
|
||||
}>({
|
||||
show: false,
|
||||
isInvite: false,
|
||||
selectedUid: []
|
||||
})
|
||||
|
||||
// 切换会话的时候重置消息已读数查询
|
||||
watch(currentSession, (val) => {
|
||||
// 清理已读数查询
|
||||
clearQueue()
|
||||
setTimeout(readCountQueue, 1000)
|
||||
// 标记房间最新消息已读
|
||||
apis.markMsgRead({ roomId: val.roomId })
|
||||
const unreadCount = chatStore.markSessionRead(val.roomId)
|
||||
// console.log(unReadMark.newMsgUnreadCount, unreadCount)
|
||||
// setTimeout(() => {
|
||||
// if (unReadMark.newMsgUnreadCount !== unReadMark.newMsgUnreadCount - unreadCount) {
|
||||
// // unReadMark.newMsgUnreadCount = unReadMark.newMsgUnreadCount - unreadCount
|
||||
// }
|
||||
// })
|
||||
const resultCount = unReadMark.newMsgUnreadCount - unreadCount
|
||||
unReadMark.newMsgUnreadCount = resultCount > 0 ? resultCount : 0
|
||||
})
|
||||
|
||||
return {
|
||||
unReadMark,
|
||||
currentSession,
|
||||
addFriendModalInfo,
|
||||
currentSelectedContact,
|
||||
currentReadUnreadList,
|
||||
createGroupModalInfo
|
||||
}
|
||||
})
|
208
src/stores/group.ts
Normal file
208
src/stores/group.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import apis from '@/services/apis'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import type { GroupDetailReq, UserItem } from '@/services/types'
|
||||
import { pageSize, useChatStore } from './chat'
|
||||
import { OnlineEnum, RoleEnum } from '@/enums'
|
||||
import { uniqueUserList } from '@/utils/unique'
|
||||
import { useCachedStore } from '@/stores/cached'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const sorAction = (pre: UserItem, next: UserItem) => {
|
||||
if (pre.activeStatus === OnlineEnum.ONLINE && next.activeStatus === OnlineEnum.ONLINE) {
|
||||
return next.lastOptTime < pre.lastOptTime ? -1 : 1
|
||||
} else if (pre.activeStatus !== OnlineEnum.ONLINE && next.activeStatus !== OnlineEnum.ONLINE) {
|
||||
return next.lastOptTime < pre.lastOptTime ? -1 : 1
|
||||
} else if (pre.activeStatus === OnlineEnum.ONLINE && next.activeStatus !== OnlineEnum.ONLINE) {
|
||||
return -1
|
||||
} else if (pre.activeStatus !== OnlineEnum.ONLINE && next.activeStatus === OnlineEnum.ONLINE) {
|
||||
return 1
|
||||
} else {
|
||||
return next.lastOptTime < pre.lastOptTime ? -1 : 1
|
||||
}
|
||||
}
|
||||
|
||||
export const useGroupStore = defineStore('group', () => {
|
||||
const cachedStore = useCachedStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const userStore = useUserStore()
|
||||
const chatStore = useChatStore()
|
||||
// 消息列表
|
||||
const userList = ref<UserItem[]>([])
|
||||
const userListOptions = reactive({ isLast: false, loading: true, cursor: '' })
|
||||
const currentRoomId = computed(() => globalStore.currentSession?.roomId)
|
||||
/**
|
||||
* 获取当前群主ID
|
||||
*/
|
||||
const currentLordId = computed(() => {
|
||||
const list = userList.value.filter((member) => member.roleId === RoleEnum.LORD)
|
||||
if (list.length) {
|
||||
return list[0]?.uid
|
||||
}
|
||||
return -99
|
||||
})
|
||||
/**
|
||||
* 获取当前管理员ID列表
|
||||
*/
|
||||
const adminUidList = computed(() => {
|
||||
return userList.value.filter((member) => member.roleId === RoleEnum.ADMIN).map((member) => member.uid)
|
||||
})
|
||||
/**
|
||||
* 获取管理员基本信息列表
|
||||
*/
|
||||
const adminList = computed(() => {
|
||||
return cachedStore.filterUsersByUidList(adminUidList.value)
|
||||
})
|
||||
/**
|
||||
* 获取管理员基本信息列表
|
||||
*/
|
||||
const memberList = computed(() => {
|
||||
const memberInfoList = cachedStore.filterUsersByUidList(userList.value.map((item) => item.uid))
|
||||
return memberInfoList.map((member) => {
|
||||
if (adminUidList.value.includes(member.uid)) {
|
||||
return {
|
||||
...member,
|
||||
roleId: RoleEnum.ADMIN
|
||||
}
|
||||
} else if (member.uid === currentLordId.value) {
|
||||
return {
|
||||
...member,
|
||||
roleId: RoleEnum.LORD
|
||||
}
|
||||
}
|
||||
return member
|
||||
})
|
||||
})
|
||||
const countInfo = ref<GroupDetailReq>({
|
||||
avatar: '',
|
||||
groupName: '',
|
||||
onlineNum: 0,
|
||||
role: 0,
|
||||
roomId: currentRoomId.value
|
||||
})
|
||||
|
||||
// 移动端控制显隐
|
||||
const showGroupList = ref(false)
|
||||
|
||||
// 获取群成员
|
||||
const getGroupUserList = async (refresh = false) => {
|
||||
const res = await apis.getGroupList({
|
||||
params: {
|
||||
pageSize,
|
||||
cursor: refresh ? undefined : userListOptions.cursor,
|
||||
roomId: currentRoomId.value
|
||||
}
|
||||
})
|
||||
if (!res) return
|
||||
const { data } = res
|
||||
const tempNew = cloneDeep(uniqueUserList(refresh ? data.list : [...data.list, ...userList.value]))
|
||||
tempNew.sort(sorAction)
|
||||
userList.value = tempNew
|
||||
userListOptions.cursor = data.cursor
|
||||
userListOptions.isLast = data.isLast
|
||||
userListOptions.loading = false
|
||||
|
||||
/** 收集需要请求用户详情的 uid */
|
||||
const uidCollectYet: Set<number> = new Set() // 去重用
|
||||
data.list?.forEach((user: UserItem) => uidCollectYet.add(user.uid))
|
||||
// 获取用户信息缓存
|
||||
await cachedStore.getBatchUserInfo([...uidCollectYet])
|
||||
}
|
||||
|
||||
// 获取群成员数量统计
|
||||
const getCountStatistic = async () => {
|
||||
const { data } = await apis.groupDetail({ id: currentRoomId.value })
|
||||
countInfo.value = data
|
||||
}
|
||||
|
||||
// 加载更多群成员
|
||||
const loadMore = async () => {
|
||||
if (userListOptions.isLast) return
|
||||
await getGroupUserList()
|
||||
}
|
||||
|
||||
// 更新用户在线状态
|
||||
const batchUpdateUserStatus = (items: UserItem[]) => {
|
||||
const tempNew = cloneDeep(userList.value)
|
||||
for (let index = 0, len = items.length; index < len; index++) {
|
||||
const curUser = items[index]
|
||||
const findIndex = tempNew.findIndex((item) => item.uid === curUser.uid)
|
||||
findIndex > -1 && (tempNew[findIndex].activeStatus = curUser.activeStatus)
|
||||
}
|
||||
tempNew.sort(sorAction)
|
||||
userList.value = tempNew
|
||||
}
|
||||
|
||||
// 过滤掉小黑子
|
||||
const filterUser = (uid: number) => {
|
||||
if (typeof uid !== 'number') return
|
||||
userList.value = userList.value.filter((item) => item.uid !== uid)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加管理员
|
||||
* @param uidList
|
||||
*/
|
||||
const addAdmin = async (uidList: number[]) => {
|
||||
await apis.addAdmin({ roomId: currentRoomId.value, uidList })
|
||||
|
||||
// 更新群成员列表
|
||||
userList.value.forEach((user) => {
|
||||
if (uidList.includes(user.uid)) {
|
||||
user.roleId = RoleEnum.ADMIN
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销管理员
|
||||
* @param uidList
|
||||
*/
|
||||
const revokeAdmin = async (uidList: number[]) => {
|
||||
await apis.revokeAdmin({ roomId: currentRoomId.value, uidList })
|
||||
|
||||
// 更新群成员列表
|
||||
userList.value.forEach((user) => {
|
||||
if (uidList.includes(user.uid)) {
|
||||
user.roleId = RoleEnum.NORMAL
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出群聊
|
||||
*/
|
||||
const exitGroup = async () => {
|
||||
await apis.exitGroup({ roomId: currentRoomId.value })
|
||||
|
||||
// 更新群成员列表
|
||||
const index = userList.value.findIndex((user) => user.uid === userStore.userInfo.uid)
|
||||
userList.value.splice(index, 1)
|
||||
|
||||
// 更新会话列表
|
||||
chatStore.removeContact(currentRoomId.value)
|
||||
|
||||
// 切换为第一个会话
|
||||
globalStore.currentSession.roomId = chatStore.sessionList[0].roomId
|
||||
}
|
||||
|
||||
return {
|
||||
userList,
|
||||
userListOptions,
|
||||
loadMore,
|
||||
getGroupUserList,
|
||||
getCountStatistic,
|
||||
currentLordId,
|
||||
countInfo,
|
||||
batchUpdateUserStatus,
|
||||
showGroupList,
|
||||
filterUser,
|
||||
adminUidList,
|
||||
adminList,
|
||||
memberList,
|
||||
addAdmin,
|
||||
revokeAdmin,
|
||||
exitGroup
|
||||
}
|
||||
})
|
105
src/stores/preview.ts
Normal file
105
src/stores/preview.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useImgPreviewStore = defineStore('image', () => {
|
||||
const previewUrl = ref('') // 图片预览链接
|
||||
const isShowing = ref(false) // 图片预览是否显示
|
||||
|
||||
/**
|
||||
* 开启图片预览
|
||||
* @param url 图片链接
|
||||
*/
|
||||
const show = (url: string) => {
|
||||
previewUrl.value = url
|
||||
isShowing.value = true
|
||||
}
|
||||
/**
|
||||
* 关闭图片预览
|
||||
*/
|
||||
const close = () => {
|
||||
isShowing.value = false
|
||||
previewUrl.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
previewUrl,
|
||||
isShowing,
|
||||
show,
|
||||
close
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 音频预览Store
|
||||
*/
|
||||
export const useVoicePreviewStore = defineStore('voice', () => {
|
||||
const previewUrl = ref('') // 音频预览链接
|
||||
const isPlaying = ref(false) // 音频是否正在播放
|
||||
const progress = ref(0) // 音频播放进度
|
||||
const audio = ref<HTMLAudioElement | null>(null) // 音频播放器
|
||||
|
||||
/**
|
||||
* 关闭音频预览
|
||||
*/
|
||||
const close = () => {
|
||||
isPlaying.value = false
|
||||
progress.value = 0
|
||||
audio.value?.pause()
|
||||
// 显式移除事件监听
|
||||
audio.value?.removeEventListener('ended', () => {})
|
||||
audio.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启音频预览
|
||||
*/
|
||||
const open = async (url: string) => {
|
||||
if (isPlaying.value && previewUrl.value === url) {
|
||||
close()
|
||||
return
|
||||
}
|
||||
close()
|
||||
previewUrl.value = url
|
||||
audio.value = new Audio(url)
|
||||
await audio.value.play()
|
||||
isPlaying.value = true
|
||||
audio.value.addEventListener('ended', () => close())
|
||||
}
|
||||
|
||||
return {
|
||||
previewUrl,
|
||||
isPlaying,
|
||||
close,
|
||||
open
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 视频预览Store
|
||||
*/
|
||||
export const useVideoPreviewStore = defineStore('video', () => {
|
||||
const previewUrl = ref('') // 预览链接
|
||||
const isPlaying = ref(false) // 是否正在播放
|
||||
|
||||
/**
|
||||
* 开启视频预览
|
||||
*/
|
||||
const open = (url: string) => {
|
||||
previewUrl.value = url
|
||||
isPlaying.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭视频预览
|
||||
*/
|
||||
const close = () => {
|
||||
previewUrl.value = ''
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
previewUrl,
|
||||
isPlaying,
|
||||
open,
|
||||
close
|
||||
}
|
||||
})
|
@ -1,6 +1,10 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { CloseBxEnum, StoresEnum, ThemeEnum } from '@/enums'
|
||||
import apis from '@/services/apis.ts'
|
||||
import { isDiffNow10Min } from '@/utils/ComputedTime.ts'
|
||||
import type { CacheBadgeItem } from '@/services/types.ts'
|
||||
|
||||
const badgeCachedList = reactive<Record<number, Partial<CacheBadgeItem>>>({})
|
||||
// TODO 使用indexDB或者把配置写出到文件中,还需要根据每个账号来进行配置 (nyh -> 2024-03-26 01:22:12)
|
||||
export const setting = defineStore(StoresEnum.SETTING, {
|
||||
state: (): STO.Setting => ({
|
||||
@ -26,8 +30,10 @@ export const setting = defineStore(StoresEnum.SETTING, {
|
||||
password: '',
|
||||
name: '',
|
||||
avatar: '',
|
||||
uid: ''
|
||||
}
|
||||
uid: 0,
|
||||
token: ''
|
||||
},
|
||||
badgeList: []
|
||||
},
|
||||
/** 聊天设置 */
|
||||
chat: {
|
||||
@ -66,9 +72,30 @@ export const setting = defineStore(StoresEnum.SETTING, {
|
||||
setAccountInfo(accountInfo: STO.Setting['login']['accountInfo']) {
|
||||
this.login.accountInfo = accountInfo
|
||||
},
|
||||
/** 批量获取用户徽章详细信息 */
|
||||
async getBatchBadgeInfo(itemIds: number[]) {
|
||||
// 没有 lastModifyTime 的要更新,lastModifyTime 距离现在 10 分钟已上的也要更新
|
||||
const result = itemIds
|
||||
.map((itemId) => {
|
||||
const cacheBadge = badgeCachedList[itemId]
|
||||
return { itemId, lastModifyTime: cacheBadge?.lastModifyTime }
|
||||
})
|
||||
.filter((item) => !item.lastModifyTime || isDiffNow10Min(item.lastModifyTime))
|
||||
if (!result.length) return
|
||||
const { data } = await apis.getBadgesBatch(result)
|
||||
data?.forEach(
|
||||
(item: CacheBadgeItem) =>
|
||||
// 更新最后更新时间。
|
||||
(badgeCachedList[item.itemId] = {
|
||||
...(item?.needRefresh ? item : badgeCachedList[item.itemId]),
|
||||
needRefresh: void 0,
|
||||
lastModifyTime: Date.now()
|
||||
})
|
||||
)
|
||||
},
|
||||
/** 清空账号信息 */
|
||||
clearAccount() {
|
||||
this.login.accountInfo = { account: '', avatar: '', name: '', password: '', uid: '' }
|
||||
this.login.accountInfo.password = ''
|
||||
}
|
||||
},
|
||||
share: {
|
||||
|
@ -1,34 +1,36 @@
|
||||
import { ref } from 'vue'
|
||||
import apis from '@/services/apis'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { UserInfoType } from '@/services/types'
|
||||
|
||||
type IState = {
|
||||
loginInfo: {
|
||||
avatarUrl: string
|
||||
email: string
|
||||
nickname: string
|
||||
ipaddress: string
|
||||
username: string
|
||||
token: string
|
||||
}
|
||||
}
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const userInfo = ref<Partial<UserInfoType>>({})
|
||||
const isSign = ref(false)
|
||||
|
||||
export const userStore = defineStore('localUserInfo', {
|
||||
state: (): IState =>
|
||||
<IState>{
|
||||
loginInfo: {}
|
||||
},
|
||||
getters: {
|
||||
getBearerToken(): any {
|
||||
return this.loginInfo.token ? this.loginInfo.token : ''
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setLoginInfo(loginInfo: any) {
|
||||
this.loginInfo = loginInfo
|
||||
},
|
||||
logout() {
|
||||
this.$reset()
|
||||
//删除localStorage中的用户信息
|
||||
localStorage.removeItem('localUserInfo')
|
||||
}
|
||||
let localUserInfo = {}
|
||||
try {
|
||||
localUserInfo = JSON.parse(localStorage.getItem('USER_INFO') || '{}')
|
||||
} catch (error) {
|
||||
localUserInfo = {}
|
||||
}
|
||||
|
||||
// 从 local读取
|
||||
if (!Object.keys(userInfo.value).length && Object.keys(localUserInfo).length) {
|
||||
userInfo.value = localUserInfo
|
||||
}
|
||||
|
||||
function getUserDetailAction() {
|
||||
apis
|
||||
.getUserDetail()
|
||||
.then((res) => {
|
||||
userInfo.value = { ...userInfo.value, ...res.data.data }
|
||||
})
|
||||
.catch(() => {
|
||||
// 删除缓存
|
||||
localStorage.removeItem('TOKEN')
|
||||
localStorage.removeItem('USER_INFO')
|
||||
})
|
||||
}
|
||||
|
||||
return { userInfo, isSign, getUserDetailAction }
|
||||
})
|
||||
|
30
src/stores/ws.ts
Normal file
30
src/stores/ws.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import wsIns from '@/services/webSocket.ts'
|
||||
import { WsRequestMsgType } from '@/utils/wsType'
|
||||
|
||||
export enum LoginStatus {
|
||||
Init,
|
||||
Waiting,
|
||||
Success
|
||||
}
|
||||
|
||||
export const useWsLoginStore = defineStore('wsLogin', () => {
|
||||
const loginQrCode = ref<string>()
|
||||
const loginStatus = ref(LoginStatus.Init)
|
||||
function getLoginQrCode() {
|
||||
wsIns.send({ type: WsRequestMsgType.RequestLoginQrCode })
|
||||
}
|
||||
function resetLoginState() {
|
||||
loginQrCode.value = undefined
|
||||
loginStatus.value = LoginStatus.Init
|
||||
}
|
||||
|
||||
return {
|
||||
loginQrCode,
|
||||
loginStatus,
|
||||
resetLoginState,
|
||||
getLoginQrCode
|
||||
}
|
||||
})
|
@ -28,7 +28,7 @@
|
||||
}
|
||||
.photo-wall {
|
||||
@extend .bubble-oneself;
|
||||
@apply flex flex-col items-start gap-6px max-w-380px;
|
||||
@apply flex flex-col items-start gap-6px flex-1;
|
||||
}
|
||||
.float-header {
|
||||
@include float;
|
||||
@ -66,11 +66,7 @@
|
||||
}
|
||||
}
|
||||
.content-span {
|
||||
width: fit-content;
|
||||
max-width: 250px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@apply w-fit truncate flex-1;
|
||||
}
|
||||
/** 回复图片的计数器样式 */
|
||||
.reply-img-sub {
|
||||
@ -79,7 +75,7 @@
|
||||
}
|
||||
/** emoji回复气泡的样式 */
|
||||
.emoji-reply-bubble {
|
||||
@apply relative rounded-8px p-[4px_8px] cursor-pointer select-none bg-#13987F66 text-14px w-fit;
|
||||
@apply relative rounded-50px p-[4px_8px] cursor-pointer select-none bg-#13987F66 text-14px w-fit border-(1px solid #13987F);
|
||||
}
|
||||
/** 跳转到回复内容时候显示的样式 */
|
||||
.active-reply {
|
||||
@ -95,6 +91,15 @@
|
||||
border-radius: 0;
|
||||
transition: all 0.4s ease-in-out;
|
||||
}
|
||||
/** 时间搓显示时候的动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
/**! 替换ait的字体颜色 */
|
||||
:deep(#aitSpan) {
|
||||
@apply text-inherit cursor-pointer;
|
||||
|
@ -2,7 +2,7 @@
|
||||
@apply flex flex-col w-180px h-100vh p-[12px_0_12px_6px] box-border select-none text-[--text-color];
|
||||
.item {
|
||||
height: 42px;
|
||||
padding-left: 4px;
|
||||
padding: 0 4px;
|
||||
width: 95%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
|
@ -42,7 +42,7 @@
|
||||
--bg-avatar: #e3e3e3;
|
||||
// 个人信息框背景颜色
|
||||
--info-hover: #eee;
|
||||
--info-text-color: #505050;
|
||||
--info-text-color: #202020;
|
||||
// 系统托盘hover样式
|
||||
--tray-hover: #eee;
|
||||
--tray-hover-e: #f5dce1;
|
||||
@ -59,6 +59,12 @@
|
||||
--reply-color: #909090;
|
||||
--reply-hover: #505050;
|
||||
--bg-reply-img-count: #e3e3e3;
|
||||
// 主页面面板分割线样式
|
||||
--split-color: #f1f1f1;
|
||||
// 编辑资料背景颜色
|
||||
--bg-edit: #f0f0f0;
|
||||
// 聊天框时间戳样式
|
||||
--time-color: #e3e3e3;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
@ -121,6 +127,12 @@ html[data-theme='dark'] {
|
||||
--reply-color: #e3e3e3;
|
||||
--reply-hover: #b1b1b1;
|
||||
--bg-reply-img-count: #505050;
|
||||
// 主页面面板分割线样式
|
||||
--split-color: #3b3b3b;
|
||||
// 编辑资料背景颜色
|
||||
--bg-edit: #262626;
|
||||
// 聊天框时间戳样式
|
||||
--time-color: #3b3b3b;
|
||||
}
|
||||
/**! end */
|
||||
// 线性动画
|
||||
|
@ -1,22 +0,0 @@
|
||||
/**!使用比较新的View Transitions API来实现主图切换效果*/
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
/** 进入dark模式和退出dark模式时,两个图像的位置顺序正好相反 */
|
||||
.dark::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
.dark::view-transition-new(root) {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
z-index: 999;
|
||||
}
|
||||
::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
/**!end*/
|
1
src/typings/auto-imports.d.ts
vendored
1
src/typings/auto-imports.d.ts
vendored
@ -55,6 +55,7 @@ declare global {
|
||||
const useDialog: typeof import('naive-ui')['useDialog']
|
||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||
const useMessage: typeof import('naive-ui')['useMessage']
|
||||
const useModal: typeof import('naive-ui')['useModal']
|
||||
const useNotification: typeof import('naive-ui')['useNotification']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
|
106
src/typings/components.d.ts
vendored
106
src/typings/components.d.ts
vendored
@ -7,58 +7,58 @@ export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ActionBar: typeof import('./../components/windows/ActionBar.vue')['default']
|
||||
ChatBox: typeof import('./../components/rightBox/chatBox/index.vue')['default']
|
||||
ChatFooter: typeof import('./../components/rightBox/chatBox/ChatFooter.vue')['default']
|
||||
ChatHeader: typeof import('./../components/rightBox/chatBox/ChatHeader.vue')['default']
|
||||
ChatMain: typeof import('./../components/rightBox/chatBox/ChatMain.vue')['default']
|
||||
ChatSidebar: typeof import('./../components/rightBox/chatBox/ChatSidebar.vue')['default']
|
||||
ContextMenu: typeof import('./../components/common/ContextMenu.vue')['default']
|
||||
Details: typeof import('./../components/rightBox/Details.vue')['default']
|
||||
Emoji: typeof import('./../components/rightBox/emoji/index.vue')['default']
|
||||
Image: typeof import('./../components/rightBox/renderMessage/Image.vue')['default']
|
||||
InfoPopover: typeof import('./../components/common/InfoPopover.vue')['default']
|
||||
MsgInput: typeof import('./../components/rightBox/MsgInput.vue')['default']
|
||||
NaiveProvider: typeof import('./../components/common/NaiveProvider.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NAvatarGroup: typeof import('naive-ui')['NAvatarGroup']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NIconWrapper: typeof import('naive-ui')['NIconWrapper']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NImageGroup: typeof import('naive-ui')['NImageGroup']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NPopselect: typeof import('naive-ui')['NPopselect']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
RenderMessage: typeof import('./../components/rightBox/renderMessage/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Text: typeof import('./../components/rightBox/renderMessage/Text.vue')['default']
|
||||
ActionBar: (typeof import('./../components/windows/ActionBar.vue'))['default']
|
||||
ChatBox: (typeof import('./../components/rightBox/chatBox/index.vue'))['default']
|
||||
ChatFooter: (typeof import('./../components/rightBox/chatBox/ChatFooter.vue'))['default']
|
||||
ChatHeader: (typeof import('./../components/rightBox/chatBox/ChatHeader.vue'))['default']
|
||||
ChatMain: (typeof import('./../components/rightBox/chatBox/ChatMain.vue'))['default']
|
||||
ChatSidebar: (typeof import('./../components/rightBox/chatBox/ChatSidebar.vue'))['default']
|
||||
ContextMenu: (typeof import('./../components/common/ContextMenu.vue'))['default']
|
||||
Details: (typeof import('./../components/rightBox/Details.vue'))['default']
|
||||
Emoji: (typeof import('./../components/rightBox/emoji/index.vue'))['default']
|
||||
Image: (typeof import('./../components/rightBox/renderMessage/Image.vue'))['default']
|
||||
InfoPopover: (typeof import('./../components/common/InfoPopover.vue'))['default']
|
||||
MsgInput: (typeof import('./../components/rightBox/MsgInput.vue'))['default']
|
||||
NaiveProvider: (typeof import('./../components/common/NaiveProvider.vue'))['default']
|
||||
NAlert: (typeof import('naive-ui'))['NAlert']
|
||||
NAvatar: (typeof import('naive-ui'))['NAvatar']
|
||||
NAvatarGroup: (typeof import('naive-ui'))['NAvatarGroup']
|
||||
NBadge: (typeof import('naive-ui'))['NBadge']
|
||||
NButton: (typeof import('naive-ui'))['NButton']
|
||||
NButtonGroup: (typeof import('naive-ui'))['NButtonGroup']
|
||||
NCheckbox: (typeof import('naive-ui'))['NCheckbox']
|
||||
NCollapse: (typeof import('naive-ui'))['NCollapse']
|
||||
NCollapseItem: (typeof import('naive-ui'))['NCollapseItem']
|
||||
NConfigProvider: (typeof import('naive-ui'))['NConfigProvider']
|
||||
NDialogProvider: (typeof import('naive-ui'))['NDialogProvider']
|
||||
NDropdown: (typeof import('naive-ui'))['NDropdown']
|
||||
NEllipsis: (typeof import('naive-ui'))['NEllipsis']
|
||||
NFlex: (typeof import('naive-ui'))['NFlex']
|
||||
NIcon: (typeof import('naive-ui'))['NIcon']
|
||||
NIconWrapper: (typeof import('naive-ui'))['NIconWrapper']
|
||||
NImage: (typeof import('naive-ui'))['NImage']
|
||||
NImageGroup: (typeof import('naive-ui'))['NImageGroup']
|
||||
NInput: (typeof import('naive-ui'))['NInput']
|
||||
NLoadingBarProvider: (typeof import('naive-ui'))['NLoadingBarProvider']
|
||||
NMessageProvider: (typeof import('naive-ui'))['NMessageProvider']
|
||||
NModal: (typeof import('naive-ui'))['NModal']
|
||||
NModalProvider: (typeof import('naive-ui'))['NModalProvider']
|
||||
NNotificationProvider: (typeof import('naive-ui'))['NNotificationProvider']
|
||||
NPopover: (typeof import('naive-ui'))['NPopover']
|
||||
NPopselect: (typeof import('naive-ui'))['NPopselect']
|
||||
NQrCode: (typeof import('naive-ui'))['NQrCode']
|
||||
NRadio: (typeof import('naive-ui'))['NRadio']
|
||||
NScrollbar: (typeof import('naive-ui'))['NScrollbar']
|
||||
NSelect: (typeof import('naive-ui'))['NSelect']
|
||||
NSkeleton: (typeof import('naive-ui'))['NSkeleton']
|
||||
NSwitch: (typeof import('naive-ui'))['NSwitch']
|
||||
NTabPane: (typeof import('naive-ui'))['NTabPane']
|
||||
NTabs: (typeof import('naive-ui'))['NTabs']
|
||||
NTooltip: (typeof import('naive-ui'))['NTooltip']
|
||||
NVirtualList: (typeof import('naive-ui'))['NVirtualList']
|
||||
RenderMessage: (typeof import('./../components/rightBox/renderMessage/index.vue'))['default']
|
||||
RouterLink: (typeof import('vue-router'))['RouterLink']
|
||||
RouterView: (typeof import('vue-router'))['RouterView']
|
||||
Text: (typeof import('./../components/rightBox/renderMessage/Text.vue'))['default']
|
||||
}
|
||||
}
|
||||
|
1
src/typings/env.d.ts
vendored
1
src/typings/env.d.ts
vendored
@ -71,4 +71,5 @@ declare interface Window {
|
||||
$notification: ReturnType<typeof useNotification>
|
||||
$loadingBar: ReturnType<typeof useLoadingBar>
|
||||
$dialog: ReturnType<typeof useDialog>
|
||||
$modal: ReturnType<typeof useModal>
|
||||
}
|
||||
|
2
src/typings/options.d.ts
vendored
2
src/typings/options.d.ts
vendored
@ -39,7 +39,7 @@ declare namespace OPT {
|
||||
label: string
|
||||
icon: string
|
||||
click?: (...args: any[]) => void
|
||||
}
|
||||
} | null
|
||||
|
||||
/** 详情页选项 */
|
||||
type Details = {
|
||||
|
5
src/typings/stores.d.ts
vendored
5
src/typings/stores.d.ts
vendored
@ -25,8 +25,11 @@ declare namespace STO {
|
||||
password?: string
|
||||
name: string
|
||||
avatar: string
|
||||
uid: string
|
||||
uid: number
|
||||
token: string
|
||||
}
|
||||
/* 用户徽章列表 */
|
||||
badgeList: T
|
||||
}
|
||||
/** 聊天设置 */
|
||||
chat: {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import mitt from 'mitt'
|
||||
import type { Emitter } from 'mitt'
|
||||
|
||||
const Mitt = mitt()
|
||||
const Mitt: Emitter<any> = mitt()
|
||||
export default Mitt
|
||||
|
104
src/utils/ComputedTime.ts
Normal file
104
src/utils/ComputedTime.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import dayjs from 'dayjs'
|
||||
import weekday from 'dayjs/plugin/weekday'
|
||||
import 'dayjs/locale/zh-cn' // 导入中文语言包
|
||||
import type { Dayjs, OpUnitType, ConfigType } from 'dayjs'
|
||||
import type { MessageType } from '@/services/types'
|
||||
|
||||
// 全局使用语言包
|
||||
dayjs.locale('zh-cn')
|
||||
// 设置一周起始位周一
|
||||
dayjs.extend(weekday)
|
||||
// 5 分钟 5 * 60 * 1000;
|
||||
const intervalTime = 300000
|
||||
// 计数上限 20 条,到达 20 重置
|
||||
const computedCountMax = 20
|
||||
// 计数
|
||||
let computedCount = 0
|
||||
|
||||
// 时间格式化为相对文本,仿微信风格
|
||||
const timeToStr = (time: number) => {
|
||||
const sendTime = dayjs(time)
|
||||
// 计算今天和消息的发送时间间隔多少天
|
||||
const gapDay = dayjs().endOf('day').diff(sendTime, 'day')
|
||||
// 消息与今天是否 7 天及以上了
|
||||
const isLastWeek = gapDay >= 7
|
||||
// 今天显示时分, 昨天的显示 `昨天 时分`, 今天往前一周内,显示`周几 时分`, 再前面显示日期 `年月日 时分`
|
||||
return gapDay < 2
|
||||
? `${gapDay === 1 ? '昨天 ' : ''}${sendTime.format('HH:mm')}`
|
||||
: isLastWeek
|
||||
? sendTime.format('YYYY-MM-DD HH:mm')
|
||||
: dayjs(sendTime).format('dddd HH:mm')
|
||||
}
|
||||
|
||||
// 超过5分钟,或者超过20条消息,就添加展示时间
|
||||
const checkTimeInterval = (cur: MessageType, pre: MessageType) => {
|
||||
// 如果有一个超过 5 分钟了或者计数达到 20 条了
|
||||
if ((pre && cur.message.sendTime - pre.message.sendTime > intervalTime) || computedCount >= computedCountMax) {
|
||||
// 重置计数
|
||||
computedCount = 0
|
||||
// 返回时间标记
|
||||
return { ...cur, timeBlock: timeToStr(cur.message.sendTime) }
|
||||
} else {
|
||||
// 时间间隔很短的就累计计数
|
||||
computedCount += 1
|
||||
return cur
|
||||
}
|
||||
}
|
||||
|
||||
export const computedTimeBlock = (list: MessageType[], needFirst = true) => {
|
||||
if (!list || list.length === 0) return []
|
||||
// 是否需要保留 传入 list 第一个,如果是接口拉回来的消息列表就要保留,如果接收到新消息,需要拿当前消息列表最后一个拿来做时间间隔计算的话,就不需要保留第一个
|
||||
const temp = needFirst ? [list[0]] : []
|
||||
// 跳过第一个
|
||||
for (let index = 1, len = list.length; index < len; index++) {
|
||||
const item = list[index]
|
||||
// 上个聊天记录
|
||||
const preItem = list[index - 1]
|
||||
// 超过20分钟,或者超过50条评论,展示时间
|
||||
temp.push(checkTimeInterval(item, preItem))
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息时间戳格式化
|
||||
* @param timestamp 时间戳
|
||||
* @param isDetail 是否显示详细时间
|
||||
* @returns 格式化后的时间字符串
|
||||
*/
|
||||
export const formatTimestamp = (timestamp: number, isDetail = false): string => {
|
||||
const now: Dayjs = dayjs()
|
||||
const date: Dayjs = dayjs(timestamp)
|
||||
// 计算今天和消息的发送时间间隔多少天
|
||||
const gapDay = dayjs().endOf('day').diff(date, 'day')
|
||||
// 消息与今天是否 7 天及以上了
|
||||
const isLastWeek = gapDay >= 7
|
||||
if (now.isSame(date, 'day')) {
|
||||
return date.format(`${isDetail ? 'HH:mm:ss' : 'HH:mm'}`)
|
||||
} else if (now.diff(date, 'year') >= 1) {
|
||||
return date.format(`${isDetail ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'}`)
|
||||
} else {
|
||||
if (isDetail) return date.format('MM-DD HH:mm:ss')
|
||||
return gapDay === 1 ? '昨天' : isLastWeek ? date.format('MM-DD') : dayjs(date).format('dddd')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息间隔判断
|
||||
* @param {ConfigType} time 输入时间
|
||||
* @param {OpUnitType} unit 间隔单位
|
||||
* @param {number} diff 间隔值
|
||||
* @returns boolean 输入时间是否间隔 now 间隔值以上。
|
||||
*/
|
||||
export const isDiffNow = ({ time, unit, diff }: { unit: OpUnitType; time: ConfigType; diff: number }): boolean => {
|
||||
return dayjs().diff(dayjs(time), unit) > diff
|
||||
}
|
||||
|
||||
/**
|
||||
* 距离现在 10 分钟了
|
||||
* @param {ConfigType} time 输入时间
|
||||
* @returns boolean 输入时间是否间隔 now 间隔值以上。
|
||||
*/
|
||||
export const isDiffNow10Min = (time: ConfigType): boolean => {
|
||||
return isDiffNow({ time, unit: 'minute', diff: 10 })
|
||||
}
|
3
src/utils/InitWorker.ts
Normal file
3
src/utils/InitWorker.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const worker: Worker = new Worker(new URL('./Worker.ts', import.meta.url), {
|
||||
type: 'module'
|
||||
})
|
50
src/utils/ReadCountQueue.ts
Normal file
50
src/utils/ReadCountQueue.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import apis from '@/services/apis'
|
||||
import type { MsgReadUnReadCountType } from '@/services/types'
|
||||
|
||||
const queue = new Set<number>()
|
||||
let timer: number | null = null
|
||||
let request: any = null
|
||||
|
||||
const onAddReadCountTask = ({ msgId }: { msgId: number }) => {
|
||||
queue.add(msgId)
|
||||
}
|
||||
const onRemoveReadCountTask = ({ msgId }: { msgId: number }) => {
|
||||
queue.delete(msgId)
|
||||
}
|
||||
const task = () => {
|
||||
// 10s 了上个请求还未完成就中断掉
|
||||
request?.abort()
|
||||
if (queue.size > 0) {
|
||||
// 开始新请求
|
||||
request = apis.getMsgReadCount({ params: { msgIds: [...queue] } })
|
||||
request.send().then((res: MsgReadUnReadCountType[]) => {
|
||||
const result = new Map<number, MsgReadUnReadCountType>()
|
||||
res.forEach((item) => result.set(item.msgId, item))
|
||||
Mitt.emit('onGetReadCount', result)
|
||||
request = null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const initListener = () => {
|
||||
Mitt.on('onAddReadCountTask', onAddReadCountTask)
|
||||
Mitt.on('onRemoveReadCountTask', onRemoveReadCountTask)
|
||||
clearQueue()
|
||||
}
|
||||
|
||||
export const clearListener = () => {
|
||||
Mitt.off('onAddReadCountTask', onAddReadCountTask)
|
||||
Mitt.off('onRemoveReadCountTask', onRemoveReadCountTask)
|
||||
timer && clearInterval(timer)
|
||||
}
|
||||
|
||||
export const clearQueue = () => {
|
||||
queue.clear()
|
||||
timer && clearInterval(timer)
|
||||
}
|
||||
|
||||
export const readCountQueue = () => {
|
||||
task()
|
||||
timer = setInterval(task, 10000) as any
|
||||
}
|
40
src/utils/RenderReplyContent.ts
Normal file
40
src/utils/RenderReplyContent.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { MsgEnum, RoomTypeEnum } from '@/enums'
|
||||
import { MSG_REPLY_TEXT_MAP } from '@/common/message'
|
||||
|
||||
// 计算展示的回复消息的内容
|
||||
export const renderReplyContent = (name?: string, type?: MsgEnum, content?: string, roomType?: RoomTypeEnum) => {
|
||||
switch (type) {
|
||||
case MsgEnum.SYSTEM:
|
||||
case MsgEnum.TEXT: {
|
||||
return roomType === RoomTypeEnum.GROUP ? `${name}:${content}` : content
|
||||
}
|
||||
case MsgEnum.IMAGE: {
|
||||
return roomType === RoomTypeEnum.GROUP
|
||||
? `${name}:${MSG_REPLY_TEXT_MAP[MsgEnum.IMAGE]}`
|
||||
: MSG_REPLY_TEXT_MAP[MsgEnum.IMAGE]
|
||||
}
|
||||
case MsgEnum.FILE: {
|
||||
return roomType === RoomTypeEnum.GROUP
|
||||
? `${name}:${MSG_REPLY_TEXT_MAP[MsgEnum.FILE]}`
|
||||
: MSG_REPLY_TEXT_MAP[MsgEnum.FILE]
|
||||
}
|
||||
case MsgEnum.VOICE: {
|
||||
return roomType === RoomTypeEnum.GROUP
|
||||
? `${name}:${MSG_REPLY_TEXT_MAP[MsgEnum.VOICE]}`
|
||||
: MSG_REPLY_TEXT_MAP[MsgEnum.VOICE]
|
||||
}
|
||||
case MsgEnum.VIDEO: {
|
||||
return roomType === RoomTypeEnum.GROUP
|
||||
? `${name}:${MSG_REPLY_TEXT_MAP[MsgEnum.VIDEO]}`
|
||||
: MSG_REPLY_TEXT_MAP[MsgEnum.VIDEO]
|
||||
}
|
||||
case MsgEnum.EMOJI: {
|
||||
return roomType === RoomTypeEnum.GROUP
|
||||
? `${name}:${MSG_REPLY_TEXT_MAP[MsgEnum.EMOJI]}`
|
||||
: MSG_REPLY_TEXT_MAP[MsgEnum.EMOJI]
|
||||
}
|
||||
default: {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,131 @@
|
||||
// 在 Web Worker 中接收消息
|
||||
self.onmessage = (event) => {
|
||||
const number = event.data
|
||||
// 发消息给主进程
|
||||
import { WsResEnum } from '@/enums'
|
||||
|
||||
// 执行一些耗时的操作
|
||||
const result = calculateSquare(number)
|
||||
|
||||
// 将结果发送回主线程
|
||||
self.postMessage(result)
|
||||
const postMsg = ({ type, value }: { type: string; value?: object }) => {
|
||||
self.postMessage(JSON.stringify({ type, value }))
|
||||
}
|
||||
|
||||
// 一些耗时的操作
|
||||
const calculateSquare = (number: any) => {
|
||||
return number * number
|
||||
// ws instance
|
||||
let connection: WebSocket
|
||||
// 心跳 timer
|
||||
let heartTimer: number | null = null
|
||||
|
||||
// 重连次数上限
|
||||
const reconnectCountMax = 5
|
||||
let reconnectCount = 0
|
||||
// 重连 timer
|
||||
let timer: null | number = null
|
||||
// 重连🔐
|
||||
let lockReconnect = false
|
||||
// 重连🔐
|
||||
let token: null | string = null
|
||||
|
||||
// 往 ws 发消息
|
||||
const connectionSend = (value: object) => {
|
||||
connection?.send(JSON.stringify(value))
|
||||
}
|
||||
|
||||
// 发送心跳 10s 内发送
|
||||
const sendHeartPack = () => {
|
||||
// 10s 检测心跳
|
||||
heartTimer = setInterval(() => {
|
||||
// 心跳消息类型 2
|
||||
connectionSend({ type: 2 })
|
||||
}, 9900) as any
|
||||
}
|
||||
// 清除❤️跳 timer
|
||||
const clearHeartPackTimer = () => {
|
||||
if (heartTimer) {
|
||||
clearInterval(heartTimer)
|
||||
heartTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const onCloseHandler = () => {
|
||||
clearHeartPackTimer()
|
||||
// 已经在连接中就不重连了
|
||||
if (lockReconnect) return
|
||||
|
||||
// 标识重连中
|
||||
lockReconnect = true
|
||||
|
||||
// 清除 timer,避免任务堆积。
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
// 达到重连次数上限
|
||||
if (reconnectCount >= reconnectCountMax) {
|
||||
reconnectCount = 0
|
||||
postMsg({ type: WsResEnum.WS_ERROR, value: { msg: '连接失败,请检查网络或联系管理员' } })
|
||||
return
|
||||
}
|
||||
|
||||
// 断线重连
|
||||
timer = setTimeout(() => {
|
||||
initConnection()
|
||||
reconnectCount++
|
||||
// 标识已经开启重连任务
|
||||
lockReconnect = false
|
||||
}, 2000) as any
|
||||
}
|
||||
|
||||
// ws 连接 error
|
||||
const onConnectError = () => {
|
||||
if (connection?.readyState !== WebSocket.OPEN) {
|
||||
postMsg({ type: WsResEnum.WS_ERROR, value: { msg: '连接失败,请检查网络或联系管理员' } })
|
||||
return
|
||||
}
|
||||
onCloseHandler()
|
||||
postMsg({ type: 'error' })
|
||||
}
|
||||
// ws 连接 close
|
||||
const onConnectClose = () => {
|
||||
onCloseHandler()
|
||||
token = null
|
||||
postMsg({ type: 'close' })
|
||||
}
|
||||
// ws 连接成功
|
||||
const onConnectOpen = () => {
|
||||
postMsg({ type: 'open' })
|
||||
// 心跳❤️检测
|
||||
sendHeartPack()
|
||||
}
|
||||
// ws 连接 接收到消息
|
||||
const onConnectMsg = (e: any) => postMsg({ type: 'message', value: e.data })
|
||||
|
||||
// 初始化 ws 连接
|
||||
const initConnection = () => {
|
||||
connection?.removeEventListener('message', onConnectMsg)
|
||||
connection?.removeEventListener('open', onConnectOpen)
|
||||
connection?.removeEventListener('close', onConnectClose)
|
||||
connection?.removeEventListener('error', onConnectError)
|
||||
// 建立链接
|
||||
// 本地配置到 .env 里面修改。生产配置在 .env.production 里面
|
||||
connection = new WebSocket(`${import.meta.env.VITE_WEBSOCKET_URL}${token ? `?token=${token}` : ''}`)
|
||||
// 收到消息
|
||||
connection.addEventListener('message', onConnectMsg)
|
||||
// 建立链接
|
||||
connection.addEventListener('open', onConnectOpen)
|
||||
// 关闭连接
|
||||
connection.addEventListener('close', onConnectClose)
|
||||
// 连接错误
|
||||
connection.addEventListener('error', onConnectError)
|
||||
}
|
||||
|
||||
self.onmessage = (e: MessageEvent<string>) => {
|
||||
const { type, value } = JSON.parse(e.data)
|
||||
switch (type) {
|
||||
case 'initWS': {
|
||||
reconnectCount = 0
|
||||
token = value
|
||||
initConnection()
|
||||
break
|
||||
}
|
||||
case 'message': {
|
||||
if (connection?.readyState !== 1) return
|
||||
connectionSend(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
10
src/utils/console.ts
Normal file
10
src/utils/console.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import pkg from '../../package.json'
|
||||
|
||||
/** 控制台打印版本信息 */
|
||||
export const consolePrint = () => {
|
||||
console.log(
|
||||
`%c 🍀 ${pkg.name} ${pkg.version}`,
|
||||
'font-size:20px;border-left: 4px solid #13987f;background: #cef9ec;font-family: Comic Sans MS, cursive;color:#581845;padding:10px;border-radius:4px;',
|
||||
`${pkg.author.url}`
|
||||
)
|
||||
}
|
5
src/utils/unique.ts
Normal file
5
src/utils/unique.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import type { UserItem } from '@/services/types'
|
||||
|
||||
export const uniqueUserList = (arr: UserItem[]) => {
|
||||
return Object.values(Object.fromEntries(arr.map((item) => [item.uid, item])))
|
||||
}
|
56
src/utils/wsType.ts
Normal file
56
src/utils/wsType.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import type { UserInfoType, UserItem } from '@/services/types'
|
||||
|
||||
// 1.登录返回二维码 2.用户扫描成功等待授权 3.用户登录成功返回用户信息 4.收到消息 5.上下线推送 6.前端token失效
|
||||
export enum WsResponseMessageType {
|
||||
/** 1.登录返回二维码 */
|
||||
LoginQrCode = 1,
|
||||
/** 2.用户扫描成功等待授权 */
|
||||
WaitingAuthorize,
|
||||
/** 3.用户登录成功返回用户信息 */
|
||||
LoginSuccess,
|
||||
/** 4.收到消息 */
|
||||
ReceiveMessage,
|
||||
/** 5.上下线推送 */
|
||||
OnOffLine,
|
||||
/** 6.前端token失效 */
|
||||
TokenExpired,
|
||||
/** 7.禁用的用户 */
|
||||
InValidUser,
|
||||
/** 8.点赞、倒赞更新通知 */
|
||||
WSMsgMarkItem,
|
||||
/** 消息撤回 */
|
||||
WSMsgRecall,
|
||||
/** 新好友申请 */
|
||||
RequestNewFriend,
|
||||
/** 新好友会话 */
|
||||
NewFriendSession
|
||||
}
|
||||
|
||||
/**
|
||||
* ws 请求 消息类型 1.请求登录二维码,2心跳检测 3用户认证
|
||||
*/
|
||||
export enum WsRequestMsgType {
|
||||
/** 1.请求登录二维码 */
|
||||
RequestLoginQrCode = 1,
|
||||
/** 2心跳检测 */
|
||||
HeartBeatDetection,
|
||||
/** 3用户认证 */
|
||||
Authorization
|
||||
}
|
||||
|
||||
export type WsReqMsgContentType = {
|
||||
type: WsRequestMsgType
|
||||
data?: Record<string, unknown>
|
||||
}
|
||||
export type LoginInitResType = { loginUrl: string }
|
||||
|
||||
export type LoginSuccessResType = Pick<UserInfoType, 'avatar' | 'name' | 'uid'> & {
|
||||
/** 用户的登录凭证,每次请求携带 */
|
||||
token: string
|
||||
}
|
||||
|
||||
export type OnStatusChangeType = {
|
||||
changeList: UserItem[]
|
||||
onlineNum: number
|
||||
totalNum: number
|
||||
}
|
@ -42,7 +42,7 @@
|
||||
</n-flex>
|
||||
|
||||
<n-flex v-else vertical :size="6" class="tray">
|
||||
<n-flex @click="exit(0)" align="center" :size="10" class="p-[8px_6px] rounded-4px hover:bg-[--tray-hover-e]">
|
||||
<n-flex @click="handleExit" align="center" :size="10" class="p-[8px_6px] rounded-4px hover:bg-[--tray-hover-e]">
|
||||
<span>退出</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
@ -55,15 +55,25 @@ import { statusItem } from './home-window/onlineStatus/config.ts'
|
||||
import { onlineStatus } from '@/stores/onlineStatus.ts'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { useWsLoginStore } from '@/stores/ws.ts'
|
||||
|
||||
const { checkWinExist, createWebviewWindow, resizeWindow } = useWindow()
|
||||
const OLStatusStore = onlineStatus()
|
||||
const isLoginWin = ref(true)
|
||||
const loginStore = useWsLoginStore()
|
||||
const loginQrCode = computed(() => loginStore.loginQrCode)
|
||||
|
||||
const division = () => {
|
||||
return <div class={'h-1px bg-[--line-color] w-full'}></div>
|
||||
}
|
||||
|
||||
const handleExit = () => {
|
||||
exit(0)
|
||||
if (loginQrCode.value) {
|
||||
localStorage.removeItem('wsLogin')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleStatus = (url: string, title: string) => {
|
||||
OLStatusStore.setOnlineStatus(url, title)
|
||||
appWindow.hide()
|
||||
|
@ -5,82 +5,69 @@
|
||||
<n-scrollbar style="max-height: calc(100vh - 126px)">
|
||||
<n-collapse :display-directive="'show'">
|
||||
<ContextMenu @contextmenu="showMenu($event)" @select="handleSelect($event.label)" :menu="menuList">
|
||||
<n-collapse-item title="我的设备" name="1">
|
||||
<n-collapse-item title="我的好友" name="1">
|
||||
<template #header-extra>
|
||||
<p class="text-(10px #707070)">1/1</p>
|
||||
</template>
|
||||
<div>可以</div>
|
||||
</n-collapse-item>
|
||||
<n-collapse-item title="特别关心" name="2">
|
||||
<template #header-extra>
|
||||
<p class="text-(10px #707070)">1/1</p>
|
||||
<span class="text-(10px #707070)">1/1</span>
|
||||
</template>
|
||||
|
||||
<!-- 用户框 多套一层div来移除默认的右键事件然后覆盖掉因为margin空隙而导致右键可用 -->
|
||||
<div @contextmenu.stop="$event.preventDefault()">
|
||||
<div
|
||||
<n-flex
|
||||
v-slide
|
||||
@click="handleClick(item.key, 2)"
|
||||
:class="{ active: activeItem === item.key }"
|
||||
:size="10"
|
||||
@click="handleClick(item.uid, RoomTypeEnum.SINGLE)"
|
||||
:class="{ active: activeItem === item.uid }"
|
||||
class="user-box w-full h-75px mb-5px"
|
||||
v-for="item in friendsList"
|
||||
:key="item.key">
|
||||
<div class="flex items-center h-full pl-6px pr-8px gap-10px">
|
||||
<img class="size-44px rounded-50% bg-#fff border-(1px solid #f1f1f1)" :src="item.avatar" alt="" />
|
||||
v-for="item in contactStore.contactsList"
|
||||
:key="item.uid">
|
||||
<n-flex v-slide align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
|
||||
<n-avatar
|
||||
round
|
||||
bordered
|
||||
:color="'#fff'"
|
||||
:size="44"
|
||||
:src="useUserInfo(item.uid).value.avatar"
|
||||
fallback-src="/logo.png" />
|
||||
|
||||
<div class="h-38px flex flex-1 flex-col justify-between">
|
||||
<div class="text-14px flex-y-center gap-4px">
|
||||
{{ item.accountName }}
|
||||
</div>
|
||||
<n-flex vertical justify="space-between" class="h-fit flex-1 truncate">
|
||||
<span class="text-14px leading-tight flex-1 truncate">{{
|
||||
useUserInfo(item.uid).value.name
|
||||
}}</span>
|
||||
|
||||
<div
|
||||
class="text w-155px h-14px text-12px flex-y-center gap-4px"
|
||||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">
|
||||
<p class="text-12px">[⛅今日天气]</p>
|
||||
<p>说的很经典哈萨克的哈萨克看到贺卡上</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text leading-tight text-12px flex-1 truncate">
|
||||
[⛅今日天气] 说的很经典哈萨克的哈萨克看到贺卡上
|
||||
</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</div>
|
||||
</n-collapse-item>
|
||||
<n-collapse-item title="默认分组" name="3">
|
||||
<template #header-extra>
|
||||
<p class="text-(10px #707070)">1/1</p>
|
||||
</template>
|
||||
|
||||
<div>123</div>
|
||||
</n-collapse-item>
|
||||
</ContextMenu>
|
||||
</n-collapse>
|
||||
</n-scrollbar>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="2" tab="群聊">
|
||||
<div
|
||||
@click="handleClick(item.key, 1)"
|
||||
:class="{ active: activeItem === item.key }"
|
||||
class="w-full h-75px mb-5px cursor-pointer"
|
||||
v-for="item in groupChatList"
|
||||
:key="item.key">
|
||||
<!-- 消息框,使用v-slide自定义指令来自动抉择右键菜单位置 -->
|
||||
<div v-slide class="flex items-center h-full pl-6px pr-8px gap-10px">
|
||||
<img class="size-44px rounded-50% bg-#fff border-(1px solid #f1f1f1)" :src="item.avatar" alt="" />
|
||||
<!-- <n-tab-pane name="2" tab="群聊">-->
|
||||
<!-- <div-->
|
||||
<!-- @click="handleClick(item.key, RoomTypeEnum.GROUP)"-->
|
||||
<!-- :class="{ active: activeItem === item.key }"-->
|
||||
<!-- class="w-full h-75px mb-5px"-->
|
||||
<!-- v-for="item in groupChatList"-->
|
||||
<!-- :key="item.key">-->
|
||||
<!-- <n-flex v-slide align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">-->
|
||||
<!-- <n-avatar round bordered :color="'#fff'" :size="44" :src="item.avatar" fallback-src="/logo.png" />-->
|
||||
|
||||
<div class="h-38px flex flex-1 flex-col justify-center">
|
||||
<div class="flex-between-center">
|
||||
<span class="text-14px">{{ item.accountName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<!-- <span class="text-14px leading-tight flex-1 truncate">{{ item.accountName }}</span>-->
|
||||
<!-- </n-flex>-->
|
||||
<!-- </div>-->
|
||||
<!-- </n-tab-pane>-->
|
||||
</n-tabs>
|
||||
</n-scrollbar>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { MockList } from '@/mock/index.ts'
|
||||
import { MittEnum } from '@/enums'
|
||||
import { MittEnum, RoomTypeEnum } from '@/enums'
|
||||
import { useContactStore } from '@/stores/contacts.ts'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
|
||||
const menuList = ref([
|
||||
{ label: '添加分组', icon: 'plus' },
|
||||
@ -91,10 +78,7 @@ const menuList = ref([
|
||||
const activeItem = ref(0)
|
||||
const detailsShow = ref(false)
|
||||
const shrinkStatus = ref(false)
|
||||
|
||||
const friendsList = ref(MockList.value.filter((item) => item.type === 2))
|
||||
const groupChatList = ref(MockList.value.filter((item) => item.type === 1))
|
||||
|
||||
const contactStore = useContactStore()
|
||||
/** 监听独立窗口关闭事件 */
|
||||
watchEffect(() => {
|
||||
Mitt.on(MittEnum.SHRINK_WINDOW, async (event) => {
|
||||
@ -106,11 +90,10 @@ const handleClick = (index: number, type: number) => {
|
||||
detailsShow.value = true
|
||||
activeItem.value = index
|
||||
const data = {
|
||||
type: type,
|
||||
data:
|
||||
type === 1
|
||||
? groupChatList.value.filter((item) => item.key === index)
|
||||
: friendsList.value.filter((item) => item.key === index),
|
||||
context: {
|
||||
type: type,
|
||||
uid: index
|
||||
},
|
||||
detailsShow: detailsShow.value
|
||||
}
|
||||
Mitt.emit(MittEnum.DETAILS_SHOW, data)
|
||||
|
@ -1,40 +1,50 @@
|
||||
<template>
|
||||
<!-- 消息列表 // TODO 使用虚拟列表组件就用不了动画和拖动了 (nyh -> 2024-03-28 06:01:00) -->
|
||||
<n-scrollbar ref="scrollbar" v-if="MockList.length > 0" style="max-height: calc(100vh - 70px)">
|
||||
<n-scrollbar ref="scrollbar" style="max-height: calc(100vh - 70px)">
|
||||
<!-- 可拖拽排序组件 -->
|
||||
<VueDraggable target=".sort-target" :animation="150" v-model="MockList" class="p-[4px_10px_0px_8px]">
|
||||
<VueDraggable v-model="sessionList" :animation="150" class="p-[4px_10px_0px_8px]" target=".sort-target">
|
||||
<!-- 右键菜单组件 -->
|
||||
<TransitionGroup type="transition" tag="div" name="fade" class="sort-target">
|
||||
<TransitionGroup class="sort-target" name="fade" tag="div" type="transition">
|
||||
<ContextMenu
|
||||
@select="$event.click(item)"
|
||||
@click="handleMsgClick(item)"
|
||||
@dblclick="handleMsgDblclick(item)"
|
||||
:data-key="item.key"
|
||||
v-for="item in sessionList"
|
||||
:key="item.roomId"
|
||||
:class="{ active: currentSession === item.roomId }"
|
||||
:data-key="item.roomId"
|
||||
:menu="menuList"
|
||||
:special-menu="specialMenuList"
|
||||
:class="{ active: activeIndex === item.key }"
|
||||
class="msg-box w-full h-75px mb-5px"
|
||||
v-for="item in MockList"
|
||||
:key="item.key">
|
||||
@click="onSelectSelectSession(item, item.type)"
|
||||
@dblclick="handleMsgDblclick(item)"
|
||||
@select="$event.click(item)">
|
||||
<!-- 消息框,使用v-slide自定义指令来自动抉择右键菜单位置 -->
|
||||
<n-flex v-slide align="center" :size="10" class="h-75px pl-6px pr-8px">
|
||||
<n-avatar round bordered :color="'#fff'" :size="44" :src="item.avatar" fallback-src="/logo.png" alt="" />
|
||||
<n-flex v-slide :size="10" align="center" class="h-75px pl-6px pr-8px flex-1">
|
||||
<n-avatar :color="'#fff'" :size="44" :src="item.avatar" bordered fallback-src="/logo.png" round />
|
||||
|
||||
<n-flex vertical justify="space-between" class="h-38px flex-1">
|
||||
<n-flex align="center" justify="space-between">
|
||||
<span class="text-14px">{{ item.accountName }}</span>
|
||||
<span class="text text-10px">昨天</span>
|
||||
<n-flex class="h-fit flex-1 truncate" justify="space-between" vertical>
|
||||
<n-flex :size="4" align="center" class="flex-1 truncate" justify="space-between">
|
||||
<n-flex :size="0" align="center" class="leading-tight flex-1 truncate">
|
||||
<span class="text-14px leading-tight flex-1 truncate">{{ item.name }}</span>
|
||||
<n-popover trigger="hover" v-if="item.hot_Flag === IsAllUserEnum.Yes">
|
||||
<template #trigger>
|
||||
<svg
|
||||
:class="[currentSession === item.roomId ? 'color-#33ceab' : 'color-#13987f']"
|
||||
class="size-20px select-none outline-none cursor-pointer">
|
||||
<use href="#auth"></use>
|
||||
</svg>
|
||||
</template>
|
||||
<span>官方群聊认证</span>
|
||||
</n-popover>
|
||||
</n-flex>
|
||||
<span class="text text-10px w-fit truncate text-right">{{ item.lastMsgTime }}</span>
|
||||
</n-flex>
|
||||
|
||||
<n-flex align="center" justify="space-between">
|
||||
<span
|
||||
class="text w-135px text-12px"
|
||||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">
|
||||
说的很经典哈萨克的哈萨克看到贺卡上
|
||||
<span class="text flex-1 leading-tight text-12px truncate">
|
||||
{{ item.lastMsg.replace(':', ':') }}
|
||||
</span>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<n-badge :value="msgTotal" :max="99" />
|
||||
<n-badge :max="99" :value="item.unreadCount" />
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
@ -42,31 +52,66 @@
|
||||
</TransitionGroup>
|
||||
</VueDraggable>
|
||||
</n-scrollbar>
|
||||
|
||||
<!-- 暂无消息 -->
|
||||
<n-flex id="image-no-data" v-else :size="20" align="center" vertical>
|
||||
<n-image
|
||||
width="120px"
|
||||
height="110px"
|
||||
src="src/assets/img/no_data.svg"
|
||||
lazy
|
||||
preview-disabled
|
||||
:intersection-observer-options="{
|
||||
root: '#image-no-data'
|
||||
}" />
|
||||
<span>暂无消息</span>
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { MockList } from '@/mock'
|
||||
import { useMessage } from '@/hooks/useMessage.ts'
|
||||
import { MittEnum } from '@/enums'
|
||||
import { MittEnum, MsgEnum, RoomTypeEnum } from '@/enums'
|
||||
import { IsAllUserEnum, SessionItem } from '@/services/types.ts'
|
||||
import { formatTimestamp } from '@/utils/ComputedTime.ts'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useUserInfo } from '@/hooks/useCached.ts'
|
||||
import { renderReplyContent } from '@/utils/RenderReplyContent.ts'
|
||||
import { useCommon } from '@/hooks/useCommon.ts'
|
||||
|
||||
const msgTotal = ref(0)
|
||||
const chatStore = useChatStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const { userUid } = useCommon()
|
||||
const scrollbar = ref()
|
||||
const { activeIndex, handleMsgClick, menuList, specialMenuList, handleMsgDblclick } = useMessage()
|
||||
const { handleMsgClick, activeIndex, menuList, specialMenuList, handleMsgDblclick } = useMessage()
|
||||
const sessionList = computed(() =>
|
||||
chatStore.sessionList.map((item) => {
|
||||
// 最后一条消息内容
|
||||
const lastMsg = Array.from(chatStore.messageMap.get(item.roomId)?.values() || [])?.slice(-1)?.[0]
|
||||
let LastUserMsg = ''
|
||||
if (lastMsg) {
|
||||
const lastMsgUserName = useUserInfo(lastMsg.fromUser.uid)
|
||||
LastUserMsg =
|
||||
lastMsg.message?.type === MsgEnum.RECALL
|
||||
? item.type === RoomTypeEnum.GROUP
|
||||
? `${lastMsgUserName.value.name}:撤回了一条消息`
|
||||
: lastMsg.fromUser.uid === userUid.value
|
||||
? '你撤回了一条消息'
|
||||
: '对方撤回了一条消息'
|
||||
: (renderReplyContent(
|
||||
lastMsgUserName.value.name,
|
||||
lastMsg.message?.type,
|
||||
lastMsg.message?.body?.content || lastMsg.message?.body,
|
||||
item.type
|
||||
) as string)
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
lastMsg: LastUserMsg || item.text || '欢迎使用HuLa',
|
||||
lastMsgTime: formatTimestamp(item?.activeTime)
|
||||
}
|
||||
})
|
||||
)
|
||||
const msgTotal = computed(() => {
|
||||
return sessionList.value.reduce((total, item) => total + item.unreadCount, 0)
|
||||
})
|
||||
// 选中的聊天对话
|
||||
const currentSession = computed(() => activeIndex.value)
|
||||
|
||||
// 选中会话
|
||||
const onSelectSelectSession = (item: SessionItem, roomType: RoomTypeEnum) => {
|
||||
handleMsgClick(item)
|
||||
globalStore.currentSession.roomId = item.roomId
|
||||
globalStore.currentSession.type = roomType
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
// TODO 如果当前信息栏中没有该信息就创建一条 (nyh -> 2024-03-22 01:05:22)
|
||||
@ -76,25 +121,30 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
// 请求回话列表
|
||||
chatStore.getSessionList(true)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// TODO 这里的key后面如果换成用户唯一标识的时候记得更换data-key的值 (nyh -> 2024-03-28 18:56:20)
|
||||
if (activeIndex.value !== -1) {
|
||||
nextTick(() => {
|
||||
const activeElement = document.querySelector(`.msg-box[data-key="${activeIndex.value}"]`) as HTMLElement
|
||||
const rect = activeElement.getBoundingClientRect()
|
||||
scrollbar.value.scrollTo({
|
||||
top: rect.top - 75,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
})
|
||||
}
|
||||
// if (currentSession.value.roomId !== -1) {
|
||||
// nextTick(() => {
|
||||
// const activeElement = document.querySelector(`.msg-box[data-key="${currentSession.value.roomId}"]`) as HTMLElement
|
||||
// const rect = activeElement.getBoundingClientRect()
|
||||
// scrollbar.value.scrollTo({
|
||||
// top: rect.top - 75,
|
||||
// behavior: 'smooth'
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// setInterval(() => {
|
||||
// msgTotal.value++
|
||||
// }, 1000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/scss/message';
|
||||
#image-no-data {
|
||||
@apply size-full mt-60px text-[--text-color] text-14px;
|
||||
|
@ -13,7 +13,7 @@
|
||||
v-for="(item, index) in titleList"
|
||||
:key="index">
|
||||
<div
|
||||
@click="handleTheme($event, item.code)"
|
||||
@click="handleTheme(item.code)"
|
||||
class="size-full rounded-8px cursor-pointer"
|
||||
:style="activeItem === item.code ? 'border: 2px solid #13987f' : 'border: 2px solid transparent'">
|
||||
<component :is="item.model" />
|
||||
@ -89,7 +89,7 @@
|
||||
<script setup lang="tsx">
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { CloseBxEnum, ThemeEnum } from '@/enums'
|
||||
import { CloseBxEnum } from '@/enums'
|
||||
import { titleList } from './model.tsx'
|
||||
import { sendOptions } from './config.ts'
|
||||
|
||||
@ -98,40 +98,13 @@ const { themes, tips, escClose, chat } = storeToRefs(settingStore)
|
||||
const activeItem = ref<string>(themes.value.pattern)
|
||||
|
||||
/** 切换主题 */
|
||||
const handleTheme = async (event: MouseEvent, code: string) => {
|
||||
const handleTheme = (code: string) => {
|
||||
if (code === themes.value.pattern) return
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))
|
||||
|
||||
let isDark: boolean
|
||||
|
||||
settingStore.toggleTheme(code)
|
||||
/**判断当前浏览器是否支持startViewTransition API*/
|
||||
if (document.startViewTransition) {
|
||||
const transition = document.startViewTransition(() => {
|
||||
isDark = code.includes(ThemeEnum.DARK)
|
||||
})
|
||||
// TODO 从亮色主题切换到暗色主题的时候没有动画效果 (nyh -> 2024-02-12 23:07:54)
|
||||
transition.ready.then(() => {
|
||||
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: isDark ? [...clipPath].reverse() : clipPath
|
||||
},
|
||||
{
|
||||
duration: 500,
|
||||
easing: 'ease-in',
|
||||
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)'
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/scss/toggle-theme';
|
||||
.item {
|
||||
@apply bg-[--bg-setting-item] rounded-12px size-full p-12px box-border;
|
||||
}
|
||||
|
@ -9,7 +9,10 @@
|
||||
<n-flex vertical :size="25" v-if="!isAutoLogin">
|
||||
<!-- 头像 -->
|
||||
<n-flex justify="center" class="w-full mt-35px">
|
||||
<img class="w-80px h-80px rounded-50% bg-#fff border-(2px solid #fff)" :src="avatarRef || '/logo.png'" alt="" />
|
||||
<img
|
||||
class="w-80px h-80px rounded-50% bg-#fff border-(2px solid #fff)"
|
||||
:src="info.avatar || '/logo.png'"
|
||||
alt="" />
|
||||
</n-flex>
|
||||
|
||||
<!-- 登录菜单 -->
|
||||
@ -19,7 +22,7 @@
|
||||
size="large"
|
||||
maxlength="16"
|
||||
minlength="6"
|
||||
v-model:value="accountRef"
|
||||
v-model:value="info.account"
|
||||
type="text"
|
||||
:placeholder="accountPH"
|
||||
@focus="accountPH = ''"
|
||||
@ -62,7 +65,7 @@
|
||||
maxlength="16"
|
||||
minlength="6"
|
||||
size="large"
|
||||
v-model:value="passwordRef"
|
||||
v-model:value="info.password"
|
||||
type="password"
|
||||
:placeholder="passwordPH"
|
||||
@focus="passwordPH = ''"
|
||||
@ -100,10 +103,12 @@
|
||||
<n-flex :size="30" vertical>
|
||||
<!-- 头像 -->
|
||||
<n-flex justify="center">
|
||||
<img
|
||||
class="w-110px h-110px rounded-50% bg-#fff border-(2px solid #fff)"
|
||||
:src="login.accountInfo.avatar || '/logo.png'"
|
||||
alt="" />
|
||||
<n-avatar
|
||||
round
|
||||
:size="110"
|
||||
:color="'#fff'"
|
||||
class="border-(2px solid #fff)"
|
||||
:src="login.accountInfo.avatar || '/logo.png'" />
|
||||
</n-flex>
|
||||
|
||||
<n-flex justify="center">
|
||||
@ -116,18 +121,19 @@
|
||||
:loading="loading"
|
||||
:disabled="loginDisabled"
|
||||
class="w-200px mt-12px mb-40px"
|
||||
@click="loginWin"
|
||||
@click="autoLogin"
|
||||
color="#13987f">
|
||||
{{ loginText }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<!-- 顶部操作栏 -->
|
||||
<n-flex justify="center" class="text-14px">
|
||||
<!-- 底部操作栏 -->
|
||||
<n-flex justify="center" class="text-14px" id="bottomBar">
|
||||
<div class="color-#13987f cursor-pointer" @click="router.push('/qrCode')">扫码登录</div>
|
||||
<div class="w-1px h-14px bg-#ccc"></div>
|
||||
<n-popover trigger="click" :show-checkmark="false" :show-arrow="false">
|
||||
<div v-if="isAutoLogin" class="color-#13987f cursor-pointer">移除账号</div>
|
||||
<n-popover v-else trigger="click" :show-checkmark="false" :show-arrow="false">
|
||||
<template #trigger>
|
||||
<div class="color-#13987f cursor-pointer">更多选项</div>
|
||||
</template>
|
||||
@ -151,10 +157,17 @@ import { useLogin } from '@/hooks/useLogin.ts'
|
||||
|
||||
const settingStore = setting()
|
||||
const { login } = storeToRefs(settingStore)
|
||||
const accountRef = ref()
|
||||
const passwordRef = ref()
|
||||
const avatarRef = ref()
|
||||
const nameRef = ref()
|
||||
/** 账号信息 */
|
||||
const info = ref({
|
||||
account: '',
|
||||
password: '',
|
||||
avatar: '',
|
||||
name: '',
|
||||
uid: 0
|
||||
})
|
||||
/** 是否中断登录 */
|
||||
const interruptLogin = ref(false)
|
||||
/** 协议 */
|
||||
const protocol = ref()
|
||||
const loginDisabled = ref(false)
|
||||
const loading = ref(false)
|
||||
@ -168,28 +181,32 @@ const accountOption = ref<STO.Setting['login']['accountInfo'][]>([
|
||||
password: '123456',
|
||||
name: '超级GG帮',
|
||||
avatar: 'https://picsum.photos/140?1',
|
||||
uid: '123456'
|
||||
uid: 123456,
|
||||
token: 'test'
|
||||
},
|
||||
{
|
||||
account: 'hula1',
|
||||
password: '123456',
|
||||
name: '二狗子',
|
||||
avatar: 'https://picsum.photos/140?2',
|
||||
uid: '123456'
|
||||
uid: 123456,
|
||||
token: 'test'
|
||||
},
|
||||
{
|
||||
account: 'hula2',
|
||||
password: '123456',
|
||||
name: '李山离',
|
||||
avatar: 'https://picsum.photos/140?3',
|
||||
uid: '123456'
|
||||
uid: 123456,
|
||||
token: 'test'
|
||||
},
|
||||
{
|
||||
account: 'hula3',
|
||||
password: '123456',
|
||||
name: '牛什么呢',
|
||||
avatar: 'https://picsum.photos/140?4',
|
||||
uid: '123456'
|
||||
uid: 123456,
|
||||
token: 'test'
|
||||
}
|
||||
])
|
||||
const accountPH = ref('输入HuLa账号')
|
||||
@ -199,10 +216,13 @@ const loginText = ref('登录')
|
||||
const { createWebviewWindow } = useWindow()
|
||||
|
||||
watchEffect(() => {
|
||||
loginDisabled.value = !(accountRef.value && passwordRef.value && protocol.value)
|
||||
loginDisabled.value = !(info.value.account && info.value.password && protocol.value)
|
||||
// 清空账号的时候设置默认头像
|
||||
if (!accountRef.value) {
|
||||
avatarRef.value = '/logo.png'
|
||||
if (!info.value.account) {
|
||||
info.value.avatar = '/logo.png'
|
||||
}
|
||||
if (interruptLogin.value) {
|
||||
loginDisabled.value = false
|
||||
}
|
||||
})
|
||||
|
||||
@ -217,9 +237,9 @@ const delAccount = (index: number) => {
|
||||
if (lengthBeforeDelete === 1 && accountOption.value.length === 0) {
|
||||
arrowStatus.value = false
|
||||
}
|
||||
accountRef.value = null
|
||||
passwordRef.value = null
|
||||
avatarRef.value = '/logo.png'
|
||||
info.value.account = ''
|
||||
info.value.password = ''
|
||||
info.value.avatar = '/logo.png'
|
||||
}
|
||||
|
||||
/**
|
||||
@ -227,39 +247,57 @@ const delAccount = (index: number) => {
|
||||
* @param item 账户信息
|
||||
* */
|
||||
const giveAccount = (item: STO.Setting['login']['accountInfo']) => {
|
||||
const { account, password, avatar, name } = item
|
||||
accountRef.value = account
|
||||
passwordRef.value = password
|
||||
avatarRef.value = avatar
|
||||
nameRef.value = name
|
||||
const { account, password, avatar, name, uid } = item
|
||||
info.value.account = account || ''
|
||||
info.value.password = password || ''
|
||||
info.value.avatar = avatar
|
||||
info.value.name = name
|
||||
info.value.uid = uid
|
||||
arrowStatus.value = false
|
||||
}
|
||||
|
||||
/**登录后创建主页窗口*/
|
||||
const loginWin = () => {
|
||||
if (interruptLogin.value) return
|
||||
loading.value = true
|
||||
delay(async () => {
|
||||
await createWebviewWindow('HuLa', 'home', 960, 720, 'login', false, true)
|
||||
loading.value = false
|
||||
if (!login.value.autoLogin || login.value.accountInfo.password === '') {
|
||||
settingStore.setAccountInfo({
|
||||
account: accountRef.value,
|
||||
password: passwordRef.value,
|
||||
avatar: avatarRef.value,
|
||||
name: nameRef.value,
|
||||
uid: '123456'
|
||||
account: info.value.account,
|
||||
password: info.value.password,
|
||||
avatar: info.value.avatar,
|
||||
name: info.value.name,
|
||||
uid: info.value.uid,
|
||||
token: 'test'
|
||||
})
|
||||
await setLoginState()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**监听是否点击了除了下拉框外的其他地方*/
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
/** 自动登录 */
|
||||
const autoLogin = () => {
|
||||
interruptLogin.value = false
|
||||
isAutoLogin.value = true
|
||||
// TODO 检查用户网络是否连接 (nyh -> 2024-03-16 12:06:59)
|
||||
loginText.value = '网络连接中'
|
||||
delay(async () => {
|
||||
loginWin()
|
||||
loginText.value = '登录'
|
||||
await setLoginState()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const closeMenu = (event: MouseEvent) => {
|
||||
const target = event.target as Element
|
||||
if (!target.matches('.account-box, .account-box *, .down')) {
|
||||
arrowStatus.value = false
|
||||
}
|
||||
if (target.matches('#bottomBar *') && isAutoLogin.value) {
|
||||
interruptLogin.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -267,20 +305,13 @@ onMounted(async () => {
|
||||
console.error('设置无状态图标失败:', error)
|
||||
})
|
||||
if (login.value.autoLogin && login.value.accountInfo.password !== '') {
|
||||
isAutoLogin.value = true
|
||||
// TODO 检查用户网络是否连接 (nyh -> 2024-03-16 12:06:59)
|
||||
loginText.value = '网络连接中'
|
||||
delay(async () => {
|
||||
loginWin()
|
||||
loginText.value = '登录'
|
||||
await setLoginState()
|
||||
}, 1000)
|
||||
autoLogin()
|
||||
}
|
||||
window.addEventListener('click', handleClickOutside, true)
|
||||
window.addEventListener('click', closeMenu, true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', handleClickOutside, true)
|
||||
window.removeEventListener('click', closeMenu, true)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -14,18 +14,19 @@
|
||||
v-else
|
||||
:size="180"
|
||||
class="rounded-12px relative"
|
||||
:class="{ blur: scanSuccess }"
|
||||
:class="{ blur: scanStatus.show }"
|
||||
:value="QRCode"
|
||||
icon-src="/logo.png"
|
||||
error-correction-level="H" />
|
||||
<!-- 二维码状态 -->
|
||||
<n-flex
|
||||
v-if="scanSuccess"
|
||||
v-if="scanStatus.show"
|
||||
vertical
|
||||
:size="12"
|
||||
align="center"
|
||||
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
<svg class="size-42px"><use href="#success"></use></svg>
|
||||
<span class="text-(16px #e3e3e3)">扫码成功</span>
|
||||
<svg class="size-42px animate-pulse"><use :href="`#${scanStatus.icon}`"></use></svg>
|
||||
<span class="text-(16px #e3e3e3)">{{ scanStatus.text }}</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
@ -33,7 +34,7 @@
|
||||
|
||||
<!-- 顶部操作栏 -->
|
||||
<n-flex justify="center" class="text-14px mt-48px">
|
||||
<div class="color-#13987f cursor-pointer" @click="toLogin">账密登录</div>
|
||||
<div class="color-#13987f cursor-pointer" @click="router.push('/login')">账密登录</div>
|
||||
<div class="w-1px h-14px bg-#ccc"></div>
|
||||
<div class="color-#13987f cursor-pointer">注册账号</div>
|
||||
</n-flex>
|
||||
@ -43,48 +44,89 @@
|
||||
import router from '@/router'
|
||||
import { delay } from 'lodash-es'
|
||||
import { lightTheme } from 'naive-ui'
|
||||
import { initWebSocket, sendToServer } from '@/services/webSocket.ts'
|
||||
import { WsReqEnum, WsResEnum } from '@/enums'
|
||||
import { WsResEnum } from '@/enums'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { useLogin } from '@/hooks/useLogin.ts'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { LoginStatus, useWsLoginStore } from '@/stores/ws.ts'
|
||||
|
||||
const settingStore = setting()
|
||||
const loginStore = useWsLoginStore()
|
||||
/** 获取登录二维码 */
|
||||
const loginQrCode = computed(() => loginStore.loginQrCode)
|
||||
/** 登录状态 */
|
||||
const loginStatus = computed(() => loginStore.loginStatus)
|
||||
const { setLoginState } = useLogin()
|
||||
const { createWebviewWindow } = useWindow()
|
||||
const loading = ref(true)
|
||||
const loadText = ref('加载中...')
|
||||
const QRCode = ref()
|
||||
const scanSuccess = ref(false)
|
||||
const scanStatus = ref<{
|
||||
status: 'error' | 'success'
|
||||
icon: 'cloudError' | 'success'
|
||||
text: string
|
||||
show: boolean
|
||||
}>({ status: 'success', icon: 'success', text: '扫码成功', show: false })
|
||||
|
||||
const toLogin = () => {
|
||||
router.push('/login')
|
||||
watchEffect(() => {
|
||||
// 等待授权中
|
||||
if (loginStatus.value === LoginStatus.Waiting) {
|
||||
loadText.value = '等待授权...'
|
||||
}
|
||||
})
|
||||
|
||||
/** 处理二维码显示和刷新 */
|
||||
const handleQRCodeLogin = () => {
|
||||
QRCode.value = loginQrCode.value
|
||||
loading.value = false
|
||||
loadText.value = '请使用微信扫码登录'
|
||||
}
|
||||
|
||||
/** 处理登录成功 */
|
||||
const handleLoginSuccess = async (e: any) => {
|
||||
scanStatus.value.show = true
|
||||
loadText.value = '登录中...'
|
||||
delay(async () => {
|
||||
await createWebviewWindow('HuLa', 'home', 960, 720, 'login', false, true)
|
||||
settingStore.setAccountInfo({
|
||||
avatar: e.avatar,
|
||||
name: e.name,
|
||||
uid: e.uid,
|
||||
token: e.token
|
||||
})
|
||||
await setLoginState()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/** 处理失败场景 */
|
||||
const handleError = (e: any) => {
|
||||
loading.value = false
|
||||
scanStatus.value = {
|
||||
status: 'error',
|
||||
icon: 'cloudError',
|
||||
text: e,
|
||||
show: true
|
||||
}
|
||||
loadText.value = '请稍后再试'
|
||||
}
|
||||
|
||||
// TODO 做一个二维码过期时间重新刷新二维码的功能 (nyh -> 2024-01-27 00:37:18)
|
||||
onMounted(() => {
|
||||
initWebSocket()
|
||||
Mitt.on(WsResEnum.QRCODE_LOGIN, (e: any) => {
|
||||
QRCode.value = e.data.loginUrl
|
||||
loading.value = false
|
||||
loadText.value = '请使用微信扫码登录'
|
||||
if (!localStorage.getItem('wsLogin')) {
|
||||
loginStore.getLoginQrCode()
|
||||
} else {
|
||||
handleQRCodeLogin()
|
||||
}
|
||||
Mitt.on(WsResEnum.QRCODE_LOGIN, () => {
|
||||
handleQRCodeLogin()
|
||||
})
|
||||
Mitt.on(WsResEnum.LOGIN_SUCCESS, (e: any) => {
|
||||
scanSuccess.value = true
|
||||
loadText.value = '登录中...'
|
||||
delay(async () => {
|
||||
await createWebviewWindow('HuLa', 'home', 960, 720, 'login', false, true)
|
||||
settingStore.setAccountInfo({
|
||||
avatar: e.data.avatar,
|
||||
name: e.data.name,
|
||||
uid: e.data.uid
|
||||
})
|
||||
await setLoginState()
|
||||
}, 1000)
|
||||
handleLoginSuccess(e)
|
||||
})
|
||||
Mitt.on(WsResEnum.WS_ERROR, (e: any) => {
|
||||
handleError(e.msg)
|
||||
})
|
||||
delay(() => {
|
||||
sendToServer({ type: WsReqEnum.LOGIN })
|
||||
}, 1000)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -25,7 +25,8 @@
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
@ -31,6 +31,8 @@ export default defineConfig({
|
||||
'absolute-rt': 'absolute right-0 top-0',
|
||||
'absolute-rb': 'absolute right-0 bottom-0',
|
||||
'absolute-center': 'absolute-lt flex-center size-full',
|
||||
'absolute-x-center': 'absolute-lt flex-x-center size-full',
|
||||
'absolute-y-center': 'absolute-lt flex-y-center size-full',
|
||||
'fixed-lt': 'fixed left-0 top-0',
|
||||
'fixed-lb': 'fixed left-0 bottom-0',
|
||||
'fixed-rt': 'fixed right-0 top-0',
|
||||
|
@ -7,13 +7,13 @@ import { getRootPath, getSrcPath } from './build/config/getPath'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import unocss from '@unocss/vite'
|
||||
import terser from '@rollup/plugin-terser'
|
||||
import { atStartup } from './build/config/console'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
/**! 不需要优化前端打包(如开启gzip) */
|
||||
export default defineConfig(({ mode }: ConfigEnv) => {
|
||||
// 获取当前环境的配置,如何设置第三个参数则加载所有变量,而不是以“VITE_”前缀的变量
|
||||
const config = loadEnv(mode, '/')
|
||||
console.log(config)
|
||||
const config = loadEnv(mode, process.cwd())
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
@ -35,6 +35,8 @@ export default defineConfig(({ mode }: ConfigEnv) => {
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true'
|
||||
},
|
||||
plugins: [
|
||||
/**! 启动时候打印项目信息(不需要可关闭) */
|
||||
atStartup(config, mode),
|
||||
/**
|
||||
* !实验性功能
|
||||
* 开启defineProps解构语法
|
||||
@ -43,7 +45,7 @@ export default defineConfig(({ mode }: ConfigEnv) => {
|
||||
vueJsx(), // 开启jsx功能
|
||||
unocss(), // 开启unocss
|
||||
AutoImport({
|
||||
imports: ['vue', { 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'] }],
|
||||
imports: ['vue', { 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar', 'useModal'] }],
|
||||
dts: 'src/typings/auto-imports.d.ts'
|
||||
}),
|
||||
/**自动导入组件,但是不会自动导入jsx和tsx*/
|
||||
@ -90,6 +92,17 @@ export default defineConfig(({ mode }: ConfigEnv) => {
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
//配置跨域
|
||||
proxy: {
|
||||
'/api': {
|
||||
// “/api” 以及前置字符串会被替换为真正域名
|
||||
target: config.VITE_SERVICE_URL, // 请求域名
|
||||
changeOrigin: true, // 是否跨域
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
},
|
||||
hmr: true, // 热更新
|
||||
cors: true, // 配置 CORS
|
||||
host: '0.0.0.0',
|
||||
port: 6130,
|
||||
strictPort: true,
|
||||
|
Loading…
Reference in New Issue
Block a user