mirror of
https://github.com/HuLaSpark/HuLa.git
synced 2026-08-30 17:14:56 +08:00
fix(common): 🐛 fix some defects and improve mobile terminal (#335)
* fix(common): 🐛 chatmain scroll behavior * fix(common): 🐛 chatMain scroll bug * fix(common): 🐛 p2p chat sendTime * feat(mobile): ✨ added friend/group page and functionality * fix(common): 🐛 order group member * style(common): 💄 refactor layout center * fix(agreement): 🐛 chat and announcements * fix(common): 🐛 group basic information editing * refactor(common): ♻️ union config * ci(config): 🎡 change some configuration * refactor(common): ♻️ config yudao * fix(common): 🐛 exit group * refactor(common): ♻️ tencent config * fix(common): 🐛 network config * feat(mobile): ✨ added Friend or Group Message Page * fix(common): 🐛 exit group error * feat(message): ✨ add message management settings * fix(mobile): 🐛 fix the issue where the mobile client fails to locate the configuration file * fix(mobile): 🐛 resolve mobile misidentification of local and production environments * feat(mobile): ✨ optimize the maximum width of bot messages and reply messages * feat(mobile): ✨ optimize the maximum width of image messages * feat(common): ✨ msg multi choose relay * build(vite): 📦 update startup and vite configuration * perf(message forwarding): ⚡ optimize message forwarding styles * fix(common): 🐛 msg time block * feat(mobile): ✨ added ability to view detailed information about friends * feat(mobile): ✨ add functionality to remove friends * fix(style): 🐛 fix special message not centered issue * fix(mobile): 🐛 fix hmr host bug * fix(mobile): 🐛 fix contact page not showing friend requests * feat(common): ✨ msg merge * feat(mobile): ✨ added chatroom redirection feature on friend detail page * fix(common): 🐛 merge msg display * feat(mobile): ✨ add file and image uploader components * fix(common): 🐛 send new msg error when scroll too top * fix(common): 🐛 reconstruct the notification module * fix(system): 🐛 fix some style and logic issues --------- Co-authored-by: wanwanruwoxin <254693270@qq.com> Co-authored-by: 卡仔 <1271013637@qq.com> Co-authored-by: 乾乾 <1046762075@qq.com>
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
# 后端服务地址
|
||||
VITE_SERVICE_URL="https://hulaspark.com/api"
|
||||
# # # websocket服务地址
|
||||
VITE_WEBSOCKET_URL="wss://hulaspark.com/api/ws/ws"
|
||||
# 项目标题
|
||||
VITE_APP_TITLE="HuLa—IM"
|
||||
# 项目名称
|
||||
VITE_APP_NAME="HuLa"
|
||||
# gitee token
|
||||
VITE_GITEE_TOKEN="a9029798336825cea39ac9e4413b8579"
|
||||
|
||||
|
||||
VITE_USE_RUST_WEBSOCKET=true
|
||||
|
||||
TAURI_DEV_HOST=192.168.1.26
|
||||
@@ -1,14 +0,0 @@
|
||||
# 后端服务地址
|
||||
VITE_SERVICE_URL="https://hulaspark.com/api"
|
||||
# # # websocket服务地址
|
||||
VITE_WEBSOCKET_URL="wss://hulaspark.com/api/ws/ws"
|
||||
# 项目标题
|
||||
VITE_APP_TITLE="HuLa—IM"
|
||||
# 项目名称
|
||||
VITE_APP_NAME="HuLa"
|
||||
# gitee token
|
||||
VITE_GITEE_TOKEN="a9029798336825cea39ac9e4413b8579"
|
||||
|
||||
VITE_USE_RUST_WEBSOCKET=true
|
||||
|
||||
TAURI_DEV_HOST=192.168.1.26
|
||||
Vendored
+8
-3
@@ -25,6 +25,10 @@
|
||||
"tauri:dev": "tauri dev",
|
||||
"========= 启动HuLa桌面应用程序(简化命令) =========": "",
|
||||
"td": "tauri dev",
|
||||
"========= windows中启动安卓纯前端(简化命令) =========": "",
|
||||
"adev:win": "set TAURI_ENV_PLATFORM=android&& vite",
|
||||
"========= mac中启动ios纯前端(简化命令) =========": "",
|
||||
"idev:mac": "TAURI_ENV_PLATFORM=ios vite",
|
||||
"========= 启动安卓程序(简化命令) =========": "",
|
||||
"adev": "tauri android dev",
|
||||
"========= 启动ios程序(简化命令) =========": "",
|
||||
@@ -107,6 +111,7 @@
|
||||
"file-type": "^21.0.0",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"hula-emojis": "^1.2.26",
|
||||
"internal-ip": "^8.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"naive-ui": "^2.43.1",
|
||||
@@ -147,7 +152,7 @@
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vueuse/core": "^13.7.0",
|
||||
"chalk": "^5.6.0",
|
||||
"chalk": "^5.6.2",
|
||||
"commitizen": "^4.3.1",
|
||||
"cz-git": "^1.12.0",
|
||||
"happy-dom": "^15.11.7",
|
||||
@@ -156,11 +161,11 @@
|
||||
"postcss-pxtorem": "^6.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"release-it": "^17.11.0",
|
||||
"sass": "1.90.0",
|
||||
"sass": "1.92.1",
|
||||
"typescript": "^5.9.2",
|
||||
"unplugin-auto-import": "^20.1.0",
|
||||
"unplugin-vue-components": "^29.0.0",
|
||||
"vite": "7.1.5",
|
||||
"vite": "7.1.6",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-tsc": "^3.0.6"
|
||||
|
||||
+394
-247
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
-4
@@ -31,10 +31,6 @@ async function main() {
|
||||
|
||||
/** @type {CheckItem[]} */
|
||||
const checks = [
|
||||
{
|
||||
script: join(__dirname, 'check-env.js'),
|
||||
description: '配置文件检查'
|
||||
},
|
||||
{
|
||||
script: join(__dirname, 'check-dependencies.js'),
|
||||
description: '环境检查'
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
RUST_BACKTRACE=1
|
||||
# APP_ENVIRONMENT=local
|
||||
APP_ENVIRONMENT=production
|
||||
@@ -1,4 +1,5 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
local.yaml
|
||||
|
||||
|
||||
Vendored
+1
@@ -2,3 +2,4 @@ database:
|
||||
sqlite_file: db.sqlite
|
||||
backend:
|
||||
base_url: https://hulaspark.com/api
|
||||
active_config: local
|
||||
|
||||
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
database:
|
||||
sqlite_file: db.sqlite
|
||||
backend:
|
||||
base_url: http://192.168.1.37:18760
|
||||
+8
@@ -2,3 +2,11 @@ database:
|
||||
sqlite_file: db.sqlite
|
||||
backend:
|
||||
base_url: https://hulaspark.com/api
|
||||
ws_url: wss://hulaspark.com/api/ws/ws
|
||||
|
||||
youdao:
|
||||
app_key: ''
|
||||
app_secret: ''
|
||||
tencent:
|
||||
api_key: ''
|
||||
secret_id: ''
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct Model {
|
||||
pub login_uid: String,
|
||||
/// 消息发送状态: pending, success, fail
|
||||
pub send_status: String,
|
||||
pub time_block: Option<i64>,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
Vendored
+2
-1
@@ -3,4 +3,5 @@
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
/tauri.properties
|
||||
/src/main/assets/configuration
|
||||
+19
-35
@@ -1,41 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.hula"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="true"
|
||||
android:windowSoftInputMode="adjustNothing"
|
||||
android:theme="@style/Theme.hula">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.hula" android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" android:launchMode="singleTask" android:label="@string/main_activity_title" android:name=".MainActivity" android:exported="true" android:resizeableActivity="true" android:windowSoftInputMode="adjustNothing" android:theme="@style/Theme.hula">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
database:
|
||||
sqlite_file: db.sqlite
|
||||
backend:
|
||||
base_url: https://hulaspark.com/api
|
||||
@@ -1,4 +0,0 @@
|
||||
database:
|
||||
sqlite_file: db.sqlite
|
||||
backend:
|
||||
base_url: http://192.168.1.37:18760
|
||||
@@ -1,4 +0,0 @@
|
||||
database:
|
||||
sqlite_file: db.sqlite
|
||||
backend:
|
||||
base_url: https://hulaspark.com/api
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -3,6 +3,7 @@ pub use sea_orm_migration::prelude::*;
|
||||
mod m20220101_000001_create_table;
|
||||
mod m20241220_000002_add_token_field;
|
||||
mod m20241220_000003_add_refresh_token_field;
|
||||
mod m20250917_000001_update_msg_table;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -13,6 +14,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20220101_000001_create_table::Migration),
|
||||
Box::new(m20241220_000002_add_token_field::Migration),
|
||||
Box::new(m20241220_000003_add_refresh_token_field::Migration),
|
||||
Box::new(m20250917_000001_update_msg_table::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager.alter_table(
|
||||
Table::alter()
|
||||
.table(ImMessage::Table)
|
||||
.add_column(ColumnDef::new(ImMessage::TimeBlock).big_integer())
|
||||
.to_owned()
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager.alter_table(
|
||||
Table::alter()
|
||||
.table(ImMessage::Table)
|
||||
.drop_column(ImMessage::TimeBlock)
|
||||
.to_owned()
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum ImMessage {
|
||||
Table,
|
||||
TimeBlock,
|
||||
}
|
||||
@@ -84,7 +84,7 @@ pub async fn query_chat_history(
|
||||
// 转换为响应格式
|
||||
let message_resps: Vec<MessageResp> = messages
|
||||
.into_iter()
|
||||
.map(|msg| crate::command::message_command::convert_message_to_resp(msg))
|
||||
.map(|msg| crate::command::message_command::convert_message_to_resp(msg, None))
|
||||
.collect();
|
||||
|
||||
// 根据返回的消息数量判断是否还有更多数据
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct MessageResp {
|
||||
pub from_user: FromUser,
|
||||
pub message: Message,
|
||||
pub old_msg_id: Option<String>,
|
||||
pub time_block: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
@@ -95,6 +96,7 @@ pub async fn page_msg(
|
||||
param: CursorPageMessageParam,
|
||||
state: State<'_, AppData>,
|
||||
) -> Result<CursorPageResp<Vec<MessageResp>>, String> {
|
||||
info!("get page msg");
|
||||
// 获取当前登录用户的 uid
|
||||
let login_uid = {
|
||||
let user_info = state.user_info.lock().await;
|
||||
@@ -116,7 +118,7 @@ pub async fn page_msg(
|
||||
.list
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|msg| convert_message_to_resp(msg))
|
||||
.map(|msg| convert_message_to_resp(msg, None))
|
||||
.rev()
|
||||
.collect();
|
||||
|
||||
@@ -129,7 +131,7 @@ pub async fn page_msg(
|
||||
}
|
||||
|
||||
/// 将数据库消息模型转换为响应模型
|
||||
pub fn convert_message_to_resp(msg: im_message::Model) -> MessageResp {
|
||||
pub fn convert_message_to_resp(msg: im_message::Model, old_msg_id: Option<String>) -> MessageResp {
|
||||
// 解析消息体 - 安全地处理 JSON 解析
|
||||
let body = msg.body.as_ref().and_then(|body_str| {
|
||||
if body_str.trim().is_empty() {
|
||||
@@ -193,7 +195,8 @@ pub fn convert_message_to_resp(msg: im_message::Model) -> MessageResp {
|
||||
message_marks,
|
||||
send_time: msg.send_time,
|
||||
},
|
||||
old_msg_id: None,
|
||||
old_msg_id: old_msg_id,
|
||||
time_block: msg.time_block,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +257,7 @@ pub async fn fetch_all_messages(
|
||||
uid, last_opt_time
|
||||
);
|
||||
// 调用后端接口 /chat/msg/list 获取所有消息,传递 last_opt_time 参数
|
||||
let params = if let Some(time) = last_opt_time {
|
||||
let body = if let Some(time) = last_opt_time {
|
||||
Some(serde_json::json!({
|
||||
"lastOptTime": time
|
||||
}))
|
||||
@@ -263,7 +266,7 @@ pub async fn fetch_all_messages(
|
||||
};
|
||||
|
||||
let messages: Option<Vec<MessageResp>> = client
|
||||
.im_request(ImUrl::GetMsgList, None::<serde_json::Value>, params)
|
||||
.im_request(ImUrl::GetMsgList, body, None::<serde_json::Value>)
|
||||
.await?;
|
||||
|
||||
if let Some(messages) = messages {
|
||||
@@ -332,6 +335,7 @@ fn convert_resp_to_model_for_fetch(msg_resp: MessageResp, uid: String) -> im_mes
|
||||
update_time: msg_resp.update_time,
|
||||
login_uid: uid.to_string(), // 这里暂时设为空字符串,实际使用时会在 save_all 中设置
|
||||
send_status: "success".to_string(), // 从后端获取的消息默认为成功状态
|
||||
time_block: msg_resp.time_block,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +380,7 @@ pub async fn send_msg(
|
||||
update_time: Some(current_time),
|
||||
login_uid: login_uid.clone(),
|
||||
send_status: "pending".to_string(), // 初始状态为pending
|
||||
time_block: None,
|
||||
};
|
||||
|
||||
let tx = state
|
||||
@@ -418,28 +423,30 @@ pub async fn send_msg(
|
||||
Ok(Some(mut resp)) => {
|
||||
resp.old_msg_id = Some(msg_id.clone());
|
||||
id = resp.message.id.clone();
|
||||
// 尝试克隆数据以避免所有权问题
|
||||
let event_data = resp.clone();
|
||||
success_channel.send(event_data).unwrap();
|
||||
"success"
|
||||
}
|
||||
_ => {
|
||||
error_channel.send(msg_id.clone()).unwrap();
|
||||
"fail"
|
||||
}
|
||||
_ => "fail",
|
||||
};
|
||||
|
||||
// 更新消息状态
|
||||
if let Err(e) = im_message_repository::update_message_status(
|
||||
let model = im_message_repository::update_message_status(
|
||||
db_conn.deref(),
|
||||
&msg_id,
|
||||
message,
|
||||
status,
|
||||
id,
|
||||
login_uid.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to update message status: {}", e);
|
||||
.await;
|
||||
|
||||
match model {
|
||||
Ok(model) => {
|
||||
let resp = convert_message_to_resp(model, Some(msg_id));
|
||||
success_channel.send(resp).unwrap();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
error_channel.send(msg_id.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -448,6 +455,7 @@ pub async fn send_msg(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_msg(data: MessageResp, state: State<'_, AppData>) -> Result<(), String> {
|
||||
info!("save msg: {:?}", serde_json::to_string(&data));
|
||||
// 创建 im_message::Model
|
||||
let message = convert_resp_to_model_for_fetch(data, state.user_info.lock().await.uid.clone());
|
||||
|
||||
|
||||
@@ -5,3 +5,4 @@ pub mod message_mark_command;
|
||||
pub mod request_command;
|
||||
pub mod room_member_command;
|
||||
pub mod user_command;
|
||||
pub mod setting_command;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{configuration::Settings, AppData};
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_settings(state: State<'_, AppData>,) -> Result<Settings, String> {
|
||||
Ok(state.config.lock().await.clone())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateSettingsParams {
|
||||
base_url: String,
|
||||
ws_url: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_settings(state: State<'_, AppData>, settings: UpdateSettingsParams) -> Result<(), String> {
|
||||
let mut config = state.config.lock().await;
|
||||
config.backend.base_url = settings.base_url.clone();
|
||||
config.backend.ws_url = settings.ws_url;
|
||||
info!("update settings: {:?}", config);
|
||||
state.rc.lock().await.set_base_url(settings.base_url.clone());
|
||||
Ok(())
|
||||
}
|
||||
+121
-197
@@ -6,22 +6,37 @@ use tauri::{AppHandle, Manager};
|
||||
use tracing::info;
|
||||
|
||||
// 应用程序设置结构体
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Settings {
|
||||
pub database: DatabaseSettings,
|
||||
pub backend: BackendSettings,
|
||||
pub youdao: Youdao,
|
||||
}
|
||||
|
||||
// 数据库配置设置
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
|
||||
pub struct DatabaseSettings {
|
||||
pub sqlite_file: String,
|
||||
}
|
||||
|
||||
// 后端服务配置设置
|
||||
#[derive(serde::Deserialize, Clone, Debug)]
|
||||
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
|
||||
pub struct BackendSettings {
|
||||
pub base_url: String,
|
||||
pub ws_url: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
|
||||
pub struct Youdao {
|
||||
pub app_key: String,
|
||||
pub app_secret: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
|
||||
pub struct Tencent {
|
||||
pub api_key: String,
|
||||
pub secret_id: String,
|
||||
}
|
||||
|
||||
// 应用程序运行环境枚举
|
||||
@@ -31,13 +46,6 @@ pub enum Environment {
|
||||
Production,
|
||||
}
|
||||
|
||||
// 配置文件来源枚举
|
||||
enum ConfigSource {
|
||||
FileSystem(PathBuf, PathBuf),
|
||||
Resource(PathBuf, PathBuf),
|
||||
Embedded,
|
||||
}
|
||||
|
||||
impl DatabaseSettings {
|
||||
/// 创建数据库连接
|
||||
/// 根据不同的运行环境(桌面开发、移动端、桌面生产)选择合适的数据库路径
|
||||
@@ -150,204 +158,120 @@ impl TryFrom<String> for Environment {
|
||||
/// * `Ok(Settings)` - 成功时返回配置设置
|
||||
/// * `Err(config::ConfigError)` - 失败时返回配置错误
|
||||
pub fn get_configuration(app_handle: &AppHandle) -> Result<Settings, config::ConfigError> {
|
||||
let environment: Environment = std::env::var("APP_ENVIRONMENT")
|
||||
.unwrap_or_else(|_| "local".into())
|
||||
.try_into()
|
||||
.map_err(|e| {
|
||||
config::ConfigError::Message(format!("Failed to parse APP_ENVIRONMENT: {:?}", e))
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let is_desktop_dev = cfg!(debug_assertions) && cfg!(desktop);
|
||||
|
||||
let config_path_buf = get_config_path_buf(app_handle, is_desktop_dev)?;
|
||||
|
||||
let settings = config::Config::builder()
|
||||
.add_source(config::File::from(config_path_buf.0))
|
||||
.add_source(config::File::from(config_path_buf.1))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
settings.try_deserialize::<Settings>()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
// 读取 base.yaml 内容
|
||||
let base_content = std::str::from_utf8(include_bytes!("../configuration/base.yaml"))
|
||||
.map_err(|e| config::ConfigError::Message(e.to_string()))?;
|
||||
|
||||
// 构建 base 配置对象
|
||||
let base_config = config::Config::builder()
|
||||
.add_source(config::File::from_str(
|
||||
base_content,
|
||||
config::FileFormat::Yaml,
|
||||
))
|
||||
.build()?;
|
||||
|
||||
// 获取 active_config 字段
|
||||
let active_config = base_config.get_string("active_config").map_err(|_| {
|
||||
config::ConfigError::Message(
|
||||
"Missing or invalid 'active_config' in base.yaml".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
info!("APP_ENVIRONMENT: {}", environment.as_str());
|
||||
let environment_filename = format!("{}.yaml", environment.as_str());
|
||||
let is_desktop_dev = cfg!(debug_assertions) && cfg!(desktop);
|
||||
// 校验 active_config 合法性
|
||||
if active_config != "local" && active_config != "production" {
|
||||
return Err(config::ConfigError::Message(
|
||||
"Only \"local\" or \"production\" can be specified in active_config".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
match load_config_source(app_handle, &environment_filename, is_desktop_dev) {
|
||||
ConfigSource::FileSystem(base_path, env_path) => {
|
||||
tracing::info!("Using filesystem configuration files");
|
||||
build_config_from_files(base_path, env_path)
|
||||
}
|
||||
ConfigSource::Resource(base_path, env_path) => {
|
||||
tracing::info!("Using Resource directory configuration files");
|
||||
build_config_from_files(base_path, env_path)
|
||||
}
|
||||
ConfigSource::Embedded => {
|
||||
tracing::info!("Using embedded configuration");
|
||||
build_config_from_embedded(&environment)
|
||||
}
|
||||
// 加载对应的配置文件内容
|
||||
let config_file_bytes: &[u8] = match active_config.as_str() {
|
||||
"local" => include_bytes!("../configuration/local.yaml").as_ref(),
|
||||
"production" => include_bytes!("../configuration/production.yaml").as_ref(),
|
||||
_ => return Err(config::ConfigError::Message("Invalid active_config".into())),// 这里可以支持更多的环境配置
|
||||
};
|
||||
|
||||
let active_content = std::str::from_utf8(config_file_bytes)
|
||||
.map_err(|e| config::ConfigError::Message(e.to_string()))?;
|
||||
|
||||
// 构建最终配置对象
|
||||
config::Config::builder()
|
||||
.add_source(config::File::from_str(
|
||||
base_content,
|
||||
config::FileFormat::Yaml,
|
||||
))
|
||||
.add_source(config::File::from_str(
|
||||
active_content,
|
||||
config::FileFormat::Yaml,
|
||||
))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?
|
||||
.try_deserialize::<Settings>()
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载配置文件来源
|
||||
/// 根据运行环境和配置文件可用性确定配置来源
|
||||
/// 优先级:文件系统 > 资源目录 > 嵌入式配置
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app_handle` - Tauri应用句柄
|
||||
/// * `environment_filename` - 环境配置文件名
|
||||
/// * `is_desktop_dev` - 是否为桌面开发环境
|
||||
///
|
||||
/// # 返回值
|
||||
/// * `ConfigSource` - 配置文件来源枚举
|
||||
fn load_config_source(
|
||||
fn get_config_path_buf(
|
||||
app_handle: &AppHandle,
|
||||
environment_filename: &str,
|
||||
is_desktop_dev: bool,
|
||||
) -> ConfigSource {
|
||||
if is_desktop_dev {
|
||||
if let Ok(config_dir) = get_configuration_directory(app_handle) {
|
||||
let base_path = config_dir.join("base.yaml");
|
||||
let env_path = config_dir.join(environment_filename);
|
||||
return ConfigSource::FileSystem(base_path, env_path);
|
||||
}
|
||||
}
|
||||
) -> Result<(PathBuf, PathBuf), config::ConfigError> {
|
||||
let dir = if is_desktop_dev {
|
||||
let base_path = std::env::current_dir().map_err(|e| {
|
||||
config::ConfigError::Message(format!("Failed to get current dir: {}", e))
|
||||
})?;
|
||||
|
||||
if let Ok(config_dir) = app_handle
|
||||
.path()
|
||||
.resolve("configuration", tauri::path::BaseDirectory::Resource)
|
||||
{
|
||||
let base_path = config_dir.join("base.yaml");
|
||||
let env_path = config_dir.join(environment_filename);
|
||||
|
||||
if base_path.exists() && env_path.exists() {
|
||||
return ConfigSource::Resource(base_path, env_path);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Resource directory configuration files do not exist, falling back to embedded config"
|
||||
);
|
||||
}
|
||||
base_path.join("configuration")
|
||||
} else {
|
||||
tracing::warn!("Failed to get Resource directory, falling back to embedded config");
|
||||
}
|
||||
|
||||
ConfigSource::Embedded
|
||||
}
|
||||
|
||||
/// 从配置文件构建配置对象
|
||||
/// 加载基础配置文件和环境特定配置文件,并合并环境变量配置
|
||||
///
|
||||
/// # 参数
|
||||
/// * `base_path` - 基础配置文件路径
|
||||
/// * `env_path` - 环境配置文件路径
|
||||
///
|
||||
/// # 返回值
|
||||
/// * `Ok(Settings)` - 成功时返回配置设置
|
||||
/// * `Err(config::ConfigError)` - 失败时返回配置错误
|
||||
fn build_config_from_files(
|
||||
base_path: PathBuf,
|
||||
env_path: PathBuf,
|
||||
) -> Result<Settings, config::ConfigError> {
|
||||
let settings = config::Config::builder()
|
||||
.add_source(config::File::from(base_path))
|
||||
.add_source(config::File::from(env_path))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?;
|
||||
settings.try_deserialize::<Settings>()
|
||||
}
|
||||
|
||||
/// 从嵌入式配置构建配置对象
|
||||
/// 使用编译时嵌入的配置文件内容构建配置
|
||||
///
|
||||
/// # 参数
|
||||
/// * `environment` - 运行环境枚举
|
||||
///
|
||||
/// # 返回值
|
||||
/// * `Ok(Settings)` - 成功时返回配置设置
|
||||
/// * `Err(config::ConfigError)` - 失败时返回配置错误
|
||||
fn build_config_from_embedded(environment: &Environment) -> Result<Settings, config::ConfigError> {
|
||||
let base_config = include_str!("../configuration/base.yaml");
|
||||
let env_config = match environment.as_str() {
|
||||
"local" => include_str!("../configuration/local.yaml"),
|
||||
"production" => include_str!("../configuration/production.yaml"),
|
||||
_ => {
|
||||
return Err(config::ConfigError::Message(format!(
|
||||
"Unsupported environment: {}",
|
||||
environment.as_str()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let settings = config::Config::builder()
|
||||
.add_source(config::File::from_str(
|
||||
base_config,
|
||||
config::FileFormat::Yaml,
|
||||
))
|
||||
.add_source(config::File::from_str(env_config, config::FileFormat::Yaml))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("APP")
|
||||
.prefix_separator("_")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?;
|
||||
settings.try_deserialize::<Settings>()
|
||||
}
|
||||
|
||||
/// 获取配置文件目录路径
|
||||
/// 根据运行环境选择合适的配置文件目录:
|
||||
/// - 桌面开发环境:项目根目录/configuration
|
||||
/// - 其他环境:优先使用Resource目录,失败则使用app_data_dir
|
||||
///
|
||||
/// # 参数
|
||||
/// * `app_handle` - Tauri应用句柄
|
||||
///
|
||||
/// # 返回值
|
||||
/// * `Ok(PathBuf)` - 成功时返回配置目录路径
|
||||
/// * `Err(config::ConfigError)` - 失败时返回配置错误
|
||||
fn get_configuration_directory(app_handle: &AppHandle) -> Result<PathBuf, config::ConfigError> {
|
||||
// 配置目录路径策略:
|
||||
// 桌面端开发环境:使用项目根目录
|
||||
// 其他情况:使用 Resource 目录
|
||||
let config_path = if cfg!(debug_assertions) && cfg!(desktop) {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("configuration");
|
||||
path
|
||||
} else {
|
||||
// 非桌面端开发环境:优先使用 Resource 目录,失败则使用 app_data_dir
|
||||
match app_handle
|
||||
app_handle
|
||||
.path()
|
||||
.resolve("configuration", tauri::path::BaseDirectory::Resource)
|
||||
{
|
||||
Ok(path) => {
|
||||
info!("Using Resource configuration directory: {:?}", path);
|
||||
path
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to get Resource configuration directory: {}, trying app_data_dir",
|
||||
e
|
||||
);
|
||||
|
||||
match app_handle.path().app_data_dir() {
|
||||
Ok(app_data_dir) => {
|
||||
let config_dir = app_data_dir.join("configuration");
|
||||
if let Err(create_err) = std::fs::create_dir_all(&config_dir) {
|
||||
tracing::warn!(
|
||||
"Failed to create app_data_dir configuration directory: {}",
|
||||
create_err
|
||||
);
|
||||
}
|
||||
info!(
|
||||
"Using backup app_data_dir configuration directory: {:?}",
|
||||
config_dir
|
||||
);
|
||||
config_dir
|
||||
}
|
||||
Err(app_data_err) => {
|
||||
let error_msg = format!(
|
||||
"Failed to get both Resource and app_data_dir configuration directories: Resource error={}, app_data_dir error={}",
|
||||
e, app_data_err
|
||||
);
|
||||
tracing::error!("{}", error_msg);
|
||||
return Err(config::ConfigError::Message(error_msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.resource_dir()
|
||||
.map_err(|e| config::ConfigError::NotFound(format!("resource not find: {}", e)))?
|
||||
.join("configuration")
|
||||
};
|
||||
|
||||
info!("Configuration file directory: {:?}", config_path);
|
||||
Ok(config_path)
|
||||
let base_path = dir.join("base.yaml");
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let base_config = config::Config::builder()
|
||||
.add_source(config::File::from(base_path.clone()))
|
||||
.build()?;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
let base_config = {
|
||||
let content = std::str::from_utf8(include_bytes!("../configuration/base.yaml"))
|
||||
.map_err(|e| config::ConfigError::Message(e.to_string()))?;
|
||||
|
||||
config::Config::builder()
|
||||
.add_source(config::File::from_str(content, config::FileFormat::Yaml))
|
||||
.build()?
|
||||
};
|
||||
|
||||
let active_config = base_config.get_string("active_config")?;
|
||||
println!("active_config: {:?}", active_config);
|
||||
let active_config_path_buf = dir.clone().join(active_config);
|
||||
Ok((base_path, active_config_path_buf))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tauri::path::BaseDirectory;
|
||||
use tauri::{AppHandle, Emitter, LogicalSize, Manager, ResourceId, Runtime, Webview};
|
||||
use tauri::{AppHandle, LogicalSize, Manager, ResourceId, Runtime, Webview};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2::rc::Retained;
|
||||
@@ -32,12 +32,6 @@ pub struct WindowsScaleInfo {
|
||||
/// 是否检测到文本缩放
|
||||
pub has_text_scaling: bool,
|
||||
}
|
||||
|
||||
use crate::desktops::window_payload::{
|
||||
WindowPayload, get_window_payload as _get_window_payload,
|
||||
push_window_payload as _push_window_payload,
|
||||
};
|
||||
|
||||
// 定义用户信息结构体
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct UserInfo {
|
||||
@@ -246,34 +240,6 @@ pub fn show_title_bar_buttons(window_label: &str, handle: AppHandle) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn push_window_payload(
|
||||
label: String,
|
||||
payload: serde_json::Value,
|
||||
handle: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
let payload_entity = WindowPayload::new(payload);
|
||||
if let Some(window) = handle.get_webview_window(&label) {
|
||||
window
|
||||
// 找到了对应label的window说明已经打开了,就只需要提醒刷新就行了
|
||||
.emit(&format!("{}:update", label), payload_entity)
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
_push_window_payload(label, payload_entity)
|
||||
.await
|
||||
// 这里是存在值的时候才算失败,不存在值则是插入成功
|
||||
.map_or_else(|| Ok(()), |_| Err("none".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_window_payload(label: String) -> Result<serde_json::Value, ()> {
|
||||
_get_window_payload(label)
|
||||
.await
|
||||
.map(|payload_entity| payload_entity.payload)
|
||||
.ok_or(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_files_meta(files_path: Vec<String>) -> Result<Vec<FileMeta>, String> {
|
||||
let mut files_meta: Vec<FileMeta> = Vec::new();
|
||||
|
||||
@@ -9,22 +9,18 @@ pub struct WindowPayload {
|
||||
pub payload: serde_json::Value,
|
||||
}
|
||||
|
||||
impl WindowPayload {
|
||||
pub fn new(payload: serde_json::Value) -> Self {
|
||||
WindowPayload { payload }
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref PAYLOAD_CACHE: Mutex<HashMap<String, WindowPayload>> = Mutex::new(HashMap::new());
|
||||
static ref PAYLOAD_CACHE: Mutex<HashMap<String, serde_json::Value>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub async fn push_window_payload(label: String, payload: WindowPayload) -> Option<WindowPayload> {
|
||||
#[tauri::command]
|
||||
pub async fn push_window_payload(label: String, payload: serde_json::Value) -> Option<serde_json::Value> {
|
||||
let mut payload_cache = PAYLOAD_CACHE.lock().await;
|
||||
payload_cache.insert(label, payload)
|
||||
}
|
||||
|
||||
pub async fn get_window_payload(label: String) -> Option<WindowPayload> {
|
||||
#[tauri::command]
|
||||
pub async fn get_window_payload(label: String) -> Option<serde_json::Value> {
|
||||
let mut payload_cache = PAYLOAD_CACHE.lock().await;
|
||||
payload_cache.remove(&label)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ impl ImRequestClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_base_url(&mut self, base_url: String) {
|
||||
self.base_url = base_url;
|
||||
}
|
||||
|
||||
pub async fn request<
|
||||
T: serde::de::DeserializeOwned,
|
||||
B: serde::Serialize,
|
||||
@@ -102,7 +106,11 @@ impl ImRequestClient {
|
||||
return Err(anyhow::anyhow!("请重新登录"));
|
||||
}
|
||||
Some(200) => {
|
||||
info!("✅ Request successful: {}, Method: {}", &url, method.clone());
|
||||
info!(
|
||||
"✅ Request successful: {}, Method: {}",
|
||||
&url,
|
||||
method.clone()
|
||||
);
|
||||
return Ok(result);
|
||||
}
|
||||
_ => {
|
||||
@@ -260,6 +268,7 @@ pub enum ImUrl {
|
||||
HandleInvite,
|
||||
ApplyUnReadCount,
|
||||
RequestApplyPage,
|
||||
RequestNoticePage,
|
||||
GetContactList,
|
||||
SearchFriend,
|
||||
ChangeUserState,
|
||||
@@ -285,6 +294,8 @@ pub enum ImUrl {
|
||||
GetFriendPage,
|
||||
MarkMsgRead,
|
||||
CheckEmail,
|
||||
MergeMsg,
|
||||
GetUserByIds,
|
||||
}
|
||||
|
||||
impl ImUrl {
|
||||
@@ -360,6 +371,7 @@ impl ImUrl {
|
||||
ImUrl::HandleInvite => (http::Method::POST, "im/room/apply/handler/apply"),
|
||||
ImUrl::ApplyUnReadCount => (http::Method::GET, "im/room/apply/unread"),
|
||||
ImUrl::RequestApplyPage => (http::Method::GET, "im/room/apply/page"),
|
||||
ImUrl::RequestNoticePage => (http::Method::GET, "im/room/notice/page"),
|
||||
ImUrl::GetFriendPage => (http::Method::GET, "im/user/friend/page"),
|
||||
ImUrl::GetContactList => (http::Method::GET, "im/chat/contact/list"),
|
||||
ImUrl::SearchFriend => (http::Method::GET, "im/user/friend/search"),
|
||||
@@ -384,12 +396,15 @@ impl ImUrl {
|
||||
ImUrl::RecallMsg => (http::Method::PUT, "im/chat/msg/recall"),
|
||||
ImUrl::MarkMsg => (http::Method::PUT, "im/chat/msg/mark"),
|
||||
ImUrl::GetMsgPage => (http::Method::GET, "im/chat/msg/page"),
|
||||
ImUrl::GetMsgList => (http::Method::GET, "im/chat/msg/list"),
|
||||
ImUrl::GetMsgList => (http::Method::POST, "im/chat/msg/list"),
|
||||
ImUrl::GetMemberStatistic => (http::Method::GET, "im/chat/member/statistic"),
|
||||
|
||||
// 群成员信息
|
||||
ImUrl::GetAllUserBaseInfo => (http::Method::GET, "im/room/group/member/list"),
|
||||
ImUrl::CheckEmail => (http::Method::GET, "oauth/anyTenant/checkEmail"),
|
||||
|
||||
ImUrl::MergeMsg => (http::Method::POST, "im/room/mergeMessage"),
|
||||
ImUrl::GetUserByIds => (http::Method::POST, "im/user/getUserByIds"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,6 +476,7 @@ impl ImUrl {
|
||||
"handleInvite" => Ok(ImUrl::HandleInvite),
|
||||
"applyUnReadCount" => Ok(ImUrl::ApplyUnReadCount),
|
||||
"requestApplyPage" => Ok(ImUrl::RequestApplyPage),
|
||||
"requestNoticePage" => Ok(ImUrl::RequestNoticePage),
|
||||
"getContactList" => Ok(ImUrl::GetContactList),
|
||||
"searchFriend" => Ok(ImUrl::SearchFriend),
|
||||
|
||||
@@ -495,6 +511,8 @@ impl ImUrl {
|
||||
"markMsgRead" => Ok(ImUrl::MarkMsgRead),
|
||||
"groupListMember" => Ok(ImUrl::GroupListMember),
|
||||
"checkEmail" => Ok(ImUrl::CheckEmail),
|
||||
"mergeMsg" => Ok(ImUrl::MergeMsg),
|
||||
"getUserByIds" => Ok(ImUrl::GetUserByIds),
|
||||
|
||||
// 未匹配的字符串
|
||||
_ => Err(anyhow::anyhow!("未知的URL类型: {}", s)),
|
||||
|
||||
+26
-13
@@ -6,21 +6,21 @@ use common::init::CustomInit;
|
||||
#[cfg(target_os = "windows")]
|
||||
use common_cmd::get_windows_scale_info;
|
||||
#[cfg(desktop)]
|
||||
use common_cmd::{
|
||||
audio, default_window_icon, get_files_meta, get_window_payload, push_window_payload,
|
||||
screenshot, set_height,
|
||||
};
|
||||
use common_cmd::{audio, default_window_icon, get_files_meta, screenshot, set_height};
|
||||
#[cfg(target_os = "macos")]
|
||||
use common_cmd::{hide_title_bar_buttons, set_window_level_above_menubar, show_title_bar_buttons};
|
||||
#[cfg(target_os = "macos")]
|
||||
use desktops::app_event;
|
||||
#[cfg(desktop)]
|
||||
use desktops::window_payload::{get_window_payload, push_window_payload};
|
||||
#[cfg(desktop)]
|
||||
use desktops::{common_cmd, directory_scanner, init, tray, video_thumbnail::get_video_thumbnail};
|
||||
#[cfg(desktop)]
|
||||
use directory_scanner::{cancel_directory_scan, get_directory_usage_info_with_progress};
|
||||
#[cfg(desktop)]
|
||||
use init::DesktopCustomInit;
|
||||
use std::sync::Arc;
|
||||
use tauri_plugin_fs::FsExt;
|
||||
pub mod command;
|
||||
pub mod common;
|
||||
pub mod configuration;
|
||||
@@ -37,8 +37,9 @@ use crate::command::request_command::{im_request_command, login_command};
|
||||
use crate::command::room_member_command::{
|
||||
cursor_page_room_members, get_room_members, page_room, update_my_room_info,
|
||||
};
|
||||
use crate::command::setting_command::{get_settings, update_settings};
|
||||
use crate::command::user_command::remove_tokens;
|
||||
use crate::configuration::get_configuration;
|
||||
use crate::configuration::{Settings, get_configuration};
|
||||
use crate::error::CommonError;
|
||||
use crate::repository::im_user_repository;
|
||||
use sea_orm::DatabaseConnection;
|
||||
@@ -55,6 +56,7 @@ pub struct AppData {
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
user_info: Arc<Mutex<UserInfo>>,
|
||||
pub rc: Arc<Mutex<im_request_client::ImRequestClient>>,
|
||||
pub config: Arc<Mutex<Settings>>,
|
||||
}
|
||||
|
||||
use crate::command::chat_history_command::query_chat_history;
|
||||
@@ -118,6 +120,7 @@ async fn initialize_app_data(
|
||||
Arc<DatabaseConnection>,
|
||||
Arc<Mutex<UserInfo>>,
|
||||
Arc<Mutex<im_request_client::ImRequestClient>>,
|
||||
Arc<Mutex<Settings>>,
|
||||
),
|
||||
CommonError,
|
||||
> {
|
||||
@@ -125,14 +128,16 @@ async fn initialize_app_data(
|
||||
use tracing::info;
|
||||
|
||||
// 加载配置
|
||||
let configuration = Arc::new(
|
||||
get_configuration(&app_handle)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to load configuration: {}", e))?,
|
||||
);
|
||||
let configuration =
|
||||
Arc::new(Mutex::new(get_configuration(&app_handle).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to load configuration: {}", e)
|
||||
})?));
|
||||
|
||||
// 初始化数据库连接
|
||||
let db: Arc<DatabaseConnection> = Arc::new(
|
||||
configuration
|
||||
.lock()
|
||||
.await
|
||||
.database
|
||||
.connection_string(&app_handle)
|
||||
.await?,
|
||||
@@ -148,8 +153,10 @@ async fn initialize_app_data(
|
||||
}
|
||||
}
|
||||
|
||||
let rc: im_request_client::ImRequestClient =
|
||||
im_request_client::ImRequestClient::new(configuration.backend.base_url.clone()).unwrap();
|
||||
let rc: im_request_client::ImRequestClient = im_request_client::ImRequestClient::new(
|
||||
configuration.lock().await.backend.base_url.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// 创建用户信息
|
||||
let user_info = UserInfo {
|
||||
@@ -159,7 +166,7 @@ async fn initialize_app_data(
|
||||
};
|
||||
let user_info = Arc::new(Mutex::new(user_info));
|
||||
|
||||
Ok((db, user_info, Arc::new(Mutex::new(rc))))
|
||||
Ok((db, user_info, Arc::new(Mutex::new(rc)), configuration))
|
||||
}
|
||||
|
||||
// 设置用户信息监听器
|
||||
@@ -352,6 +359,9 @@ fn setup_mobile() {
|
||||
|
||||
// 公共的 setup 函数
|
||||
fn common_setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let scope = app.fs_scope();
|
||||
scope.allow_directory("configuration", false).unwrap();
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
setup_user_info_listener_early(app.handle().clone());
|
||||
#[cfg(desktop)]
|
||||
@@ -359,12 +369,13 @@ fn common_setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>>
|
||||
|
||||
// 异步初始化应用数据,避免阻塞主线程
|
||||
match tauri::async_runtime::block_on(initialize_app_data(app_handle.clone())) {
|
||||
Ok((db, user_info, rc)) => {
|
||||
Ok((db, user_info, rc, settings)) => {
|
||||
// 使用 manage 方法在运行时添加状态
|
||||
app_handle.manage(AppData {
|
||||
db_conn: db.clone(),
|
||||
user_info: user_info.clone(),
|
||||
rc: rc,
|
||||
config: settings,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -455,5 +466,7 @@ fn get_invoke_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Se
|
||||
ws_get_app_background_state,
|
||||
login_command,
|
||||
im_request_command,
|
||||
get_settings,
|
||||
update_settings,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use sea_orm::prelude::Expr;
|
||||
use sea_orm::sea_query::Alias;
|
||||
use sea_orm::{
|
||||
ColumnTrait, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, EntityTrait,
|
||||
IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set,
|
||||
IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set, TryIntoModel,
|
||||
};
|
||||
|
||||
use tracing::{debug, error, info};
|
||||
@@ -237,19 +237,45 @@ pub async fn save_message(
|
||||
/// 更新消息发送状态
|
||||
pub async fn update_message_status(
|
||||
db: &DatabaseConnection,
|
||||
message_id: &str,
|
||||
message: im_message::Model,
|
||||
status: &str,
|
||||
id: Option<String>,
|
||||
login_uid: String,
|
||||
) -> Result<(), CommonError> {
|
||||
) -> Result<im_message::Model, CommonError> {
|
||||
let mut active_model: im_message::ActiveModel =
|
||||
im_message::Entity::find_by_id((message_id.to_string(), login_uid))
|
||||
im_message::Entity::find_by_id((message.id.clone(), login_uid))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to find message: {}", e))?
|
||||
.ok_or_else(|| CommonError::UnexpectedError(anyhow::anyhow!("Message not found")))?
|
||||
.into_active_model();
|
||||
|
||||
// 设置 time_block
|
||||
if message.id.starts_with('T') {
|
||||
info!("check msg sendTime");
|
||||
// 查找该房间的最后一条消息
|
||||
let last_message = im_message::Entity::find()
|
||||
.filter(im_message::Column::RoomId.eq(&message.room_id))
|
||||
.filter(im_message::Column::LoginUid.eq(message.login_uid.clone()))
|
||||
.filter(im_message::Column::Id.ne(&message.id))
|
||||
.order_by_desc(im_message::Column::Id)
|
||||
.limit(1)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to find last message: {}", e))?;
|
||||
|
||||
if let Some(last_message) = last_message {
|
||||
// 比较时间戳
|
||||
if let (Some(last_send_time), Some(send_time)) =
|
||||
(last_message.send_time, message.send_time)
|
||||
{
|
||||
if send_time - last_send_time >= 1000 * 60 * 15 {
|
||||
active_model.time_block = Set(Some(send_time - last_send_time));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
active_model.send_status = Set(status.to_string());
|
||||
|
||||
if status == "success" {
|
||||
@@ -263,11 +289,11 @@ pub async fn update_message_status(
|
||||
}
|
||||
|
||||
im_message::Entity::update_many()
|
||||
.set(active_model)
|
||||
.filter(im_message::Column::Id.eq(message_id))
|
||||
.set(active_model.clone())
|
||||
.filter(im_message::Column::Id.eq(message.id))
|
||||
.exec(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
Ok(active_model.try_into_model()?)
|
||||
}
|
||||
|
||||
/// 更新消息撤回状态
|
||||
|
||||
@@ -22,8 +22,6 @@ fn get_websocket_client_container() -> &'static Arc<RwLock<Option<WebSocketClien
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InitWsParams {
|
||||
pub server_url: String,
|
||||
pub token: Option<String>,
|
||||
pub client_id: String,
|
||||
}
|
||||
|
||||
@@ -72,7 +70,7 @@ pub async fn ws_init_connection(
|
||||
let rc = state.rc.lock().await;
|
||||
|
||||
let config = WebSocketConfig {
|
||||
server_url: params.server_url,
|
||||
server_url: state.config.lock().await.backend.ws_url.clone(),
|
||||
client_id: params.client_id,
|
||||
token: rc.token.clone(),
|
||||
..Default::default()
|
||||
|
||||
@@ -105,7 +105,7 @@ const handleItemMouseEnter = (index: number) => {
|
||||
|
||||
// 获取当前悬停项的DOM元素
|
||||
const item =
|
||||
document.activeElement?.closest('.float-block') || (event?.target as HTMLElement)?.closest('.float-block')
|
||||
(event?.target as HTMLElement)?.closest('.float-block') || document.activeElement?.closest('.float-block')
|
||||
|
||||
if (item) {
|
||||
// 获取元素相对于列表容器的位置
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<n-flex vertical class="select-none">
|
||||
<n-flex align="center" justify="space-between" class="color-[--text-color] px-20px py-10px">
|
||||
<p class="text-16px">{{ props.type === 'friend' ? '好友通知' : '群通知' }}</p>
|
||||
<svg class="size-18px cursor-pointer"><use href="#delete"></use></svg>
|
||||
<svg class="size-18px cursor-pointer">
|
||||
<use href="#delete"></use>
|
||||
</svg>
|
||||
</n-flex>
|
||||
|
||||
<n-virtual-list
|
||||
@@ -22,14 +24,20 @@
|
||||
<n-avatar
|
||||
round
|
||||
size="large"
|
||||
:src="avatarSrc(groupStore.getUserInfo(isCurrentUser(item.uid) ? item.targetId : item.uid)!.avatar!)"
|
||||
:src="
|
||||
avatarSrc(
|
||||
groupStore.getUserInfo(isCurrentUser(item.senderId) ? item.operateId : item.senderId)!.avatar!
|
||||
)
|
||||
"
|
||||
class="mr-10px" />
|
||||
<n-flex vertical :size="12">
|
||||
<n-flex align="center" :size="10">
|
||||
<p
|
||||
@click="isCurrentUser(item.uid) ? (currentUserId = item.targetId) : (currentUserId = item.uid)"
|
||||
@click="
|
||||
isCurrentUser(item.senderId) ? (currentUserId = item.operateId) : (currentUserId = item.senderId)
|
||||
"
|
||||
class="text-(14px #13987f) cursor-pointer">
|
||||
{{ groupStore.getUserInfo(isCurrentUser(item.uid) ? item.targetId : item.uid)!.name }}
|
||||
{{ groupStore.getUserInfo(isCurrentUser(item.senderId) ? item.operateId : item.senderId)!.name }}
|
||||
</p>
|
||||
|
||||
<p class="text-(14px [--text-color])">
|
||||
@@ -38,39 +46,53 @@
|
||||
|
||||
<p class="text-(10px #909090)">{{ formatTimestamp(item.createTime) }}</p>
|
||||
</n-flex>
|
||||
<p v-show="item.msg" class="text-(12px [--text-color])">留言:{{ item.msg }}</p>
|
||||
<p v-show="isFriendApplyOrGroupInvite(item)" class="text-(12px [--text-color])">
|
||||
留言:{{ item.content }}
|
||||
</p>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<n-flex
|
||||
align="center"
|
||||
:size="10"
|
||||
v-if="item.status === RequestFriendAgreeStatus.Waiting && !isCurrentUser(item.uid)">
|
||||
<n-button secondary :loading="loadingMap[item.applyId]" @click="handleAgree(item.applyId)">接受</n-button>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="dropdownOptions"
|
||||
@select="(key: string) => handleFriendAction(key, item.applyId)">
|
||||
<n-icon class="cursor-pointer px-6px">
|
||||
<svg class="size-16px color-[--text-color]"><use href="#more"></use></svg>
|
||||
</n-icon>
|
||||
</n-dropdown>
|
||||
</n-flex>
|
||||
<span
|
||||
class="text-(12px #64a29c)"
|
||||
:class="{ 'text-(12px #c14053)': item.status === RequestFriendAgreeStatus.Reject }"
|
||||
v-else-if="isCurrentUser(item.uid)">
|
||||
{{
|
||||
isAccepted(item.targetId)
|
||||
? '已同意'
|
||||
: item.status === RequestFriendAgreeStatus.Reject
|
||||
? '对方已拒绝'
|
||||
: '等待验证'
|
||||
}}
|
||||
</span>
|
||||
<span class="text-(12px #64a29c)" v-else-if="item.status === RequestFriendAgreeStatus.Agree">已同意</span>
|
||||
<span class="text-(12px #c14053)" v-else-if="item.status === RequestFriendAgreeStatus.Reject">已拒绝</span>
|
||||
<span class="text-(12px #909090)" v-else-if="item.status === RequestFriendAgreeStatus.Ignore">已忽略</span>
|
||||
<div v-if="isFriendApplyOrGroupInvite(item)">
|
||||
<n-flex
|
||||
align="center"
|
||||
:size="10"
|
||||
v-if="item.status === RequestNoticeAgreeStatus.UNTREATED && !isCurrentUser(item.senderId)">
|
||||
<n-button secondary :loading="loadingMap[item.applyId]" @click="handleAgree(item.applyId)">
|
||||
接受
|
||||
</n-button>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="dropdownOptions"
|
||||
@select="(key: string) => handleFriendAction(key, item.applyId)">
|
||||
<n-icon class="cursor-pointer px-6px">
|
||||
<svg class="size-16px color-[--text-color]">
|
||||
<use href="#more"></use>
|
||||
</svg>
|
||||
</n-icon>
|
||||
</n-dropdown>
|
||||
</n-flex>
|
||||
<span class="text-(12px #64a29c)" v-else-if="item.status === RequestNoticeAgreeStatus.ACCEPTED">
|
||||
已同意
|
||||
</span>
|
||||
<span class="text-(12px #c14053)" v-else-if="item.status === RequestNoticeAgreeStatus.REJECTED">
|
||||
已拒绝
|
||||
</span>
|
||||
<span class="text-(12px #909090)" v-else-if="item.status === RequestNoticeAgreeStatus.IGNORE">
|
||||
已忽略
|
||||
</span>
|
||||
<span
|
||||
class="text-(12px #64a29c)"
|
||||
:class="{ 'text-(12px #c14053)': item.status === RequestNoticeAgreeStatus.REJECTED }"
|
||||
v-else-if="isCurrentUser(item.senderId)">
|
||||
{{
|
||||
isAccepted(item)
|
||||
? '已同意'
|
||||
: item.status === RequestNoticeAgreeStatus.REJECTED
|
||||
? '对方已拒绝'
|
||||
: '等待验证'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
@@ -78,12 +100,12 @@
|
||||
|
||||
<!-- 空数据提示 -->
|
||||
<n-flex v-if="applyList.length === 0" vertical justify="center" align="center" class="py-40px">
|
||||
<n-empty description="暂无好友申请" />
|
||||
<n-empty :description="props.type === 'friend' ? '暂无好友申请' : '暂无群通知'" />
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { RequestFriendAgreeStatus } from '@/services/types.ts'
|
||||
import { NoticeType, RequestNoticeAgreeStatus } from '@/services/types.ts'
|
||||
import { useContactStore } from '@/stores/contacts.ts'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
@@ -102,9 +124,8 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
// 检查好友申请是否已被接受
|
||||
const isAccepted = (targetId: string) => {
|
||||
// 使用缓存集合快速检查目标用户是否在联系人列表中
|
||||
return contactStore.contactsList.some((contact) => contact.uid === targetId)
|
||||
const isAccepted = (item: any) => {
|
||||
return item.status !== RequestNoticeAgreeStatus.UNTREATED
|
||||
}
|
||||
|
||||
const applyList = computed(() => {
|
||||
@@ -119,13 +140,13 @@ const applyList = computed(() => {
|
||||
|
||||
const applyMsg = computed(() => (item: any) => {
|
||||
if (props.type === 'friend') {
|
||||
return isCurrentUser(item.uid)
|
||||
? isAccepted(item.targetId)
|
||||
? '已同意你的请求'
|
||||
: '正在验证你的邀请'
|
||||
: '请求加为好友'
|
||||
return isCurrentUser(item.senderId) ? (isAccepted(item) ? '已同意你的请求' : '正在验证你的邀请') : '请求加为好友'
|
||||
} else {
|
||||
return isCurrentUser(item.uid) ? '已同意你的邀请' : '请求邀请进入群聊'
|
||||
if (isFriendApplyOrGroupInvite(item)) {
|
||||
return isCurrentUser(item.senderId) ? '已同意加入' + item.content : '邀请你加入' + item.content
|
||||
} else if (item.eventType === NoticeType.GROUP_MEMBER_DELETE) {
|
||||
return '已被' + groupStore.getUserInfo(item.senderId)!.name + '踢出' + item.content
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -148,6 +169,16 @@ const isCurrentUser = (uid: string) => {
|
||||
return uid === userStore.userInfo!.uid
|
||||
}
|
||||
|
||||
// 判断是否为好友申请或者群申请、群邀请
|
||||
const isFriendApplyOrGroupInvite = (item: any) => {
|
||||
return (
|
||||
item.eventType === NoticeType.FRIEND_APPLY ||
|
||||
item.eventType === NoticeType.GROUP_INVITE ||
|
||||
item.eventType === NoticeType.GROUP_INVITE_ME ||
|
||||
item.eventType === NoticeType.ADD_ME
|
||||
)
|
||||
}
|
||||
|
||||
// 处理滚动事件
|
||||
const handleScroll = (e: Event) => {
|
||||
if (isLoadingMore.value) return
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- 底部栏 -->
|
||||
<main class="border-t-(1px solid [--right-chat-footer-line-color]) relative" :style="{ height: `${footerHeight}px` }">
|
||||
<main class="border-t-(1px solid [--right-chat-footer-line-color]) relative flex justify-center items-center h-full">
|
||||
<!-- 拖拽手柄 -->
|
||||
<div class="resize-handle" :class="{ dragging: isDragging }" @mousedown="startDrag">
|
||||
<div class="resize-indicator"></div>
|
||||
@@ -15,7 +15,9 @@
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<div class="size-full relative color-[--icon-color] flex flex-col">
|
||||
<ChatMsgMultiChoose v-if="chatStore.isMsgMultiChoose" />
|
||||
|
||||
<div v-else class="size-full relative color-[--icon-color] flex flex-col">
|
||||
<!-- 输入框顶部选项栏 -->
|
||||
<n-flex align="center" justify="space-between" class="p-[10px_22px_5px] select-none flex-shrink-0">
|
||||
<n-flex align="center" :size="0" class="input-options">
|
||||
@@ -162,6 +164,7 @@ import { useGlobalShortcut } from '@/hooks/useGlobalShortcut.ts'
|
||||
import { useMitt } from '@/hooks/useMitt'
|
||||
import { useWindow } from '@/hooks/useWindow'
|
||||
import type { ContactItem, FilesMeta, SessionItem } from '@/services/types'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useContactStore } from '@/stores/contacts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useHistoryStore } from '@/stores/history'
|
||||
@@ -176,6 +179,7 @@ const { detailId } = defineProps<{
|
||||
const globalStore = useGlobalStore()
|
||||
const contactStore = useContactStore()
|
||||
const historyStore = useHistoryStore()
|
||||
const chatStore = useChatStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { handleScreenshot } = useGlobalShortcut()
|
||||
const MsgInputRef = ref()
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
[{{ groupStore.countInfo?.memberNum }}]
|
||||
</p>
|
||||
</label>
|
||||
<svg
|
||||
v-if="activeItem.hotFlag === IsAllUserEnum.Yes && !headerLoading"
|
||||
class="size-20px color-#13987f select-none outline-none">
|
||||
<svg v-if="activeItem.hotFlag === IsAllUserEnum.Yes" class="size-20px color-#13987f select-none outline-none">
|
||||
<use href="#auth"></use>
|
||||
</svg>
|
||||
<n-flex v-else-if="activeItem.type === RoomTypeEnum.SINGLE" align="center">
|
||||
@@ -431,17 +429,15 @@ const tips = ref()
|
||||
const optionsType = ref<RoomActEnum>()
|
||||
const modalShow = ref(false)
|
||||
const sidebarShow = ref(false)
|
||||
const headerLoading = ref(false)
|
||||
const cacheStore = useCachedStore()
|
||||
const { currentSession: activeItem } = storeToRefs(globalStore)
|
||||
// const activeItem = globalStore.currentSession!
|
||||
|
||||
// 是否为频道(仅显示 more 按钮)
|
||||
const isChannel = computed(() => activeItem.value.hotFlag === IsAllUserEnum.Yes || activeItem.value.roomId === '1')
|
||||
const isChannel = computed(() => activeItem.value?.hotFlag === IsAllUserEnum.Yes || activeItem.value?.roomId === '1')
|
||||
// 是否为群主
|
||||
const isGroupOwner = computed(() => {
|
||||
// 频道不能修改群头像和群名称
|
||||
if (activeItem.value.roomId === '1' || activeItem.value.hotFlag === IsAllUserEnum.Yes) {
|
||||
if (!activeItem.value || activeItem.value.roomId === '1' || activeItem.value.hotFlag === IsAllUserEnum.Yes) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -459,8 +455,8 @@ const groupNameInputRef = useTemplateRef<HTMLInputElement | null>('groupNameInpu
|
||||
|
||||
const messageSettingType = computed(() => {
|
||||
// 群消息设置只在免打扰模式下有意义
|
||||
if (activeItem.value.muteNotification === NotificationTypeEnum.NOT_DISTURB) {
|
||||
return activeItem.value.shield ? 'shield' : 'notification'
|
||||
if (activeItem.value?.muteNotification === NotificationTypeEnum.NOT_DISTURB) {
|
||||
return activeItem.value?.shield ? 'shield' : 'notification'
|
||||
}
|
||||
// 非免打扰模式下,默认返回 notification
|
||||
return 'notification'
|
||||
@@ -471,13 +467,14 @@ const messageSettingOptions = ref([
|
||||
])
|
||||
/** 是否在线 */
|
||||
const isOnline = computed(() => {
|
||||
if (!activeItem.value) return false
|
||||
if (activeItem.value.type === RoomTypeEnum.GROUP) return true
|
||||
const contact = contactStore.contactsList.find((item) => item.uid === activeItem.value.detailId)
|
||||
return contact?.activeStatus === OnlineEnum.ONLINE
|
||||
})
|
||||
/** 是否还是好友 */
|
||||
const shouldShowDeleteFriend = computed(() => {
|
||||
if (activeItem.value.type === RoomTypeEnum.GROUP) return false
|
||||
if (!activeItem.value || activeItem.value.type === RoomTypeEnum.GROUP) return false
|
||||
return contactStore.contactsList.some((item) => item.uid === activeItem.value.detailId)
|
||||
})
|
||||
const groupUserList = computed(() => groupStore.userList)
|
||||
@@ -499,7 +496,7 @@ const userList = computed(() => {
|
||||
})
|
||||
/** 获取当前用户的状态信息 */
|
||||
const currentUserStatus = computed(() => {
|
||||
if (activeItem.value.type === RoomTypeEnum.GROUP) return null
|
||||
if (!activeItem.value || activeItem.value.type === RoomTypeEnum.GROUP) return null
|
||||
|
||||
// 使用 useUserInfo 获取用户信息
|
||||
if (!activeItem.value.detailId) return null
|
||||
@@ -522,6 +519,7 @@ const statusTitle = computed(() => {
|
||||
|
||||
// 获取用户的最新头像
|
||||
const currentUserAvatar = computed(() => {
|
||||
if (!activeItem.value) return ''
|
||||
if (activeItem.value.type === RoomTypeEnum.GROUP) {
|
||||
return AvatarUtils.getAvatarUrl(activeItem.value.avatar)
|
||||
} else if (activeItem.value.detailId) {
|
||||
@@ -556,15 +554,6 @@ const {
|
||||
}
|
||||
})
|
||||
|
||||
// 监听消息加载状态变化
|
||||
watch(
|
||||
() => chatStore.currentMessageOptions?.isLoading,
|
||||
(isLoading) => {
|
||||
headerLoading.value = isLoading!
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
stream.value?.getVideoTracks()[0]?.addEventListener('ended', () => {
|
||||
stop()
|
||||
@@ -903,7 +892,6 @@ const handleVideoCall = async (remotedUid: string, callType: CallTypeEnum) => {
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', closeMenu, true)
|
||||
if (!chatStore.currentMessageOptions?.isLoading) headerLoading.value = false
|
||||
|
||||
useMitt.on(WsResponseMessageType.VideoCallRequest, (event) => {
|
||||
info(`收到通话请求:${JSON.stringify(event)}`)
|
||||
|
||||
@@ -49,31 +49,44 @@
|
||||
<!-- 消息列表 -->
|
||||
<div class="message-list min-h-full flex flex-col">
|
||||
<!-- 没有更多消息提示 -->
|
||||
<div v-show="chatStore.shouldShowNoMoreMessage" class="flex-center gap-6px h-32px flex-shrink-0">
|
||||
<span class="text-(12px #909090)">没有更多消息</span>
|
||||
<div
|
||||
v-show="chatStore.shouldShowNoMoreMessage"
|
||||
class="flex-center gap-6px h-32px flex-shrink-0 cursor-default select-none">
|
||||
<p class="text-(12px #909090)">没有更多消息</p>
|
||||
</div>
|
||||
<n-flex
|
||||
v-for="(item, index) in chatStore.chatMessageList"
|
||||
:key="item.message.id"
|
||||
vertical
|
||||
class="flex-y-center"
|
||||
:class="[
|
||||
item.message.type === MsgEnum.RECALL ? 'min-h-22px' : 'min-h-62px',
|
||||
isGroup ? 'p-[14px_10px_14px_20px]' : 'chat-single p-[4px_10px_10px_20px]',
|
||||
{ 'active-reply': activeReply === item.message.id }
|
||||
]"
|
||||
class="flex-y-center mb-12px"
|
||||
:data-message-id="item.message.id"
|
||||
:data-message-index="index">
|
||||
<!-- 信息间隔时间 -->
|
||||
<span class="text-(12px #909090) select-none p-4px" v-if="item.timeBlock">
|
||||
{{ item.timeBlock }}
|
||||
<span class="text-(12px #909090) select-none p-4px" v-if="item.timeBlock" @click.stop>
|
||||
{{ timeToStr(item.message.sendTime) }}
|
||||
</span>
|
||||
<RenderMessage
|
||||
:message="item"
|
||||
:is-group="isGroup"
|
||||
:from-user="{ uid: item.fromUser.uid }"
|
||||
:upload-progress="item.uploadProgress"
|
||||
@jump2-reply="jumpToReplyMsg" />
|
||||
<!-- 消息内容容器 -->
|
||||
<div
|
||||
@mouseenter="hoverId = item.message.id"
|
||||
:class="[
|
||||
'w-full box-border',
|
||||
item.message.type === MsgEnum.RECALL ? 'min-h-22px' : 'min-h-62px',
|
||||
isGroup ? 'p-[14px_10px_14px_20px]' : 'chat-single p-[4px_10px_10px_20px]',
|
||||
{ 'active-reply': activeReply === item.message.id },
|
||||
{ 'bg-#90909020': computeMsgHover(item.message.id) }
|
||||
]"
|
||||
@click="
|
||||
() => {
|
||||
item.isCheck = !item.isCheck
|
||||
}
|
||||
">
|
||||
<RenderMessage
|
||||
:message="item"
|
||||
:is-group="isGroup"
|
||||
:from-user="{ uid: item.fromUser.uid }"
|
||||
:upload-progress="item.uploadProgress"
|
||||
@jump2-reply="jumpToReplyMsg" />
|
||||
</div>
|
||||
</n-flex>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,6 +160,7 @@ import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { audioManager } from '@/utils/AudioManager'
|
||||
import { timeToStr } from '@/utils/ComputedTime'
|
||||
import { isMac, isWindows } from '@/utils/PlatformConstants'
|
||||
|
||||
type AnnouncementData = {
|
||||
@@ -162,8 +176,12 @@ const userStore = useUserStore()
|
||||
const cachedStore = useCachedStore()
|
||||
const networkStatus = useNetworkStatus()
|
||||
const { footerHeight } = useChatLayoutGlobal()
|
||||
// 添加标记,用于识别是否正在加载历史消息
|
||||
const isLoadingMore = ref(false)
|
||||
const { createWebviewWindow } = useWindow()
|
||||
const { handleConfirm, tips, modalShow, selectKey, scrollTop } = useChatMain()
|
||||
const { handlePopoverUpdate, enableScroll } = usePopover(selectKey, 'image-chat-main')
|
||||
|
||||
provide('popoverControls', { enableScroll })
|
||||
|
||||
// 滚动意图状态
|
||||
const scrollIntent = ref<ScrollIntentEnum>(ScrollIntentEnum.NONE)
|
||||
|
||||
@@ -171,26 +189,19 @@ const scrollIntent = ref<ScrollIntentEnum>(ScrollIntentEnum.NONE)
|
||||
const isGroup = computed<boolean>(() => chatStore.isGroup)
|
||||
const userUid = computed(() => userStore.userInfo!.uid || '')
|
||||
const currentNewMsgCount = computed(() => chatStore.currentNewMsgCount || null)
|
||||
const { createWebviewWindow } = useWindow()
|
||||
const currentRoomId = computed(() => globalStore.currentSession?.roomId ?? null)
|
||||
|
||||
// 响应式状态变量
|
||||
const activeReply = ref<string>('')
|
||||
const scrollContainerRef = useTemplateRef<HTMLDivElement>('scrollContainer')
|
||||
const infoPopoverRefs = shallowRef<Record<string, any>>({})
|
||||
const showScrollbar = ref<boolean>(true)
|
||||
// const rafId = ref<number>()
|
||||
const messageIdToIndexMap = shallowRef<Map<string, number>>(new Map())
|
||||
const isAnnouncementHover = ref<boolean>(false)
|
||||
const topAnnouncement = ref<AnnouncementData | null>(null)
|
||||
|
||||
const computeMsgHover = computed(() => (id: string) => {
|
||||
return (
|
||||
chatStore.isMsgMultiChoose &&
|
||||
(hoverId.value === id || chatStore.chatMessageList.find((msg) => msg.message.id === id)?.isCheck)
|
||||
)
|
||||
})
|
||||
// CSS 变量计算,避免动态高度重排
|
||||
const cssVariables = computed(() => {
|
||||
return {
|
||||
'--footer-height': `${footerHeight.value}px`
|
||||
}
|
||||
})
|
||||
|
||||
// 是否显示悬浮页脚
|
||||
const shouldShowFloatFooter = computed<boolean>(() => {
|
||||
const container = scrollContainerRef.value
|
||||
@@ -223,9 +234,19 @@ const shouldShowFloatFooter = computed<boolean>(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
const { handleConfirm, tips, modalShow, selectKey, scrollTop } = useChatMain()
|
||||
const { handlePopoverUpdate, enableScroll } = usePopover(selectKey, 'image-chat-main')
|
||||
provide('popoverControls', { enableScroll })
|
||||
// 响应式状态变量
|
||||
const activeReply = ref<string>('')
|
||||
const scrollContainerRef = useTemplateRef<HTMLDivElement>('scrollContainer')
|
||||
const infoPopoverRefs = shallowRef<Record<string, any>>({})
|
||||
const showScrollbar = ref<boolean>(true)
|
||||
const isAnnouncementHover = ref<boolean>(false)
|
||||
const topAnnouncement = ref<AnnouncementData | null>(null)
|
||||
const hoverId = ref('')
|
||||
// 添加标记,用于识别是否正在加载历史消息
|
||||
const isLoadingMore = ref(false)
|
||||
// 监听公告更新和清空事件的变量
|
||||
let announcementUpdatedListener: any = null
|
||||
let announcementClearListener: any = null
|
||||
|
||||
// 1. 监听房间切换,触发初始化滚动意图
|
||||
watch(
|
||||
@@ -251,7 +272,7 @@ watchPostEffect(() => {
|
||||
// 跳转到回复消息
|
||||
const jumpToReplyMsg = async (key: string): Promise<void> => {
|
||||
// 先在当前列表中尝试查找
|
||||
let messageIndex = chatStore.chatMessageList.findIndex((msg) => msg.message.id === String(key))
|
||||
let messageIndex = chatStore.chatMessageList.findIndex((msg: any) => msg.message.id === String(key))
|
||||
|
||||
// 如果找到了,直接滚动到该消息
|
||||
if (messageIndex !== -1) {
|
||||
@@ -345,12 +366,14 @@ const handleScrollByIntent = (intent: ScrollIntentEnum): void => {
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = (): void => {
|
||||
// console.log('触发滚动到底部')
|
||||
const container = scrollContainerRef.value
|
||||
if (!container) return
|
||||
// 立即清除新消息计数
|
||||
chatStore.clearNewMsgCount()
|
||||
scrollContainerRef.value.scrollTo({
|
||||
top: scrollContainerRef.value.scrollHeight,
|
||||
// 使用requestAnimationFrame确保在下一帧执行,避免布局抖动
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'auto'
|
||||
})
|
||||
}
|
||||
@@ -358,7 +381,10 @@ const scrollToBottom = (): void => {
|
||||
// 处理悬浮按钮点击 - 重置消息列表并滚动到底部
|
||||
const handleFloatButtonClick = async () => {
|
||||
try {
|
||||
await chatStore.resetAndRefreshCurrentRoomMessages()
|
||||
// 只有消息数量超过60条才进行重置和刷新
|
||||
if (chatStore.chatMessageList.length > 60) {
|
||||
await chatStore.resetAndRefreshCurrentRoomMessages()
|
||||
}
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
console.error('重置消息列表失败:', error)
|
||||
@@ -366,14 +392,6 @@ const handleFloatButtonClick = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新消息索引映射
|
||||
const updateMessageIndexMap = (): void => {
|
||||
messageIdToIndexMap.value.clear()
|
||||
chatStore.chatMessageList.forEach((msg, index) => {
|
||||
messageIdToIndexMap.value.set(msg.message.id, index)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理滚动事件(用于页脚显示功能)
|
||||
const handleScroll = (event: Event) => {
|
||||
const container = event.target as HTMLElement
|
||||
@@ -430,11 +448,8 @@ watch(
|
||||
|
||||
// 监听消息列表变化
|
||||
watch(
|
||||
chatStore.chatMessageList,
|
||||
(value, oldValue) => {
|
||||
// 更新消息索引映射
|
||||
updateMessageIndexMap()
|
||||
|
||||
() => chatStore.chatMessageList,
|
||||
async (value, oldValue) => {
|
||||
// 简化消息列表监听,避免直接滚动操作
|
||||
if (value.length > oldValue.length) {
|
||||
// 获取最新消息
|
||||
@@ -448,10 +463,9 @@ watch(
|
||||
// 新消息计数逻辑(不在底部时)
|
||||
const container = scrollContainerRef.value
|
||||
if (container) {
|
||||
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight
|
||||
const distanceFromBottom = container.scrollHeight - container.scrollTop
|
||||
const isOtherUserMessage =
|
||||
latestMessage?.fromUser?.uid && String(latestMessage.fromUser.uid) !== String(userUid.value)
|
||||
|
||||
// 只有当不在底部且是他人消息时才增加计数
|
||||
if (distanceFromBottom > 300 && isOtherUserMessage) {
|
||||
const current = chatStore.newMsgCount.get(globalStore.currentSession!.roomId)
|
||||
@@ -463,6 +477,9 @@ watch(
|
||||
} else {
|
||||
current.count++
|
||||
}
|
||||
} else {
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,9 +528,6 @@ const handleLoadMore = async (): Promise<void> => {
|
||||
try {
|
||||
await chatStore.loadMore()
|
||||
|
||||
// 更新消息索引映射
|
||||
updateMessageIndexMap()
|
||||
|
||||
// 计算新的滚动位置,保持用户在加载前的相对位置
|
||||
const newScrollHeight = container.scrollHeight
|
||||
const heightDifference = newScrollHeight - oldScrollHeight
|
||||
@@ -573,9 +587,20 @@ const handleViewAnnouncement = (): void => {
|
||||
})
|
||||
}
|
||||
|
||||
// 监听公告更新和清空事件的变量
|
||||
let announcementUpdatedListener: any = null
|
||||
let announcementClearListener: any = null
|
||||
useMitt.on(`${MittEnum.INFO_POPOVER}-Main`, (event: any) => {
|
||||
selectKey.value = event.uid
|
||||
infoPopoverRefs.value[event.uid].setShow(true)
|
||||
handlePopoverUpdate(event.uid)
|
||||
})
|
||||
|
||||
// 监听滚动到底部的事件
|
||||
useMitt.on(MittEnum.CHAT_SCROLL_BOTTOM, async () => {
|
||||
// 只有消息数量超过60条才进行重置和刷新
|
||||
if (chatStore.chatMessageList.length > 20) {
|
||||
await chatStore.resetAndRefreshCurrentRoomMessages()
|
||||
}
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化监听器
|
||||
@@ -614,22 +639,8 @@ onMounted(() => {
|
||||
initListeners().catch(console.error)
|
||||
|
||||
nextTick(() => {
|
||||
updateMessageIndexMap()
|
||||
loadTopAnnouncement()
|
||||
})
|
||||
|
||||
useMitt.on(`${MittEnum.INFO_POPOVER}-Main`, (event: any) => {
|
||||
selectKey.value = event.uid
|
||||
infoPopoverRefs.value[event.uid].setShow(true)
|
||||
handlePopoverUpdate(event.uid)
|
||||
})
|
||||
|
||||
// 监听滚动到底部的事件
|
||||
useMitt.on(MittEnum.CHAT_SCROLL_BOTTOM, () => {
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
})
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div class="relative flex-center gap-22px h-full">
|
||||
<!-- 功能模块 -->
|
||||
<div class="flex flex-col flex-y-center gap-14px" v-for="opt in opts">
|
||||
<n-button :disabled="opt.disabled" secondary circle @click="opt.click" class="size-46px">
|
||||
<template #icon>
|
||||
<svg class="size-22px">
|
||||
<use :href="opt.icon"></use>
|
||||
</svg>
|
||||
</template>
|
||||
</n-button>
|
||||
<p class="text-(12px [--text-color])">{{ opt.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-modal v-model:show="showModal" class="w-70% rounded-8px">
|
||||
<div class="bg-[--bg-popover] h-full p-6px box-border flex flex-col">
|
||||
<div
|
||||
v-if="isMac()"
|
||||
@click="showModal = false"
|
||||
class="mac-close z-999 size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none absolute left-6px">
|
||||
<svg class="hidden size-7px color-#000 select-none absolute top-3px left-3px">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<svg v-if="isWindows()" @click="showModal = false" class="size-12px ml-a cursor-pointer select-none">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
<div class="pt-20px flex flex-col select-none">
|
||||
<div class="flex flex-row">
|
||||
<!-- 搜索会话 -->
|
||||
<div class="flex-1 h-64vh bg-#f3f3f3 dark:bg-#222 rounded-8px px-12px pt-12px flex flex-col">
|
||||
<n-input
|
||||
id="search"
|
||||
v-model:value="searchText"
|
||||
class="rounded-6px w-full relative text-12px"
|
||||
style="background: var(--search-bg-color)"
|
||||
:maxlength="20"
|
||||
clearable
|
||||
size="small">
|
||||
<template #prefix>
|
||||
<svg class="w-12px h-12px">
|
||||
<use href="#search"></use>
|
||||
</svg>
|
||||
</template>
|
||||
</n-input>
|
||||
|
||||
<n-scrollbar class="flex-1">
|
||||
<template v-for="session in chatStore.sessionList" :key="session.roomId">
|
||||
<n-flex align="center" :size="8" class="text-(12px #909090) py-8px px-4px">
|
||||
<n-checkbox v-model:checked="session.isCheck" @click.stop />
|
||||
<n-avatar class="rounded-8px" :size="30" :src="AvatarUtils.getAvatarUrl(session.avatar)" />
|
||||
<p>{{ session.remark ? session.remark : session.name }}</p>
|
||||
<p v-if="session.type === RoomTypeEnum.GROUP">
|
||||
({{ groupStore.getGroupDetailByRoomId(session.roomId)?.memberNum }})
|
||||
</p>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
<!-- 已选择会话 -->
|
||||
<div class="flex-1 min-w-0 h-64vh px-12px flex flex-col">
|
||||
<p class="text-(12px #909090) pb-10px">分别发送给</p>
|
||||
<n-scrollbar class="flex-1">
|
||||
<template v-for="session in selectedSessions" :key="session.roomId">
|
||||
<n-flex align="center" class="p-8px">
|
||||
<n-avatar class="rounded-8px" :size="30" :src="AvatarUtils.getAvatarUrl(session.avatar)" />
|
||||
<p class="text-(12px [--chat-text-color])">{{ session.remark ? session.remark : session.name }}</p>
|
||||
<n-button quaternary circle size="small" class="ml-auto" @click="handleRemoveSession(session.roomId)">
|
||||
<template #icon>
|
||||
<n-icon size="14">
|
||||
<svg class="w-14px h-14px">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-scrollbar>
|
||||
|
||||
<span class="w-full h-1px bg-[--line-color] my-8px"></span>
|
||||
|
||||
<div class="flex-1 flex flex-col">
|
||||
<ChatMultiMsg :content-list="msgContents" :msg-ids="msgIds" />
|
||||
<n-input class="my-12px" placeholder="给朋友留言" />
|
||||
<div class="flex justify-between">
|
||||
<n-button quaternary class="w-100px h-30px" @click="showModal = false">取消</n-button>
|
||||
<n-button secondary type="primary" class="w-100px h-30px" @click="sendMsg">发送</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MergeMessageType, RoomTypeEnum } from '@/enums'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { mergeMsg } from '@/utils/ImRequestUtils'
|
||||
import { isMac, isWindows } from '@/utils/PlatformConstants'
|
||||
import type { MsgId } from '~/src/typings/global'
|
||||
import ChatMultiMsg from './ChatMultiMsg.vue'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const groupStore = useGroupStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const showModal = ref(false)
|
||||
const searchText = ref('')
|
||||
const selectedSessions = computed(() => {
|
||||
return chatStore.sessionList.filter((session) => session.isCheck === true)
|
||||
})
|
||||
const selectedMsgs = computed(() => {
|
||||
return chatStore.chatMessageList.filter((msg) => msg.isCheck === true)
|
||||
})
|
||||
|
||||
const msgContents = computed(() => {
|
||||
return selectedMsgs.value.map((msg) => {
|
||||
const userInfo = groupStore.getUserInfo(msg.fromUser.uid)
|
||||
return userInfo?.name + ': ' + msg.message.body.content
|
||||
})
|
||||
})
|
||||
|
||||
const msgIds = computed((): MsgId[] => {
|
||||
return selectedMsgs.value.map((msg) => {
|
||||
return {
|
||||
msgId: msg.message.id,
|
||||
fromUid: msg.fromUser.uid
|
||||
}
|
||||
})
|
||||
})
|
||||
let mergeMessageType = MergeMessageType.SINGLE
|
||||
|
||||
const opts = computed(() => [
|
||||
{
|
||||
text: '逐条转发',
|
||||
icon: '#share-three',
|
||||
disabled: selectedMsgs.value.length === 0,
|
||||
click: () => {
|
||||
console.log(selectedMsgs)
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '合并转发',
|
||||
icon: '#share',
|
||||
disabled: selectedMsgs.value.length === 0,
|
||||
click: () => {
|
||||
if (selectedMsgs.value.length > 0) {
|
||||
showModal.value = true
|
||||
mergeMessageType = MergeMessageType.MERGE
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '收藏',
|
||||
icon: '#collect'
|
||||
},
|
||||
{
|
||||
text: '保存至电脑',
|
||||
icon: '#collect-laptop'
|
||||
},
|
||||
{
|
||||
text: '删除',
|
||||
icon: '#delete'
|
||||
},
|
||||
{
|
||||
text: '',
|
||||
icon: '#close',
|
||||
click: () => {
|
||||
chatStore.chatMessageList.forEach((msg) => (msg.isCheck = false))
|
||||
chatStore.setMsgMultiChoose(false)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const handleRemoveSession = (roomId: string) => {
|
||||
const session = chatStore.sessionList.find((session) => session.roomId === roomId)
|
||||
if (session) {
|
||||
session.isCheck = false
|
||||
}
|
||||
}
|
||||
|
||||
const sendMsg = async () => {
|
||||
console.log('发送合并消息:', mergeMessageType)
|
||||
const selectedRoomIds = selectedSessions.value.map((item) => item.roomId)
|
||||
const selectedMsgIds = selectedMsgs.value.map((item) => item.message.id)
|
||||
|
||||
await mergeMsg({
|
||||
roomIds: selectedRoomIds,
|
||||
type: mergeMessageType,
|
||||
messageIds: selectedMsgIds,
|
||||
fromRoomId: globalStore.currentSessionRoomId
|
||||
})
|
||||
.then(() => {
|
||||
window.$message.success('消息转发成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('消息转发失败', e)
|
||||
window.$message.error(e)
|
||||
})
|
||||
.finally(() => {
|
||||
showModal.value = false
|
||||
chatStore.clearSessionCheck()
|
||||
chatStore.setMsgMultiChoose(false)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
/**! 修改naive-ui复选框的样式 */
|
||||
:deep(.n-checkbox .n-checkbox-box) {
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="border-solid border-2 border-#F9F9F9 p-2" @click="openMultiMsgWindow">
|
||||
<p class="font-600 text-size-14px mb-2">群聊的聊天记录</p>
|
||||
<div class="max-h-15 overflow-hidden">
|
||||
<p v-for="content in contentList" class="text-size-14px text-#A2A2A2 line-height-normal truncate">
|
||||
{{ content }}
|
||||
</p>
|
||||
</div>
|
||||
<n-divider class="!my-2" />
|
||||
<p class="text-size-14px text-#A2A2A2">聊天记录</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWindow } from '@/hooks/useWindow'
|
||||
import type { MsgId } from '@/typings/global'
|
||||
|
||||
const props = defineProps<{
|
||||
contentList: string[]
|
||||
msgIds: MsgId[]
|
||||
}>()
|
||||
|
||||
const { contentList, msgIds } = toRefs(props)
|
||||
const { createWindow, sendWindowPayload } = useWindow()
|
||||
|
||||
const openMultiMsgWindow = async () => {
|
||||
const label = 'multiMsg--' + Date.now()
|
||||
try {
|
||||
// 创建窗口
|
||||
await createWindow({
|
||||
title: '聊天记录',
|
||||
label: label,
|
||||
width: 800,
|
||||
height: 600,
|
||||
wantCloseWindow: undefined,
|
||||
resizable: true,
|
||||
minW: 600,
|
||||
minH: 400,
|
||||
transparent: false,
|
||||
visible: true,
|
||||
queryParams: {
|
||||
key: label
|
||||
}
|
||||
})
|
||||
|
||||
// 向窗口发送消息数据
|
||||
await sendWindowPayload(label, msgIds.value)
|
||||
} catch (e) {
|
||||
console.error('创建合并消息窗口失败:', e)
|
||||
window.$message?.error('打开聊天记录失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -254,6 +254,10 @@ const filteredUserList = computed(() => {
|
||||
})
|
||||
}
|
||||
return userList.sort((a, b) => {
|
||||
if (a.roleId && b.roleId && a.roleId !== b.roleId) {
|
||||
return a.roleId - b.roleId
|
||||
}
|
||||
|
||||
if (a.activeStatus !== b.activeStatus) {
|
||||
return a.activeStatus - b.activeStatus
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
<!-- 头部 -->
|
||||
<ChatHeader />
|
||||
<n-flex :class="{ 'shadow-inner': page.shadow }" :size="0" class="h-full">
|
||||
<div class="flex flex-col flex-1 relative min-h-0">
|
||||
<div class="flex-col flex-1 relative">
|
||||
<!-- 中间聊天框内容 -->
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
<div class="max-h-80%">
|
||||
<ChatMain />
|
||||
</div>
|
||||
|
||||
<!-- 输入框和操作列表 -->
|
||||
<ChatFooter :detail-id="globalStore.currentSession!.detailId" />
|
||||
<ChatFooter class="max-h-20%" :detail-id="globalStore.currentSession!.detailId" />
|
||||
</div>
|
||||
<!-- 右侧栏占位:群聊时预留宽度直至 Sidebar 挂载完成,随后由子组件控制宽度(含折叠) -->
|
||||
<ChatSidebar />
|
||||
@@ -22,12 +23,4 @@ const settingStore = useSettingStore()
|
||||
const { page } = storeToRefs(settingStore)
|
||||
const globalStore = useGlobalStore()
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
/**! 修改naive-ui虚拟列表滚动条的间距 */
|
||||
:deep(
|
||||
.n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--vertical,
|
||||
.n-scrollbar + .n-scrollbar-rail.n-scrollbar-rail--vertical
|
||||
) {
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -40,13 +40,14 @@
|
||||
import { MsgEnum } from '@/enums/index'
|
||||
import { useImageViewer } from '@/hooks/useImageViewer'
|
||||
import type { ImageBody } from '@/services/types'
|
||||
import { isMobile } from '@/utils/PlatformConstants'
|
||||
|
||||
const props = defineProps<{
|
||||
body: ImageBody
|
||||
onImageClick?: (url: string) => void
|
||||
}>()
|
||||
// 图片显示相关常量
|
||||
const MAX_WIDTH = 320
|
||||
const MAX_WIDTH = isMobile() ? 240 : 320
|
||||
const MAX_HEIGHT = 240
|
||||
const MIN_WIDTH = 60
|
||||
const MIN_HEIGHT = 60
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<ChatMultiMsg :content-list="body.content" :msg-ids="msgIds" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MergeBody } from '@/services/types'
|
||||
import type { MsgId } from '@/typings/global'
|
||||
|
||||
const props = defineProps<{
|
||||
body: MergeBody
|
||||
}>()
|
||||
|
||||
const { body } = toRefs(props)
|
||||
|
||||
const msgIds = computed((): MsgId[] => {
|
||||
return body.value.body.map((item) => {
|
||||
return {
|
||||
msgId: item.messageId,
|
||||
fromUid: item.uid
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// console.log(props.body.body)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -9,26 +9,30 @@
|
||||
{{ item }}
|
||||
</span>
|
||||
<template v-else-if="item.startsWith('http')">
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<svg class="size-12px cursor-pointer pr-4px" @click="handleCopy(item)">
|
||||
<use href="#copy"></use>
|
||||
</svg>
|
||||
</template>
|
||||
<span>复制网址</span>
|
||||
</n-tooltip>
|
||||
<n-highlight
|
||||
v-if="props.searchKeyword"
|
||||
:text="item"
|
||||
:patterns="[props.searchKeyword]"
|
||||
:highlight-style="{
|
||||
userSelect: 'text',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '6px',
|
||||
color: '#000',
|
||||
background: '#13987f'
|
||||
}" />
|
||||
<p v-else>{{ item }}</p>
|
||||
<n-flex align="center" :wrap="false">
|
||||
<n-tooltip trigger="hover" style="flex-shrink: 0">
|
||||
<template #trigger>
|
||||
<svg class="size-12px cursor-pointer pr-4px" @click="handleCopy(item)">
|
||||
<use href="#copy"></use>
|
||||
</svg>
|
||||
</template>
|
||||
<span>复制网址</span>
|
||||
</n-tooltip>
|
||||
<div style="flex: 1; word-wrap: break-word; overflow-wrap: anywhere">
|
||||
<n-highlight
|
||||
v-if="props.searchKeyword"
|
||||
:text="item"
|
||||
:patterns="[props.searchKeyword]"
|
||||
:highlight-style="{
|
||||
userSelect: 'text',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '6px',
|
||||
color: '#000',
|
||||
background: '#13987f'
|
||||
}" />
|
||||
<p v-else style="margin: 0">{{ item }}</p>
|
||||
</div>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-highlight
|
||||
v-else-if="props.searchKeyword"
|
||||
|
||||
@@ -18,244 +18,246 @@
|
||||
<!-- 信息时间(单聊) -->
|
||||
<div
|
||||
v-if="!isGroup"
|
||||
class="text-(12px #909090) h-12px w-fit select-none"
|
||||
:class="
|
||||
isMe
|
||||
? activeReply === message.message.id
|
||||
? 'pr-68px'
|
||||
: 'pr-42px'
|
||||
: activeReply === message.message.id
|
||||
? 'pl-68px'
|
||||
: 'pl-42px'
|
||||
">
|
||||
class="text-(12px #909090) h-12px flex select-none"
|
||||
:class="{
|
||||
'pr-48px justify-end': isMe,
|
||||
'pl-42px justify-start': !isMe
|
||||
}">
|
||||
<Transition name="fade-single">
|
||||
<span v-if="hoverMsgId === message.message.id">
|
||||
{{ formatTimestamp(message.message.sendTime, true) }}
|
||||
</span>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="flex items-start flex-1 select-none" :class="isMe ? 'flex-row-reverse' : ''">
|
||||
<!-- 回复消息提示的箭头 -->
|
||||
<svg
|
||||
v-if="activeReply === message.message.id"
|
||||
class="size-16px pt-4px color-#909090"
|
||||
:class="isMe ? 'ml-8px' : 'mr-8px'">
|
||||
<use :href="isMe ? `#corner-down-left` : `#corner-down-right`"></use>
|
||||
</svg>
|
||||
<!-- 头像 -->
|
||||
<n-popover
|
||||
:ref="(el: any) => (infoPopoverRefs[message.message.id] = el)"
|
||||
@update:show="handlePopoverUpdate(message.message.id, $event)"
|
||||
trigger="click"
|
||||
placement="right"
|
||||
:show-arrow="false"
|
||||
style="padding: 0; background: var(--bg-info)">
|
||||
<template #trigger>
|
||||
<ContextMenu
|
||||
@select="$event.click(message, 'Main')"
|
||||
:content="message"
|
||||
:menu="isGroup ? optionsList : void 0"
|
||||
:special-menu="report">
|
||||
<!-- 存在头像时候显示 -->
|
||||
<n-avatar
|
||||
round
|
||||
:size="34"
|
||||
@click="selectKey = message.message.id"
|
||||
class="select-none"
|
||||
:color="themes.content === ThemeEnum.DARK ? '' : '#fff'"
|
||||
:fallback-src="themes.content === ThemeEnum.DARK ? '/logoL.png' : '/logoD.png'"
|
||||
:src="getAvatarSrc(message.fromUser.uid)"
|
||||
:class="isMe ? '' : 'mr-10px'" />
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<!-- 用户个人信息框 -->
|
||||
<InfoPopover v-if="selectKey === message.message.id" :uid="fromUser.uid" />
|
||||
</n-popover>
|
||||
<div class="flex justify-center items-center">
|
||||
<n-checkbox
|
||||
v-model:checked="message.isCheck"
|
||||
v-if="chatStore.isMsgMultiChoose"
|
||||
class="mr-3 select-none"
|
||||
:focusable="false"
|
||||
@click.stop />
|
||||
<div class="flex items-start flex-1 select-none" :class="isMe ? 'flex-row-reverse' : ''">
|
||||
<!-- 回复消息提示的箭头 -->
|
||||
<svg
|
||||
v-if="activeReply === message.message.id"
|
||||
class="size-16px pt-4px color-#909090"
|
||||
:class="isMe ? 'ml-8px' : 'mr-8px'">
|
||||
<use :href="isMe ? `#corner-down-left` : `#corner-down-right`"></use>
|
||||
</svg>
|
||||
<!-- 头像 -->
|
||||
<n-popover
|
||||
:ref="(el: any) => (infoPopoverRefs[message.message.id] = el)"
|
||||
@update:show="handlePopoverUpdate(message.message.id, $event)"
|
||||
trigger="click"
|
||||
placement="right"
|
||||
:show-arrow="false"
|
||||
style="padding: 0; background: var(--bg-info)">
|
||||
<template #trigger>
|
||||
<ContextMenu
|
||||
@select="$event.click(message, 'Main')"
|
||||
:content="message"
|
||||
:menu="isGroup ? optionsList : void 0"
|
||||
:special-menu="report">
|
||||
<!-- 存在头像时候显示 -->
|
||||
<n-avatar
|
||||
round
|
||||
:size="34"
|
||||
@click="selectKey = message.message.id"
|
||||
class="select-none"
|
||||
:color="themes.content === ThemeEnum.DARK ? '' : '#fff'"
|
||||
:fallback-src="themes.content === ThemeEnum.DARK ? '/logoL.png' : '/logoD.png'"
|
||||
:src="getAvatarSrc(message.fromUser.uid)"
|
||||
:class="isMe ? '' : 'mr-10px'" />
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<!-- 用户个人信息框 -->
|
||||
<InfoPopover v-if="selectKey === message.message.id" :uid="fromUser.uid" />
|
||||
</n-popover>
|
||||
|
||||
<n-flex
|
||||
vertical
|
||||
justify="center"
|
||||
:size="6"
|
||||
class="color-[--text-color] flex-1 select-none"
|
||||
:class="isMe ? 'items-end mr-10px' : ''">
|
||||
<n-flex :size="6" align="center" :style="isMe ? 'flex-direction: row-reverse' : ''">
|
||||
<ContextMenu
|
||||
@select="$event.click(message, 'Main')"
|
||||
:content="message"
|
||||
:menu="isGroup ? optionsList : void 0"
|
||||
:special-menu="report">
|
||||
<n-flex
|
||||
:size="6"
|
||||
class="select-none cursor-default"
|
||||
align="center"
|
||||
v-if="isGroup"
|
||||
:style="isMe ? 'flex-direction: row-reverse' : ''">
|
||||
<!-- 用户徽章 -->
|
||||
<n-popover
|
||||
v-if="
|
||||
globalStore.currentSession?.roomId === '1' &&
|
||||
useBadgeInfo(groupStore.getUserInfo(fromUser.uid)?.wearingItemId).value.img
|
||||
"
|
||||
trigger="hover">
|
||||
<template #trigger>
|
||||
<n-avatar
|
||||
class="select-none"
|
||||
:size="18"
|
||||
round
|
||||
:fallback-src="themes.content === ThemeEnum.DARK ? '/logoL.png' : '/logoD.png'"
|
||||
:src="useBadgeInfo(groupStore.getUserInfo(fromUser.uid)?.wearingItemId).value.img" />
|
||||
</template>
|
||||
<span>
|
||||
{{ useBadgeInfo(groupStore.getUserInfo(fromUser.uid)?.wearingItemId).value.describe }}
|
||||
<n-flex
|
||||
vertical
|
||||
:size="6"
|
||||
class="color-[--text-color] flex-1 select-none"
|
||||
:class="isMe ? 'items-end mr-10px' : ''">
|
||||
<n-flex :size="6" align="center" :style="isMe ? 'flex-direction: row-reverse' : ''">
|
||||
<ContextMenu
|
||||
@select="$event.click(message, 'Main')"
|
||||
:content="message"
|
||||
:menu="isGroup ? optionsList : void 0"
|
||||
:special-menu="report">
|
||||
<n-flex
|
||||
:size="6"
|
||||
class="select-none cursor-default"
|
||||
align="center"
|
||||
v-if="isGroup"
|
||||
:style="isMe ? 'flex-direction: row-reverse' : ''">
|
||||
<!-- 用户徽章 -->
|
||||
<n-popover
|
||||
v-if="
|
||||
globalStore.currentSession?.roomId === '1' &&
|
||||
useBadgeInfo(groupStore.getUserInfo(fromUser.uid)?.wearingItemId).value.img
|
||||
"
|
||||
trigger="hover">
|
||||
<template #trigger>
|
||||
<n-avatar
|
||||
class="select-none"
|
||||
:size="18"
|
||||
round
|
||||
:fallback-src="themes.content === ThemeEnum.DARK ? '/logoL.png' : '/logoD.png'"
|
||||
:src="useBadgeInfo(groupStore.getUserInfo(fromUser.uid)?.wearingItemId).value.img" />
|
||||
</template>
|
||||
<span>
|
||||
{{ useBadgeInfo(groupStore.getUserInfo(fromUser.uid)?.wearingItemId).value.describe }}
|
||||
</span>
|
||||
</n-popover>
|
||||
<!-- 用户名 -->
|
||||
<span class="text-12px select-none color-#909090 inline-block align-top">
|
||||
{{ groupStore.getUserDisplayName(fromUser.uid) }}
|
||||
</span>
|
||||
</n-popover>
|
||||
<!-- 用户名 -->
|
||||
<span class="text-12px select-none color-#909090 inline-block align-top">
|
||||
{{ groupStore.getUserDisplayName(fromUser.uid) }}
|
||||
</span>
|
||||
<!-- 消息归属地 -->
|
||||
<span v-if="groupStore.getUserInfo(fromUser.uid)?.locPlace" class="text-(12px #909090)">
|
||||
({{ groupStore.getUserInfo(fromUser.uid)?.locPlace }})
|
||||
</span>
|
||||
</n-flex>
|
||||
</ContextMenu>
|
||||
<!-- 群主 -->
|
||||
<div
|
||||
v-if="groupStore.isCurrentLord(fromUser.uid)"
|
||||
class="flex px-4px py-3px rounded-4px bg-#d5304f30 size-fit select-none">
|
||||
<span class="text-(9px #d5304f)">群主</span>
|
||||
</div>
|
||||
<!-- 管理员 -->
|
||||
<div
|
||||
v-if="groupStore.isAdmin(fromUser.uid)"
|
||||
class="flex px-4px py-3px rounded-4px bg-#1a7d6b30 size-fit select-none">
|
||||
<span class="text-(9px #008080)">管理员</span>
|
||||
</div>
|
||||
<!-- 信息时间(群聊) -->
|
||||
<Transition name="fade-group">
|
||||
<span v-if="isGroup && hoverMsgId === message.message.id" class="text-(12px #909090) select-none">
|
||||
{{ formatTimestamp(message.message.sendTime, true) }}
|
||||
</span>
|
||||
</Transition>
|
||||
</n-flex>
|
||||
<!-- 气泡样式 -->
|
||||
<ContextMenu
|
||||
:content="message"
|
||||
@contextmenu="handleMacSelect"
|
||||
@mouseenter="() => (hoverMsgId = message.message.id)"
|
||||
@mouseleave="() => (hoverMsgId = '')"
|
||||
class="w-fit relative flex flex-col"
|
||||
:data-key="isMe ? `U${message.message.id}` : `Q${message.message.id}`"
|
||||
:class="isMe ? 'items-end' : 'items-start'"
|
||||
:style="{ '--bubble-max-width': isGroup ? '32vw' : '50vw' }"
|
||||
@select="$event.click(message)"
|
||||
:menu="handleItemType(message.message.type)"
|
||||
:emoji="emojiList"
|
||||
:special-menu="specialMenuList(message.message.type)"
|
||||
@reply-emoji="handleEmojiSelect($event, message)"
|
||||
@click="handleMsgClick(message)">
|
||||
<component
|
||||
:class="[
|
||||
message.message.type === MsgEnum.VOICE ? 'select-none cursor-pointer' : 'select-text cursor-text',
|
||||
!isSpecialMsgType(message.message.type) ? (isMe ? 'bubble-oneself' : 'bubble') : '',
|
||||
{
|
||||
active:
|
||||
activeBubble === message.message.id &&
|
||||
!isSpecialMsgType(message.message.type) &&
|
||||
message.message.type !== MsgEnum.VOICE
|
||||
}
|
||||
]"
|
||||
:is="componentMap[message.message.type]"
|
||||
:body="message.message.body"
|
||||
:message-status="message.message.status"
|
||||
:upload-progress="uploadProgress"
|
||||
:from-user-uid="fromUser?.uid"
|
||||
:message="message.message"
|
||||
:data-message-id="message.message.id"
|
||||
:is-group="isGroup"
|
||||
:on-image-click="onImageClick"
|
||||
:on-video-click="onVideoClick"
|
||||
:search-keyword="searchKeyword" />
|
||||
|
||||
<!-- 显示翻译文本 -->
|
||||
<Transition name="fade-translate" appear mode="out-in">
|
||||
<!-- 消息归属地 -->
|
||||
<span v-if="groupStore.getUserInfo(fromUser.uid)?.locPlace" class="text-(12px #909090)">
|
||||
({{ groupStore.getUserInfo(fromUser.uid)?.locPlace }})
|
||||
</span>
|
||||
</n-flex>
|
||||
</ContextMenu>
|
||||
<!-- 群主 -->
|
||||
<div
|
||||
v-if="message.message.body.translatedText"
|
||||
class="translated-text select-none cursor-default flex flex-col"
|
||||
:class="[isMe ? 'item-end' : 'item-start']">
|
||||
<n-flex align="center" justify="space-between" class="mb-6px">
|
||||
<n-flex align="center" :size="4">
|
||||
<span class="text-(12px #909090)">{{ message.message.body.translatedText.provider }}</span>
|
||||
<svg class="size-12px">
|
||||
<use href="#success"></use>
|
||||
v-if="groupStore.isCurrentLord(fromUser.uid)"
|
||||
class="flex px-4px py-3px rounded-4px bg-#d5304f30 size-fit select-none">
|
||||
<span class="text-(9px #d5304f)">群主</span>
|
||||
</div>
|
||||
<!-- 管理员 -->
|
||||
<div
|
||||
v-if="groupStore.isAdmin(fromUser.uid)"
|
||||
class="flex px-4px py-3px rounded-4px bg-#1a7d6b30 size-fit select-none">
|
||||
<span class="text-(9px #008080)">管理员</span>
|
||||
</div>
|
||||
<!-- 信息时间(群聊) -->
|
||||
<Transition name="fade-group">
|
||||
<span v-if="isGroup && hoverMsgId === message.message.id" class="text-(12px #909090) select-none">
|
||||
{{ formatTimestamp(message.message.sendTime, true) }}
|
||||
</span>
|
||||
</Transition>
|
||||
</n-flex>
|
||||
<!-- 气泡样式 -->
|
||||
<ContextMenu
|
||||
:content="message"
|
||||
@contextmenu="handleMacSelect"
|
||||
@mouseenter="() => (hoverMsgId = message.message.id)"
|
||||
@mouseleave="() => (hoverMsgId = '')"
|
||||
class="w-fit relative flex flex-col chat-message-max-width"
|
||||
:data-key="isMe ? `U${message.message.id}` : `Q${message.message.id}`"
|
||||
:class="isMe ? 'items-end' : 'items-start'"
|
||||
:style="{ '--bubble-max-width': isGroup ? '32vw' : '50vw' }"
|
||||
@select="$event.click(message)"
|
||||
:menu="handleItemType(message.message.type)"
|
||||
:emoji="emojiList"
|
||||
:special-menu="specialMenuList(message.message.type)"
|
||||
@reply-emoji="handleEmojiSelect($event, message)"
|
||||
@click="handleMsgClick(message)">
|
||||
<component
|
||||
:class="[
|
||||
message.message.type === MsgEnum.VOICE ? 'select-none cursor-pointer' : 'select-text cursor-text',
|
||||
!isSpecialMsgType(message.message.type) ? (isMe ? 'bubble-oneself' : 'bubble') : '',
|
||||
{
|
||||
active:
|
||||
activeBubble === message.message.id &&
|
||||
!isSpecialMsgType(message.message.type) &&
|
||||
message.message.type !== MsgEnum.VOICE
|
||||
}
|
||||
]"
|
||||
:is="componentMap[message.message.type]"
|
||||
:body="message.message.body"
|
||||
:message-status="message.message.status"
|
||||
:upload-progress="uploadProgress"
|
||||
:from-user-uid="fromUser?.uid"
|
||||
:message="message.message"
|
||||
:data-message-id="message.message.id"
|
||||
:is-group="isGroup"
|
||||
:on-image-click="onImageClick"
|
||||
:on-video-click="onVideoClick"
|
||||
:search-keyword="searchKeyword" />
|
||||
|
||||
<!-- 显示翻译文本 -->
|
||||
<Transition name="fade-translate" appear mode="out-in">
|
||||
<div
|
||||
v-if="message.message.body.translatedText"
|
||||
class="translated-text select-none cursor-default flex flex-col"
|
||||
:class="[isMe ? 'item-end' : 'item-start']">
|
||||
<n-flex align="center" justify="space-between" class="mb-6px">
|
||||
<n-flex align="center" :size="4">
|
||||
<span class="text-(12px #909090)">{{ message.message.body.translatedText.provider }}</span>
|
||||
<svg class="size-12px">
|
||||
<use href="#success"></use>
|
||||
</svg>
|
||||
</n-flex>
|
||||
<svg class="size-10px cursor-pointer" @click="message.message.body.translatedText = null">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
</n-flex>
|
||||
<svg class="size-10px cursor-pointer" @click="message.message.body.translatedText = null">
|
||||
<use href="#close"></use>
|
||||
<p class="select-text cursor-text">{{ message.message.body.translatedText.text }}</p>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- 消息状态指示器 -->
|
||||
<div v-if="isMe" class="absolute -left-6 top-2">
|
||||
<n-icon v-if="message.message.status === MessageStatusEnum.SENDING" class="text-gray-400">
|
||||
<img class="size-16px" src="@/assets/img/loading-one.svg" alt="" />
|
||||
</n-icon>
|
||||
<n-icon
|
||||
v-if="message.message.status === MessageStatusEnum.FAILED"
|
||||
class="text-#d5304f cursor-pointer"
|
||||
@click.stop="handleRetry(message)">
|
||||
<svg class="size-16px">
|
||||
<use href="#cloudError"></use>
|
||||
</svg>
|
||||
</n-flex>
|
||||
<p class="select-text cursor-text">{{ message.message.body.translatedText.text }}</p>
|
||||
</n-icon>
|
||||
</div>
|
||||
</Transition>
|
||||
</ContextMenu>
|
||||
|
||||
<!-- 消息状态指示器 -->
|
||||
<div v-if="isMe" class="absolute -left-6 top-2">
|
||||
<n-icon v-if="message.message.status === MessageStatusEnum.SENDING" class="text-gray-400">
|
||||
<img class="size-16px" src="@/assets/img/loading-one.svg" alt="" />
|
||||
</n-icon>
|
||||
<n-icon
|
||||
v-if="message.message.status === MessageStatusEnum.FAILED"
|
||||
class="text-#d5304f cursor-pointer"
|
||||
@click.stop="handleRetry(message)">
|
||||
<svg class="size-16px">
|
||||
<use href="#cloudError"></use>
|
||||
</svg>
|
||||
</n-icon>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
<!-- 回复的内容 -->
|
||||
<n-flex
|
||||
align="center"
|
||||
:size="6"
|
||||
v-if="message.message.body.reply"
|
||||
@click="emit('jump2Reply', message.message.body.reply.id)"
|
||||
class="reply-bubble relative w-fit custom-shadow select-none chat-message-max-width">
|
||||
<svg class="size-14px">
|
||||
<use href="#to-top"></use>
|
||||
</svg>
|
||||
<n-avatar
|
||||
class="reply-avatar"
|
||||
round
|
||||
:size="20"
|
||||
:color="themes.content === ThemeEnum.DARK ? '' : '#fff'"
|
||||
:fallback-src="themes.content === ThemeEnum.DARK ? '/logoL.png' : '/logoD.png'"
|
||||
:src="getAvatarSrc(message.message.body.reply.uid)" />
|
||||
<span>{{ `${message.message.body.reply.username}: ` }}</span>
|
||||
<span class="content-span">
|
||||
{{ message.message.body.reply.body }}
|
||||
</span>
|
||||
<div v-if="message.message.body.reply.imgCount" class="reply-img-sub">
|
||||
{{ message.message.body.reply.imgCount }}
|
||||
</div>
|
||||
</n-flex>
|
||||
|
||||
<!-- 回复的内容 -->
|
||||
<n-flex
|
||||
align="center"
|
||||
:size="6"
|
||||
v-if="message.message.body.reply"
|
||||
@click="emit('jump2Reply', message.message.body.reply.id)"
|
||||
class="reply-bubble relative w-fit custom-shadow select-none">
|
||||
<svg class="size-14px">
|
||||
<use href="#to-top"></use>
|
||||
</svg>
|
||||
<n-avatar
|
||||
class="reply-avatar"
|
||||
round
|
||||
:size="20"
|
||||
:color="themes.content === ThemeEnum.DARK ? '' : '#fff'"
|
||||
:fallback-src="themes.content === ThemeEnum.DARK ? '/logoL.png' : '/logoD.png'"
|
||||
:src="getAvatarSrc(message.message.body.reply.uid)" />
|
||||
<span>{{ `${message.message.body.reply.username}: ` }}</span>
|
||||
<span class="content-span">
|
||||
{{ message.message.body.reply.body }}
|
||||
</span>
|
||||
<div v-if="message.message.body.reply.imgCount" class="reply-img-sub">
|
||||
{{ message.message.body.reply.imgCount }}
|
||||
<!-- 动态渲染所有回复表情反应 -->
|
||||
<div
|
||||
v-if="message.message"
|
||||
class="flex-y-center gap-6px flex-wrap w-270px"
|
||||
:class="{ 'justify-end': isSingleLineEmojis(message) }">
|
||||
<template v-for="emoji in emojiList" :key="emoji.value">
|
||||
<!-- 根据表情类型获取对应的计数属性名 -->
|
||||
<div class="flex-y-center" v-if="message && getEmojiCount(message, emoji.value) > 0">
|
||||
<div class="emoji-reply-bubble" @click.stop="message && cancelReplyEmoji(message, emoji.value)">
|
||||
<img :title="emoji.title" class="size-18px" :src="emoji.url" :alt="emoji.title" />
|
||||
<span class="text-(12px #eee)">{{ message ? getEmojiCount(message, emoji.value) : 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</n-flex>
|
||||
|
||||
<!-- 动态渲染所有回复表情反应 -->
|
||||
<div
|
||||
v-if="message.message"
|
||||
class="flex-y-center gap-6px flex-wrap w-270px"
|
||||
:class="{ 'justify-end': isSingleLineEmojis(message) }">
|
||||
<template v-for="emoji in emojiList" :key="emoji.value">
|
||||
<!-- 根据表情类型获取对应的计数属性名 -->
|
||||
<div class="flex-y-center" v-if="message && getEmojiCount(message, emoji.value) > 0">
|
||||
<div class="emoji-reply-bubble" @click.stop="message && cancelReplyEmoji(message, emoji.value)">
|
||||
<img :title="emoji.title" class="size-18px" :src="emoji.url" :alt="emoji.title" />
|
||||
<span class="text-(12px #eee)">{{ message ? getEmojiCount(message, emoji.value) : 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</n-flex>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -272,6 +274,7 @@ import { useGroupStore } from '@/stores/group'
|
||||
import { useSettingStore } from '@/stores/setting'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { formatTimestamp } from '@/utils/ComputedTime.ts'
|
||||
import { useChatStore } from '~/src/stores/chat'
|
||||
import { useUserStore } from '~/src/stores/user'
|
||||
import { markMsg } from '~/src/utils/ImRequestUtils'
|
||||
import { isMac } from '~/src/utils/PlatformConstants'
|
||||
@@ -280,6 +283,7 @@ import AudioCall from './AudioCall.vue'
|
||||
import Emoji from './Emoji.vue'
|
||||
import File from './File.vue'
|
||||
import Image from './Image.vue'
|
||||
import MergeMessage from './MergeMessage.vue'
|
||||
import BotMessage from './special/BotMessage.vue'
|
||||
import RecallMessage from './special/RecallMessage.vue'
|
||||
import SystemMessage from './special/SystemMessage.vue'
|
||||
@@ -320,6 +324,7 @@ const settingStore = useSettingStore()
|
||||
const { themes } = storeToRefs(settingStore)
|
||||
const { optionsList, report, activeBubble, handleItemType, emojiList, specialMenuList, handleMsgClick } = useChatMain()
|
||||
const groupStore = useGroupStore()
|
||||
const chatStore = useChatStore()
|
||||
const recordEL = ref<HTMLElement>()
|
||||
|
||||
// 获取用户头像
|
||||
@@ -340,7 +345,8 @@ const componentMap: Partial<Record<MsgEnum, Component>> = {
|
||||
[MsgEnum.AUDIO_CALL]: AudioCall,
|
||||
[MsgEnum.SYSTEM]: SystemMessage,
|
||||
[MsgEnum.RECALL]: RecallMessage,
|
||||
[MsgEnum.BOT]: BotMessage
|
||||
[MsgEnum.BOT]: BotMessage,
|
||||
[MsgEnum.MERGE]: MergeMessage
|
||||
}
|
||||
|
||||
const isSpecialMsgType = (type: number): boolean => {
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<template>
|
||||
<!-- 消息为机器人消息时 -->
|
||||
<div
|
||||
class="dark:bg-[#fbb99020] bg-[#fbb99030] dark:border-(1px solid #fbb99020) border-(1px solid #fbb99040) flex-center gap-8px px-12px py-4px rounded-8px">
|
||||
<n-avatar class="select-none" round :size="22" :src="getAvatarSrc(fromUserUid)" />
|
||||
<main class="w-full flex-center">
|
||||
<div
|
||||
v-for="(part, index) in parseMessage(body.content)"
|
||||
:key="index"
|
||||
class="text-(12px #fbb990) select-none cursor-default">
|
||||
<p v-if="part.type === 'text'">{{ part.text }}</p>
|
||||
<p v-else-if="part.type === 'bracket'" class="text-#13987f">{{ part.text }}</p>
|
||||
<p v-else-if="part.type === 'number'" class="text-#fbb160">{{ part.text }}</p>
|
||||
class="chat-message-max-width dark:bg-[#fbb99020] bg-[#fbb99030] dark:border-(1px solid #fbb99020) border-(1px solid #fbb99040) flex-center chat-bot-message-gap px-12px py-4px rounded-8px">
|
||||
<n-avatar class="select-none" round :size="22" :src="getAvatarSrc(fromUserUid)" />
|
||||
<div
|
||||
v-for="(part, index) in parseMessage(body.content)"
|
||||
:key="index"
|
||||
class="text-(12px #fbb990) select-none cursor-default">
|
||||
<p v-if="part.type === 'text'">{{ part.text }}</p>
|
||||
<p v-else-if="part.type === 'bracket'" class="text-#13987f truncate max-w-20">
|
||||
{{ part.text }}
|
||||
</p>
|
||||
<p v-else-if="part.type === 'number'" class="text-#fbb160">{{ part.text }}</p>
|
||||
</div>
|
||||
<img class="size-18px" src="/emoji/party-popper.webp" alt="" />
|
||||
</div>
|
||||
<img class="size-18px" src="/emoji/party-popper.webp" alt="" />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- 消息为撤回消息 -->
|
||||
<div>
|
||||
<main class="w-full flex-center">
|
||||
<template v-if="isGroup">
|
||||
<n-flex align="center" :size="6" v-if="fromUserUid === userUid">
|
||||
<p class="text-(12px #909090) select-none cursor-default">你撤回了一条消息</p>
|
||||
@@ -26,7 +26,7 @@
|
||||
</p>
|
||||
</n-flex>
|
||||
</template>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<!-- 系统消息 -->
|
||||
<div class="flex-col-y-center h-62px select-none cursor-default text-(12px #909090)">
|
||||
{{ props.body }}
|
||||
</div>
|
||||
<main class="w-full flex-center h-62px">
|
||||
<p class="select-none cursor-default text-(12px #909090)">
|
||||
{{ props.body }}
|
||||
</p>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
+14
-2
@@ -101,7 +101,9 @@ export enum MittEnum {
|
||||
/** 视频下载状态更新 */
|
||||
VIDEO_DOWNLOAD_STATUS_UPDATED = 'videoDownloadStatusUpdated',
|
||||
/** 切换语言页面 */
|
||||
VOICE_RECORD_TOGGLE = 'voiceRecordToggle'
|
||||
VOICE_RECORD_TOGGLE = 'voiceRecordToggle',
|
||||
/** 消息多选 */
|
||||
MSG_MULTI_CHOOSE = 'msgMultiChoose'
|
||||
}
|
||||
|
||||
/** 主题类型 */
|
||||
@@ -632,6 +634,8 @@ export enum ImUrlEnum {
|
||||
APPLY_UN_READ_COUNT = 'applyUnReadCount',
|
||||
/** 请求申请页面 */
|
||||
REQUEST_APPLY_PAGE = 'requestApplyPage',
|
||||
/** 请求通知页面 */
|
||||
REQUEST_NOTICE_PAGE = 'requestNoticePage',
|
||||
/** 获取联系人列表 */
|
||||
GET_CONTACT_LIST = 'getContactList',
|
||||
/** 搜索好友 */
|
||||
@@ -686,7 +690,10 @@ export enum ImUrlEnum {
|
||||
MARK_MSG_READ = 'markMsgRead',
|
||||
/** 移出群成员 */
|
||||
REMOVE_GROUP_MEMBER = 'removeGroupMember',
|
||||
CHECK_EMAIL = 'checkEmail'
|
||||
CHECK_EMAIL = 'checkEmail',
|
||||
|
||||
MERGE_MSG = 'mergeMsg',
|
||||
GET_USER_BY_IDS = 'getUserByIds'
|
||||
}
|
||||
|
||||
// 滚动意图管理枚举
|
||||
@@ -696,3 +703,8 @@ export enum ScrollIntentEnum {
|
||||
NEW_MESSAGE = 'new_message', // 新消息到达
|
||||
LOAD_MORE = 'load_more' // 加载更多历史消息
|
||||
}
|
||||
|
||||
export enum MergeMessageType {
|
||||
SINGLE = 1,
|
||||
MERGE = 2
|
||||
}
|
||||
|
||||
@@ -201,6 +201,13 @@ export const useChatMain = (isHistoryMode = false) => {
|
||||
handleCopy(item.message.body.content, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '多选',
|
||||
icon: 'list-checkbox',
|
||||
click: () => {
|
||||
chatStore.setMsgMultiChoose(true)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '翻译',
|
||||
icon: 'translate',
|
||||
|
||||
+19
-2
@@ -41,6 +41,22 @@ export const useMessage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载聊天室
|
||||
* @param roomId
|
||||
*/
|
||||
const preloadChatRoom = async (roomId: string = '1') => {
|
||||
globalStore.updateCurrentSessionRoomId(roomId)
|
||||
try {
|
||||
await chatStore.changeRoom()
|
||||
await markMsgRead(roomId)
|
||||
chatStore.markSessionRead(roomId || '1')
|
||||
await globalStore.updateGlobalUnreadCount()
|
||||
} catch (error) {
|
||||
console.error('尝试进入聊天室时出现错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
* @param roomId 会话信息
|
||||
@@ -223,9 +239,10 @@ export const useMessage = () => {
|
||||
return 'logout'
|
||||
},
|
||||
click: async (item: SessionItem) => {
|
||||
console.log('删除好友或退出群聊执行')
|
||||
// 单聊:删除好友
|
||||
if (item.type === RoomTypeEnum.SINGLE) {
|
||||
await contactStore.onDeleteContact(item.id)
|
||||
await contactStore.onDeleteContact(item.detailId)
|
||||
await handleMsgDelete(item.roomId)
|
||||
window.$message.success('已删除好友')
|
||||
return
|
||||
@@ -295,5 +312,5 @@ export const useMessage = () => {
|
||||
useMitt.off(MittEnum.SHRINK_WINDOW, () => {})
|
||||
})
|
||||
|
||||
return { msgBoxShow, handleMsgClick, handleMsgDelete, handleMsgDblclick, menuList, specialMenuList }
|
||||
return { msgBoxShow, handleMsgClick, handleMsgDelete, handleMsgDblclick, menuList, specialMenuList, preloadChatRoom }
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export const useMockMessage = () => {
|
||||
},
|
||||
status: MessageStatusEnum.PENDING
|
||||
},
|
||||
sendTime: String(currentTimeStamp),
|
||||
sendTime: currentTimeStamp,
|
||||
loading: true
|
||||
}
|
||||
}
|
||||
|
||||
+31
-35
@@ -361,7 +361,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
}
|
||||
const msg = await messageStrategy.getMsg(msgInput.value, reply.value)
|
||||
const atUidList = extractAtUserIds(msgInput.value, groupStore.userList)
|
||||
const tempMsgId = Date.now().toString()
|
||||
const tempMsgId = 'T' + Date.now().toString()
|
||||
|
||||
// 根据消息类型创建消息体
|
||||
const messageBody = {
|
||||
@@ -379,12 +379,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
console.log('👾临时消息:', tempMsg)
|
||||
|
||||
// 设置发送状态的定时器
|
||||
const statusTimer = setTimeout(() => {
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.SENDING
|
||||
})
|
||||
}, 800)
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.SENDING
|
||||
})
|
||||
|
||||
try {
|
||||
// 如果是图片或表情消息,需要先上传文件
|
||||
@@ -472,8 +470,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: message.oldMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: message.message.id,
|
||||
body: message.message.body
|
||||
body: message.message.body,
|
||||
timeBlock: message.timeBlock
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
// 监听错误响应
|
||||
@@ -483,6 +483,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: msgId,
|
||||
status: MessageStatusEnum.FAILED
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
await invoke(TauriCommand.SEND_MSG, {
|
||||
@@ -496,16 +497,6 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
errorChannel
|
||||
})
|
||||
|
||||
// 停止发送状态的定时器
|
||||
clearTimeout(statusTimer)
|
||||
|
||||
// 更新消息状态为成功,并使用服务器返回的消息体
|
||||
// chatStore.updateMsg({
|
||||
// msgId: tempMsgId,
|
||||
// status: MessageStatusEnum.SUCCESS,
|
||||
// newMsgId: res,
|
||||
// })
|
||||
|
||||
// 更新会话最后活动时间
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession!.roomId)
|
||||
|
||||
@@ -520,7 +511,6 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('消息发送失败:', error)
|
||||
clearTimeout(statusTimer)
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.FAILED
|
||||
@@ -963,8 +953,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: message.oldMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: message.message.id,
|
||||
body: message.message.body
|
||||
body: message.message.body,
|
||||
timeBlock: message.timeBlock
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
// 监听错误响应
|
||||
@@ -974,6 +966,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: msgId,
|
||||
status: MessageStatusEnum.FAILED
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
await invoke(TauriCommand.SEND_MSG, {
|
||||
@@ -1047,8 +1040,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: message.oldMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: message.message.id,
|
||||
body: message.message.body
|
||||
body: message.message.body,
|
||||
timeBlock: message.timeBlock
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
// 监听错误响应
|
||||
@@ -1058,6 +1053,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: msgId,
|
||||
status: MessageStatusEnum.FAILED
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
await invoke(TauriCommand.SEND_MSG, {
|
||||
@@ -1158,8 +1154,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: message.oldMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: message.message.id,
|
||||
body: message.message.body
|
||||
body: message.message.body,
|
||||
timeBlock: message.timeBlock
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
// 监听错误响应
|
||||
@@ -1169,6 +1167,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: msgId,
|
||||
status: MessageStatusEnum.FAILED
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
await invoke(TauriCommand.SEND_MSG, {
|
||||
@@ -1232,7 +1231,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
duration: voiceData.duration,
|
||||
filename: voiceData.filename
|
||||
}
|
||||
const tempMsgId = Date.now().toString()
|
||||
const tempMsgId = 'T' + Date.now().toString()
|
||||
|
||||
// 创建消息体(初始使用本地路径)
|
||||
const messageBody = {
|
||||
@@ -1260,7 +1259,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
body: messageBody,
|
||||
messageMarks: {}
|
||||
},
|
||||
sendTime: new Date().toISOString(),
|
||||
sendTime: Date.now(),
|
||||
loading: false
|
||||
}
|
||||
|
||||
@@ -1268,12 +1267,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
chatStore.pushMsg(tempMsg)
|
||||
|
||||
// 设置发送状态的定时器
|
||||
const statusTimer = setTimeout(() => {
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.SENDING
|
||||
})
|
||||
}, 800)
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.SENDING
|
||||
})
|
||||
|
||||
try {
|
||||
// 获取语音消息策略
|
||||
@@ -1318,8 +1315,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: message.oldMsgId,
|
||||
status: MessageStatusEnum.SUCCESS,
|
||||
newMsgId: message.message.id,
|
||||
body: message.message.body
|
||||
body: message.message.body,
|
||||
timeBlock: message.timeBlock
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
// 监听错误响应
|
||||
@@ -1329,6 +1328,7 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgId: msgId,
|
||||
status: MessageStatusEnum.FAILED
|
||||
})
|
||||
useMitt.emit(MittEnum.CHAT_SCROLL_BOTTOM)
|
||||
}
|
||||
|
||||
await invoke(TauriCommand.SEND_MSG, {
|
||||
@@ -1336,8 +1336,6 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
successChannel: voiceSuccessChannel,
|
||||
errorChannel: voiceErrorChannel
|
||||
})
|
||||
// 停止发送状态的定时器
|
||||
clearTimeout(statusTimer)
|
||||
|
||||
// 更新会话最后活动时间
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession!.roomId)
|
||||
@@ -1347,7 +1345,6 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
// asset:// 协议不需要手动释放
|
||||
}
|
||||
} catch (apiError: any) {
|
||||
clearTimeout(statusTimer)
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.FAILED
|
||||
@@ -1355,7 +1352,6 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
throw new Error(`发送消息失败: ${apiError.message || apiError}`)
|
||||
}
|
||||
} catch (uploadError) {
|
||||
clearTimeout(statusTimer)
|
||||
chatStore.updateMsg({
|
||||
msgId: tempMsgId,
|
||||
status: MessageStatusEnum.FAILED
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { useEventListener, useNetwork, useTimeoutFn } from '@vueuse/core'
|
||||
import { RoomTypeEnum } from '@/enums'
|
||||
import webSocket from '@/services/webSocketAdapter'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useContactStore } from '@/stores/contacts'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { isMobile } from '@/utils/PlatformConstants'
|
||||
|
||||
/**
|
||||
@@ -52,7 +47,7 @@ export const useNetworkReconnect = () => {
|
||||
}
|
||||
|
||||
// 执行数据刷新
|
||||
refreshAllData()
|
||||
// refreshAllData()
|
||||
}
|
||||
|
||||
// 网络变为离线时记录时间
|
||||
@@ -89,7 +84,7 @@ export const useNetworkReconnect = () => {
|
||||
// 如果离线超过30秒,则刷新数据
|
||||
if (lastOfflineTimestamp > 0 && currentTime - lastOfflineTimestamp > 30000) {
|
||||
console.log('[Network] 移动端应用从后台恢复,刷新数据')
|
||||
refreshAllData()
|
||||
// refreshAllData()
|
||||
lastOfflineTimestamp = 0
|
||||
}
|
||||
}
|
||||
@@ -143,45 +138,45 @@ export const useNetworkReconnect = () => {
|
||||
webSocket.forceReconnect()
|
||||
|
||||
// 刷新数据
|
||||
refreshAllData()
|
||||
// refreshAllData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新所有重要数据
|
||||
*/
|
||||
const refreshAllData = async () => {
|
||||
const chatStore = useChatStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const groupStore = useGroupStore()
|
||||
const contactStore = useContactStore()
|
||||
// const refreshAllData = async () => {
|
||||
// const chatStore = useChatStore()
|
||||
// const globalStore = useGlobalStore()
|
||||
// const groupStore = useGroupStore()
|
||||
// const contactStore = useContactStore()
|
||||
|
||||
// 刷新会话列表
|
||||
await chatStore.getSessionList(true)
|
||||
// 如果当前有选中的聊天室,重置并刷新消息列表
|
||||
if (globalStore.currentSession?.roomId) {
|
||||
// 获取最新消息
|
||||
await chatStore.resetAndRefreshCurrentRoomMessages()
|
||||
}
|
||||
// 如果当前是群聊,刷新群组信息
|
||||
if (globalStore.currentSession?.type === RoomTypeEnum.GROUP) {
|
||||
await groupStore.getGroupUserList(globalStore.currentSession.roomId)
|
||||
}
|
||||
// 刷新联系人列表
|
||||
await contactStore.getContactList(true)
|
||||
// 更新未读消息计数
|
||||
await globalStore.updateGlobalUnreadCount()
|
||||
// 刷新在线用户列表
|
||||
await groupStore.refreshGroupMembers()
|
||||
// // 刷新会话列表
|
||||
// await chatStore.getSessionList(true)
|
||||
// // 如果当前有选中的聊天室,重置并刷新消息列表
|
||||
// // if (globalStore.currentSession?.roomId) {
|
||||
// // // 获取最新消息
|
||||
// // await chatStore.resetAndRefreshCurrentRoomMessages()
|
||||
// // }
|
||||
// // 如果当前是群聊,刷新群组信息
|
||||
// if (globalStore.currentSession?.type === RoomTypeEnum.GROUP) {
|
||||
// await groupStore.getGroupUserList(globalStore.currentSession.roomId)
|
||||
// }
|
||||
// // 刷新联系人列表
|
||||
// await contactStore.getContactList(true)
|
||||
// // 更新未读消息计数
|
||||
// await globalStore.updateGlobalUnreadCount()
|
||||
// // 刷新在线用户列表
|
||||
// await groupStore.refreshGroupMembers()
|
||||
|
||||
console.log('[Network] 数据刷新完成')
|
||||
}
|
||||
// console.log('[Network] 数据刷新完成')
|
||||
// }
|
||||
|
||||
return {
|
||||
isOnline,
|
||||
offlineAt,
|
||||
onlineAt,
|
||||
refreshAllData,
|
||||
// refreshAllData,
|
||||
checkConnectionHealth
|
||||
}
|
||||
}
|
||||
|
||||
+39
-5
@@ -6,9 +6,41 @@ import { info } from '@tauri-apps/plugin-log'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { isCompatibility, isWindows } from '@/utils/PlatformConstants'
|
||||
|
||||
type WebviewWindowOpt = {
|
||||
title: string
|
||||
label: string
|
||||
width: number
|
||||
height: number
|
||||
wantCloseWindow?: string
|
||||
resizable: boolean
|
||||
minW: number
|
||||
minH: number
|
||||
transparent?: boolean
|
||||
visible: boolean
|
||||
queryParams?: Record<string, string | number | boolean>
|
||||
}
|
||||
|
||||
/** 判断是兼容的系统 */
|
||||
const isCompatibilityMode = computed(() => isCompatibility())
|
||||
export const useWindow = () => {
|
||||
const createWindow = async (opt: WebviewWindowOpt) => {
|
||||
const { title, label, width, height, wantCloseWindow, resizable, minW, minH, transparent, visible, queryParams } =
|
||||
opt
|
||||
await createWebviewWindow(
|
||||
title,
|
||||
label,
|
||||
width,
|
||||
height,
|
||||
wantCloseWindow,
|
||||
resizable,
|
||||
minW,
|
||||
minH,
|
||||
transparent,
|
||||
visible,
|
||||
queryParams
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建窗口
|
||||
* @param title 窗口标题
|
||||
@@ -36,16 +68,17 @@ export const useWindow = () => {
|
||||
visible = false,
|
||||
queryParams?: Record<string, string | number | boolean>
|
||||
) => {
|
||||
const checkLabel = computed(() => {
|
||||
const checkLabel = () => {
|
||||
/** 如果是打开独立窗口就截取label中的固定label名称 */
|
||||
if (label.includes(EventEnum.ALONE)) {
|
||||
return label.replace(/\d/g, '')
|
||||
} else {
|
||||
return label
|
||||
}
|
||||
})
|
||||
}
|
||||
label = checkLabel()
|
||||
// 构建URL,包含查询参数
|
||||
let url = `/${checkLabel.value}`
|
||||
let url = `/${label.split('--')[0]}`
|
||||
if (queryParams && Object.keys(queryParams).length > 0) {
|
||||
const searchParams = new URLSearchParams()
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
@@ -120,7 +153,7 @@ export const useWindow = () => {
|
||||
* const payload = await getWindowPayload<MyPayload>('my-window')
|
||||
*/
|
||||
const getWindowPayload = async <T>(windowLabel: string) => {
|
||||
return invoke<T>('get_window_payload', { label: windowLabel })
|
||||
return await invoke<T>('get_window_payload', { label: windowLabel })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,6 +305,7 @@ export const useWindow = () => {
|
||||
checkWinExist,
|
||||
setResizable,
|
||||
sendWindowPayload,
|
||||
getWindowPayload
|
||||
getWindowPayload,
|
||||
createWindow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,13 +132,16 @@ import { MittEnum } from '@/enums'
|
||||
import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import { useWindow } from '@/hooks/useWindow'
|
||||
import router from '@/router'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import * as ImRequestUtils from '@/utils/ImRequestUtils'
|
||||
import { isMac, isWindows } from '@/utils/PlatformConstants'
|
||||
import { createGroup, options, renderLabel, renderSourceList, renderTargetList } from './model.tsx'
|
||||
import { options, renderLabel, renderSourceList, renderTargetList } from './model.tsx'
|
||||
|
||||
const { createWebviewWindow } = useWindow()
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const settingStore = useSettingStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const { page } = storeToRefs(settingStore)
|
||||
@@ -235,7 +238,16 @@ const resetCreateGroupState = () => {
|
||||
const handleCreateGroup = async () => {
|
||||
if (selectedValue.value.length < 2) return
|
||||
try {
|
||||
await createGroup(selectedValue.value)
|
||||
const result = await ImRequestUtils.createGroup({ uidList: selectedValue.value })
|
||||
|
||||
// 创建成功后刷新会话列表以显示新群聊
|
||||
await chatStore.getSessionList(true)
|
||||
|
||||
// 如果API返回了id,切换到新创建的群聊
|
||||
if (result?.id) {
|
||||
globalStore.updateCurrentSessionRoomId(result.id)
|
||||
}
|
||||
|
||||
resetCreateGroupState()
|
||||
window.$message.success('创建群聊成功')
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useContactStore } from '@/stores/contacts.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useGroupStore } from '@/stores/group.ts'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import * as ImRequestUtils from '@/utils/ImRequestUtils'
|
||||
|
||||
const contactStore = useContactStore()
|
||||
const groupStore = useGroupStore()
|
||||
@@ -57,17 +56,6 @@ export const getFilteredOptions = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const createGroup = async (selectedUids: string[]) => {
|
||||
try {
|
||||
const uidList = selectedUids.map((uid) => uid)
|
||||
const result = await ImRequestUtils.createGroup({ uidList })
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('创建群聊失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的源列表渲染函数,通过参数控制是否使用过滤后的选项
|
||||
export const renderSourceList = (preSelectedFriendId = '', enablePreSelection = true): TransferRenderSourceList => {
|
||||
return ({ onCheck, checkedOptions, pattern }) => {
|
||||
|
||||
+13
-2
@@ -30,7 +30,7 @@ import { emitTo, listen } from '@tauri-apps/api/event'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { info } from '@tauri-apps/plugin-log'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
import { ChangeTypeEnum, MittEnum, ModalEnum, NotificationTypeEnum, OnlineEnum, TauriCommand } from '@/enums'
|
||||
import { ChangeTypeEnum, MittEnum, ModalEnum, MsgEnum, NotificationTypeEnum, OnlineEnum, TauriCommand } from '@/enums'
|
||||
import { useCheckUpdate } from '@/hooks/useCheckUpdate'
|
||||
import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import type { MarkItemType, MessageType, RevokedMsgType, UserItem } from '@/services/types.ts'
|
||||
@@ -46,6 +46,7 @@ import { useConfigStore } from '@/stores/config'
|
||||
import { useContactStore } from '@/stores/contacts.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { useSettingStore } from '@/stores/setting'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { audioManager } from '@/utils/AudioManager'
|
||||
import { isWindows } from '@/utils/PlatformConstants'
|
||||
@@ -72,7 +73,7 @@ const AsyncLeft = defineAsyncComponent({
|
||||
const AsyncCenter = defineAsyncComponent({
|
||||
loader: async () => {
|
||||
await import('./left/index.vue')
|
||||
loadingText.value = '正在加载中间面板...'
|
||||
loadingText.value = '正在加载数据中...'
|
||||
const comp = await import('./center/index.vue')
|
||||
|
||||
// 加载所有会话
|
||||
@@ -118,12 +119,18 @@ const groupStore = useGroupStore()
|
||||
const userStore = useUserStore()
|
||||
const chatStore = useChatStore()
|
||||
const configStore = useConfigStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { checkUpdate, CHECK_UPDATE_TIME } = useCheckUpdate()
|
||||
const userUid = computed(() => userStore.userInfo!.uid)
|
||||
const shrinkStatus = ref(false)
|
||||
|
||||
// 播放消息音效
|
||||
const playMessageSound = async () => {
|
||||
// 检查是否开启了消息提示音
|
||||
if (!settingStore.notification?.messageSound) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const audio = new Audio('/sound/message.mp3')
|
||||
await audioManager.play(audio, 'message-notification')
|
||||
@@ -305,7 +312,11 @@ useMitt.on(WsResponseMessageType.MY_ROOM_INFO_CHANGE, (data: { myName: string; r
|
||||
})
|
||||
|
||||
useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => {
|
||||
if (userStore.isMe(data.fromUser.uid) && data.message.type !== MsgEnum.MERGE) {
|
||||
return
|
||||
}
|
||||
chatStore.pushMsg(data)
|
||||
|
||||
data.message.sendTime = new Date(data.message.sendTime).getTime()
|
||||
await invokeSilently(TauriCommand.SAVE_MSG, {
|
||||
data
|
||||
|
||||
@@ -26,19 +26,47 @@
|
||||
|
||||
<div class="flex justify-between px-25px flex-1 items-center py-10px">
|
||||
<div v-for="item in options" :key="item.icon" class="flex flex-wrap items-center" @click="clickItem(item.icon)">
|
||||
<svg class="h-24px w-24px iconpark-icon"><use :href="`#${item.icon}`"></use></svg>
|
||||
<svg
|
||||
v-if="item.showArrow"
|
||||
:class="['h-15px w-15px iconpark-icon transition-transform duration-300', item.isRotate ? 'rotate' : '']">
|
||||
<use href="#down" />
|
||||
</svg>
|
||||
<div v-if="item.label !== 'file' && item.label !== 'image'">
|
||||
<svg class="h-24px w-24px iconpark-icon"><use :href="`#${item.icon}`"></use></svg>
|
||||
<svg
|
||||
v-if="item.showArrow"
|
||||
:class="['h-15px w-15px iconpark-icon transition-transform duration-300', item.isRotate ? 'rotate' : '']">
|
||||
<use href="#down" />
|
||||
</svg>
|
||||
</div>
|
||||
<div v-else-if="item.label === 'file'">
|
||||
<van-uploader multiple :after-read="afterReadFile">
|
||||
<svg class="h-24px w-24px iconpark-icon"><use :href="`#${item.icon}`"></use></svg>
|
||||
<svg
|
||||
v-if="item.showArrow"
|
||||
:class="[
|
||||
'h-15px w-15px iconpark-icon transition-transform duration-300',
|
||||
item.isRotate ? 'rotate' : ''
|
||||
]">
|
||||
<use href="#down" />
|
||||
</svg>
|
||||
</van-uploader>
|
||||
</div>
|
||||
<div v-else-if="item.label === 'image'">
|
||||
<van-uploader accept="image/*" multiple :after-read="afterReadImage">
|
||||
<svg class="h-24px w-24px iconpark-icon"><use :href="`#${item.icon}`"></use></svg>
|
||||
<svg
|
||||
v-if="item.showArrow"
|
||||
:class="[
|
||||
'h-15px w-15px iconpark-icon transition-transform duration-300',
|
||||
item.isRotate ? 'rotate' : ''
|
||||
]">
|
||||
<use href="#down" />
|
||||
</svg>
|
||||
</van-uploader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开面板 -->
|
||||
<Transition @before-enter="beforeEnter" @enter="enter" @leave="leave">
|
||||
<div v-show="isPanelVisible" class="w-full overflow-hidden bg-#13987f flex flex-col">
|
||||
<div style="height: 180px"><!-- 模拟内容高度 --></div>
|
||||
<div style="height: 180px"></div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
@@ -46,7 +74,79 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UploaderFileListItem } from 'vant/es'
|
||||
import { useMobileStore } from '@/stores/mobile'
|
||||
import 'vant/es/dialog/style'
|
||||
|
||||
const uploadFileList = ref<
|
||||
{
|
||||
url: string
|
||||
status: 'uploading' | 'failed' | 'done'
|
||||
message: string
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const afterReadImage = (fileList: UploaderFileListItem | UploaderFileListItem[]) => {
|
||||
const validTypes = ['image/jpeg', 'image/png', 'image/gif']
|
||||
const files = Array.isArray(fileList) ? fileList : [fileList]
|
||||
|
||||
console.log('选择的文件:', files)
|
||||
|
||||
for (const file of files) {
|
||||
const rawFile = file.file
|
||||
|
||||
if (!rawFile) {
|
||||
console.log('文件不存在:', file)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!validTypes.includes(rawFile.type)) {
|
||||
console.log('已过滤非图片文件:', file)
|
||||
if (!Array.isArray(fileList)) {
|
||||
window.$message.warning('只能选择图片哦~')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
uploadFileList.value.push({
|
||||
url: file.url as string,
|
||||
status: 'done',
|
||||
message: '待上传'
|
||||
})
|
||||
|
||||
console.log('已添加文件:', file)
|
||||
}
|
||||
}
|
||||
|
||||
const afterReadFile = (fileList: UploaderFileListItem | UploaderFileListItem[]) => {
|
||||
const imageTypes = ['image/jpeg', 'image/png', 'image/gif']
|
||||
const files = Array.isArray(fileList) ? fileList : [fileList]
|
||||
|
||||
console.log('选择的文件:', files)
|
||||
|
||||
for (const file of files) {
|
||||
const rawFile = file.file
|
||||
|
||||
if (!rawFile) {
|
||||
console.log('文件不存在:', file)
|
||||
continue
|
||||
}
|
||||
|
||||
// ✅ 只保留非图片文件
|
||||
if (imageTypes.includes(rawFile.type)) {
|
||||
console.log('已过滤图片文件:', file)
|
||||
continue
|
||||
}
|
||||
|
||||
uploadFileList.value.push({
|
||||
url: file.url as string,
|
||||
status: 'done',
|
||||
message: '待上传(非图片)'
|
||||
})
|
||||
|
||||
console.log('已选择文件:', file)
|
||||
}
|
||||
}
|
||||
|
||||
// ==== 类型声明(让 send 有类型提示)====
|
||||
interface MsgInputReturn {
|
||||
@@ -109,12 +209,12 @@ const handleSend = async () => {
|
||||
|
||||
// ==== 展开面板 ====
|
||||
const options = ref([
|
||||
{ icon: 'smiling-face', showArrow: false, isRotate: false },
|
||||
{ icon: 'screenshot', showArrow: true, isRotate: false },
|
||||
{ icon: 'file2', showArrow: true, isRotate: false },
|
||||
{ icon: 'photo', showArrow: false, isRotate: false },
|
||||
{ icon: 'voice', showArrow: false, isRotate: false },
|
||||
{ icon: 'history', showArrow: true, isRotate: false }
|
||||
{ label: 'emoji', icon: 'smiling-face', showArrow: true, isRotate: false },
|
||||
{ label: 'cut', icon: 'screenshot', showArrow: true, isRotate: false },
|
||||
{ label: 'file', icon: 'file', showArrow: false, isRotate: true },
|
||||
{ label: 'image', icon: 'photo', showArrow: false, isRotate: true },
|
||||
{ label: 'video', icon: 'voice', showArrow: true, isRotate: false },
|
||||
{ label: 'history', icon: 'history', showArrow: true, isRotate: false }
|
||||
])
|
||||
|
||||
const isPanelVisible = ref(false)
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
<div class="w-full h-full overflow-hidden flex items-center justify-center">
|
||||
<div :class="props.isOfficial ? ['chat-room-name-official'] : ['chat-room-name']">
|
||||
<div class="truncate whitespace-nowrap overflow-hidden text-ellipsis w-full text-center">
|
||||
{{
|
||||
gloabalStore.currentSession.remark ? gloabalStore.currentSession.remark : gloabalStore.currentSession.name
|
||||
}}
|
||||
{{ props.roomName }}
|
||||
</div>
|
||||
<svg v-if="props.isOfficial" class="w-18px h-18px iconpark-icon text-#1A9B83"><use href="#auth"></use></svg>
|
||||
</div>
|
||||
@@ -31,9 +29,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import router from '@/router'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
|
||||
const gloabalStore = useGlobalStore()
|
||||
const props = defineProps({
|
||||
msgCount: {
|
||||
type: Number,
|
||||
@@ -54,6 +50,10 @@ const props = defineProps({
|
||||
enableShadow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
roomName: {
|
||||
type: String,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<n-flex vertical class="select-none">
|
||||
<n-flex
|
||||
v-if="props.closeHeader === true ? false : true"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
class="color-[--text-color] px-20px py-10px">
|
||||
<p class="text-16px">{{ props.type === 'friend' ? '好友通知' : '群通知' }}</p>
|
||||
<svg class="size-18px cursor-pointer">
|
||||
<use href="#delete"></use>
|
||||
</svg>
|
||||
</n-flex>
|
||||
|
||||
<n-virtual-list
|
||||
:style="{
|
||||
maxHeight: props.customHeight
|
||||
? props.customHeight + 'px'
|
||||
: 'max-height: calc(100vh / var(--page-scale, 1) - 80px)'
|
||||
}"
|
||||
:items="applyList"
|
||||
:item-size="87"
|
||||
:item-resizable="true"
|
||||
@scroll="handleScroll"
|
||||
ref="virtualListRef">
|
||||
<template #default="{ item }">
|
||||
<div class="flex gap-2 w-full text-14px mb-15px">
|
||||
<div class="flex h-full">
|
||||
<n-avatar
|
||||
round
|
||||
size="large"
|
||||
:src="
|
||||
avatarSrc(
|
||||
groupStore.getUserInfo(isCurrentUser(item.senderId) ? item.operateId : item.senderId)?.avatar!
|
||||
)
|
||||
" />
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col gap-10px">
|
||||
<div
|
||||
@click="isCurrentUser(item.senderId) ? (currentUserId = item.operateId) : (currentUserId = item.senderId)"
|
||||
class="flex justify-between text-14px text-#2DA38D">
|
||||
{{ groupStore.getUserInfo(isCurrentUser(item.senderId) ? item.operateId : item.senderId)?.name }}
|
||||
</div>
|
||||
<div class="flex justify-between text-gray-500 text-12px">
|
||||
<span>
|
||||
{{ applyMsg(item) }}
|
||||
</span>
|
||||
<span>
|
||||
{{ formatTimestamp(item.createTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-show="isFriendApplyOrGroupInvite(item)" class="flex gap-2 flex-1 text-12px text-gray-500">
|
||||
<div class="whitespace-nowrap">留言:</div>
|
||||
<n-ellipsis :tooltip="true" expand-trigger="click" line-clamp="2" style="max-width: 100%">
|
||||
{{ item.content }}
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-17 max-h-45px flex-col items-center justify-center">
|
||||
<n-flex
|
||||
align="center"
|
||||
:size="10"
|
||||
v-if="item.status === RequestNoticeAgreeStatus.UNTREATED && !isCurrentUser(item.senderId)">
|
||||
<n-button secondary :loading="loadingMap[item.applyId]" @click="handleAgree(item.applyId)">接受</n-button>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="dropdownOptions"
|
||||
@select="(key: string) => handleFriendAction(key, item.applyId)">
|
||||
<n-icon class="cursor-pointer px-6px">
|
||||
<svg class="size-16px color-[--text-color]">
|
||||
<use href="#more"></use>
|
||||
</svg>
|
||||
</n-icon>
|
||||
</n-dropdown>
|
||||
</n-flex>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="dropdownOptions"
|
||||
@select="(key: string) => handleFriendAction(key, item.applyId)"
|
||||
v-if="item.status === RequestNoticeAgreeStatus.UNTREATED && !isCurrentUser(item.senderId)">
|
||||
<n-icon class="cursor-pointer px-6px rounded-10px bg-gray-300 h-50% items-center flex">
|
||||
<svg class="size-16px color-[--text-color]">
|
||||
<use href="#more"></use>
|
||||
</svg>
|
||||
</n-icon>
|
||||
</n-dropdown>
|
||||
<span class="text-(12px #64a29c)" v-else-if="item.status === RequestNoticeAgreeStatus.ACCEPTED">
|
||||
已同意
|
||||
</span>
|
||||
<span class="text-(12px #c14053)" v-else-if="item.status === RequestNoticeAgreeStatus.REJECTED">
|
||||
已拒绝
|
||||
</span>
|
||||
<span class="text-(12px #909090)" v-else-if="item.status === RequestNoticeAgreeStatus.IGNORE">已忽略</span>
|
||||
<span
|
||||
class="text-(12px #64a29c)"
|
||||
:class="{ 'text-(12px #c14053)': item.status === RequestNoticeAgreeStatus.REJECTED }"
|
||||
v-else-if="isCurrentUser(item.senderId)">
|
||||
{{
|
||||
isAccepted(item)
|
||||
? '已同意'
|
||||
: item.status === RequestNoticeAgreeStatus.REJECTED
|
||||
? '对方已拒绝'
|
||||
: '等待验证'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
|
||||
<!-- 空数据提示 -->
|
||||
<n-flex v-if="applyList.length === 0" vertical justify="center" align="center" class="py-40px">
|
||||
<n-empty description="暂无好友申请" />
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { NoticeType, RequestNoticeAgreeStatus } from '@/services/types.ts'
|
||||
import { useContactStore } from '@/stores/contacts.ts'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { formatTimestamp } from '@/utils/ComputedTime.ts'
|
||||
import { useGroupStore } from '~/src/stores/group'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const contactStore = useContactStore()
|
||||
const groupStore = useGroupStore()
|
||||
const currentUserId = ref('0')
|
||||
const loadingMap = ref<Record<string, boolean>>({})
|
||||
const virtualListRef = ref()
|
||||
const isLoadingMore = ref(false)
|
||||
const props = defineProps<{
|
||||
type: 'friend' | 'group'
|
||||
customHeight?: number
|
||||
closeHeader?: boolean
|
||||
}>()
|
||||
|
||||
// 检查好友申请是否已被接受
|
||||
const isAccepted = (item: any) => {
|
||||
// 使用缓存集合快速检查目标用户是否在联系人列表中
|
||||
return item.status !== RequestNoticeAgreeStatus.UNTREATED
|
||||
}
|
||||
|
||||
const applyList = computed(() => {
|
||||
return contactStore.requestFriendsList.filter((item) => {
|
||||
if (props.type === 'friend') {
|
||||
return item.type === 2
|
||||
} else {
|
||||
return item.type === 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 判断是否为好友申请或者群申请、群邀请
|
||||
const isFriendApplyOrGroupInvite = (item: any) => {
|
||||
return (
|
||||
item.eventType === NoticeType.FRIEND_APPLY ||
|
||||
item.eventType === NoticeType.GROUP_INVITE ||
|
||||
item.eventType === NoticeType.GROUP_INVITE_ME ||
|
||||
item.eventType === NoticeType.ADD_ME
|
||||
)
|
||||
}
|
||||
|
||||
const applyMsg = computed(() => (item: any) => {
|
||||
if (props.type === 'friend') {
|
||||
return isCurrentUser(item.senderId) ? (isAccepted(item) ? '已同意你的请求' : '正在验证你的邀请') : '请求加为好友'
|
||||
} else {
|
||||
if (isFriendApplyOrGroupInvite(item)) {
|
||||
return isCurrentUser(item.senderId) ? '已同意加入' + item.content : '邀请你加入' + item.content
|
||||
} else if (item.eventType === NoticeType.GROUP_MEMBER_DELETE) {
|
||||
return '已被' + groupStore.getUserInfo(item.senderId)!.name + '踢出' + item.content
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 下拉菜单选项
|
||||
const dropdownOptions = [
|
||||
{
|
||||
label: '拒绝',
|
||||
key: 'reject'
|
||||
},
|
||||
{
|
||||
label: '忽略',
|
||||
key: 'ignore'
|
||||
}
|
||||
]
|
||||
|
||||
const avatarSrc = (url: string) => AvatarUtils.getAvatarUrl(url)
|
||||
|
||||
// 判断是否为当前登录用户
|
||||
const isCurrentUser = (uid: string) => {
|
||||
return uid === userStore.userInfo!.uid
|
||||
}
|
||||
|
||||
// 处理滚动事件
|
||||
const handleScroll = (e: Event) => {
|
||||
if (isLoadingMore.value) return
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target as HTMLElement
|
||||
// 当滚动到距离底部20px以内时触发加载更多
|
||||
if (scrollHeight - scrollTop - clientHeight < 20) {
|
||||
loadMoreFriendRequests()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多好友申请
|
||||
const loadMoreFriendRequests = async () => {
|
||||
// 如果已经是最后一页或正在加载中,则不再加载
|
||||
if (contactStore.applyPageOptions.isLast) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoadingMore.value = true
|
||||
try {
|
||||
await contactStore.getApplyPage(false)
|
||||
} finally {
|
||||
isLoadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAgree = async (applyId: string) => {
|
||||
loadingMap.value[applyId] = true
|
||||
contactStore
|
||||
.onHandleInvite({
|
||||
applyId,
|
||||
state: 2
|
||||
})
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
loadingMap.value[applyId] = false
|
||||
}, 600)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理好友请求操作(拒绝或忽略)
|
||||
const handleFriendAction = async (action: string, applyId: string) => {
|
||||
loadingMap.value[applyId] = true
|
||||
try {
|
||||
if (action === 'reject') {
|
||||
await contactStore.onHandleInvite({
|
||||
applyId,
|
||||
state: 0
|
||||
})
|
||||
} else if (action === 'ignore') {
|
||||
await contactStore.onHandleInvite({
|
||||
applyId,
|
||||
state: 3
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
loadingMap.value[applyId] = false
|
||||
}, 600)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 组件挂载时刷新一次列表
|
||||
contactStore.getApplyPage(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -7,56 +7,30 @@
|
||||
<div
|
||||
class="self-center h-auto transition-transform duration-300 ease-in-out origin-top"
|
||||
:style="{ transform: props.isShow ? 'scale(1) translateY(0)' : 'scale(0.62) translateY(0px)' }">
|
||||
<n-avatar
|
||||
:size="86"
|
||||
:src="AvatarUtils.getAvatarUrl(userStore.userInfo!.avatar)"
|
||||
fallback-src="/logo.png"
|
||||
round />
|
||||
<n-avatar :size="86" :src="AvatarUtils.getAvatarUrl(userDetailInfo!.avatar)" fallback-src="/logo.png" round />
|
||||
</div>
|
||||
|
||||
<!-- 基本信息栏 -->
|
||||
<div ref="infoBox" class="pl-2 flex gap-8px flex-col transition-transform duration-300 ease-in-out">
|
||||
<!-- 名字与在线状态 -->
|
||||
<div class="flex flex-warp gap-4 items-center">
|
||||
<span class="font-bold text-20px text-#373838">{{ userStore.userInfo!.name }}</span>
|
||||
<div class="bg-#E7EFE6 flex flex-wrap ps-2 items-center rounded-full gap-1 w-50px h-24px">
|
||||
<span class="w-12px h-12px rounded-15px bg-#079669"></span>
|
||||
<span class="text-bold-style" style="font-size: 12px; color: #373838">在线</span>
|
||||
<span class="font-bold text-20px text-#373838">{{ userDetailInfo!.name }}</span>
|
||||
<div class="bg-#E7EFE6 flex flex-wrap ps-2 px-8px items-center rounded-full gap-1 h-24px">
|
||||
<span class="w-12px h-12px rounded-15px flex items-center">
|
||||
<img
|
||||
:src="friendUserState.url ? friendUserState.url : currentState?.url"
|
||||
alt=""
|
||||
class="rounded-50% size-14px" />
|
||||
</span>
|
||||
<span class="text-bold-style" style="font-size: 12px; color: #373838">
|
||||
{{ friendUserState.title ? friendUserState.title : currentState.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 在线状态点 -->
|
||||
<template v-if="!statusIcon">
|
||||
<n-popover trigger="hover" placement="top" :show-arrow="false">
|
||||
<template #trigger>
|
||||
<div
|
||||
@click="openContent('在线状态', 'onlineStatus', 320, 480)"
|
||||
class="z-30 absolute top-72px left-72px cursor-pointer border-(6px solid [--avatar-border-color]) rounded-full size-18px"
|
||||
:class="[activeStatus === OnlineEnum.ONLINE ? 'bg-#1ab292' : 'bg-#909090']"></div>
|
||||
</template>
|
||||
<span>{{ activeStatus === OnlineEnum.ONLINE ? '在线' : '离线' }}</span>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<!-- 独立的状态图标 -->
|
||||
<template v-if="statusIcon">
|
||||
<n-popover trigger="hover" placement="top" :show-arrow="false">
|
||||
<template #trigger>
|
||||
<div class="z-30 absolute top-72px left-72px size-26px bg-[--avatar-border-color] rounded-full">
|
||||
<img
|
||||
:src="statusIcon"
|
||||
@click="openContent('在线状态', 'onlineStatus', 320, 480)"
|
||||
class="p-4px cursor-pointer rounded-full size-18px"
|
||||
alt="" />
|
||||
</div>
|
||||
</template>
|
||||
<span>{{ currentStateTitle }}</span>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<!-- 账号 -->
|
||||
<div class="flex flex-warp gap-2 items-center">
|
||||
<span class="text-bold-style">账号:{{ userStore.userInfo!.account }}</span>
|
||||
<span class="text-bold-style">账号:{{ userDetailInfo!.account }}</span>
|
||||
<span @click="toMyQRCode" class="pe-15px">
|
||||
<img class="w-14px h-14px" src="@/assets/mobile/my/qr-code.webp" alt="" />
|
||||
</span>
|
||||
@@ -68,14 +42,14 @@
|
||||
style="transform: translateZ(0)"
|
||||
class="relative w-118px overflow-hidden">
|
||||
<img class="block w-full" src="@/assets/mobile/my/my-medal.webp" alt="" />
|
||||
<div class="text-10px absolute inset-0 flex ps-2 items-center justify-start text-white font-medium">
|
||||
<div class="text-10px absolute inset-0 flex ps-2 items-center justify-around text-white font-medium">
|
||||
<span class="flex items-center">
|
||||
<template v-if="(userStore.userInfo?.itemIds?.length ?? 0) > 0">
|
||||
<div v-if="(userStore.userInfo?.itemIds?.length ?? 0) > 0">
|
||||
<span class="font-bold">已点亮</span>
|
||||
<span class="medal-number">{{ userStore.userInfo?.itemIds?.length }}</span>
|
||||
<span class="font-bold">枚勋章</span>
|
||||
</template>
|
||||
<span v-else>还没获取任何勋章</span>
|
||||
</div>
|
||||
<span v-else>还没勋章哦~</span>
|
||||
</span>
|
||||
|
||||
<span class="flex ms-3">
|
||||
@@ -99,41 +73,49 @@
|
||||
<div class="flex flex-warp gap-2 items-center">
|
||||
<div class="min-w-10 flex flex-col items-center">
|
||||
<div class="fans-number">920.13W</div>
|
||||
<div class="mt-2 text-bold-style">粉丝</div>
|
||||
<div class="fans-title">粉丝</div>
|
||||
</div>
|
||||
<div class="h-20px w-1px bg-gray-300"></div>
|
||||
<div class="min-w-10 flex flex-col items-center">
|
||||
<div class="fans-number">120</div>
|
||||
<div class="mt-2 text-bold-style">关注</div>
|
||||
<div class="fans-title">关注</div>
|
||||
</div>
|
||||
<div class="h-20px w-1px bg-gray-300"></div>
|
||||
<div class="min-w-10 flex flex-col items-center">
|
||||
<div class="fans-number">43.15W</div>
|
||||
<div class="mt-2 text-bold-style">点赞</div>
|
||||
<div class="fans-title">点赞</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 justify-end flex items-center gap-3">
|
||||
<div
|
||||
<n-button
|
||||
:disabled="loading"
|
||||
@click="toEditProfile"
|
||||
v-if="props.isMyPage"
|
||||
class="font-bold px-4 py-10px bg-#EEF4F3 text-#373838 rounded-full text-12px">
|
||||
编辑资料
|
||||
</div>
|
||||
<div
|
||||
</n-button>
|
||||
<n-button
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="handleDelete"
|
||||
v-if="!props.isMyPage && props.isMyFriend"
|
||||
class="px-4 py-10px font-bold text-center bg-red text-white rounded-full text-12px">
|
||||
- 删除好友
|
||||
</div>
|
||||
<div
|
||||
删除
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
:disabled="loading"
|
||||
v-if="!props.isMyPage && !props.isMyFriend"
|
||||
class="px-4 py-10px font-bold text-center bg-#13987f text-white rounded-full text-12px">
|
||||
+ 添加好友
|
||||
</div>
|
||||
<div
|
||||
</n-button>
|
||||
<n-button
|
||||
@click="toChatRoom"
|
||||
:disabled="loading"
|
||||
v-if="!props.isMyPage"
|
||||
class="px-4 py-10px text-center font-bold bg-#EEF4F3 text-#373838 rounded-full text-12px">
|
||||
私聊
|
||||
</div>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,53 +123,133 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { OnlineEnum } from '@/enums/index.ts'
|
||||
import { showDialog } from 'vant'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useUserStatusStore } from '@/stores/userStatus'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import 'vant/es/dialog/style'
|
||||
import { OnlineEnum } from '@/enums'
|
||||
import { useMessage } from '@/hooks/useMessage.ts'
|
||||
import type { UserInfoType, UserItem } from '@/services/types'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useContactStore } from '@/stores/contacts'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { getSessionDetailWithFriends } from '@/utils/ImRequestUtils'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const userStatusStore = useUserStatusStore()
|
||||
const groupStore = useGroupStore()
|
||||
const route = useRoute()
|
||||
const contactStore = useContactStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
const { preloadChatRoom } = useMessage()
|
||||
|
||||
const uid = route.params.uid as string
|
||||
|
||||
const toChatRoom = async () => {
|
||||
try {
|
||||
const res = await getSessionDetailWithFriends({ id: uid, roomType: 2 })
|
||||
globalStore.updateCurrentSessionRoomId(res.roomId)
|
||||
// 先检查会话是否已存在
|
||||
const existingSession = chatStore.getSession(res.roomId)
|
||||
if (!existingSession) {
|
||||
// 只有当会话不存在时才更新会话列表顺序
|
||||
chatStore.updateSessionLastActiveTime(res.roomId)
|
||||
// 如果会话不存在,需要重新获取会话列表,但保持当前选中的会话
|
||||
await chatStore.getSessionList(true)
|
||||
}
|
||||
await preloadChatRoom(res.roomId)
|
||||
router.push(`/mobile/chatRoom/chatMain`)
|
||||
} catch (error) {
|
||||
console.error('私聊尝试进入聊天室失败:', error)
|
||||
window.$message.error('显示会话失败')
|
||||
}
|
||||
}
|
||||
|
||||
// const toChatRoom = async () => {
|
||||
// try {
|
||||
// // await preloadChatRoom(uid)
|
||||
// // router.push(`/mobile/chatRoom/chatMain`)
|
||||
// } catch (error) {
|
||||
// console.error('尝试进入私聊错误:', error)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 用户详情信息,默认字段只写必要的,不加可能会报错undefined
|
||||
const userDetailInfo = ref<UserItem | UserInfoType | undefined>({
|
||||
activeStatus: OnlineEnum.ONLINE,
|
||||
avatar: '',
|
||||
lastOptTime: 0,
|
||||
name: '',
|
||||
uid: '',
|
||||
account: ''
|
||||
})
|
||||
|
||||
// 这个值只有在查看好友详细信息时才用
|
||||
const friendUserState = ref<any>({
|
||||
title: '',
|
||||
url: ''
|
||||
})
|
||||
|
||||
const { stateList } = storeToRefs(userStatusStore)
|
||||
const activeStatus = ref<OnlineEnum>(OnlineEnum.ONLINE)
|
||||
|
||||
const getUserState = (stateId: string) => {
|
||||
return stateList.value.find((state: { id: string }) => state.id === stateId)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('userStatusStore = ', userStatusStore)
|
||||
|
||||
// 如果地址中的uid存在,那当前就是好友的页面,就从地址栏获取uid拿到好友的详情信息,如果没有就默认显示自己的信息
|
||||
if (uid) {
|
||||
const foundedUser = groupStore.allUserInfo.find((i) => i.uid === uid)
|
||||
userDetailInfo.value = foundedUser
|
||||
if (foundedUser?.userStateId) {
|
||||
const state = getUserState(foundedUser.userStateId)
|
||||
friendUserState.value = state
|
||||
}
|
||||
} else {
|
||||
userDetailInfo.value = userStore.userInfo
|
||||
}
|
||||
})
|
||||
|
||||
const currentState = computed(() => userStatusStore.currentState)
|
||||
|
||||
const animatedBox = ref<HTMLElement | null>(null)
|
||||
const statusIcon = computed(() => {
|
||||
const userStateId = userStore.userInfo?.userStateId
|
||||
|
||||
// 如果在线且有特殊状态
|
||||
if (userStateId && userStateId !== '1') {
|
||||
const state = stateList.value.find((s: { id: string }) => s.id === userStateId)
|
||||
if (state) {
|
||||
return state.url
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
const loading = ref(false)
|
||||
|
||||
// 计算当前状态的标题
|
||||
const currentStateTitle = computed(() => {
|
||||
const userStateId = userStore.userInfo?.userStateId
|
||||
|
||||
if (userStateId && userStateId !== '1') {
|
||||
const state = stateList.value.find((s: { id: string }) => s.id === userStateId)
|
||||
if (state) {
|
||||
return state.title
|
||||
}
|
||||
}
|
||||
return activeStatus.value === OnlineEnum.ONLINE ? '在线' : '离线'
|
||||
})
|
||||
|
||||
/**
|
||||
* 打开内容对应窗口 [安卓版本]
|
||||
* @param title 窗口的标题
|
||||
* @param label 窗口的标识
|
||||
* @param w 窗口的宽度
|
||||
* @param h 窗口的高度
|
||||
* */
|
||||
const openContent = (_title: string, _label: string, _w = 840, _h = 600) => {
|
||||
console.log('安卓的弹窗')
|
||||
const handleDelete = () => {
|
||||
showDialog({
|
||||
title: '删除好友',
|
||||
message: '确定删除该好友吗?',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '取消',
|
||||
cancelButtonText: '确定'
|
||||
})
|
||||
.then(() => {})
|
||||
.catch(async () => {
|
||||
if (userDetailInfo.value?.uid) {
|
||||
try {
|
||||
loading.value = true
|
||||
await contactStore.onDeleteContact(userDetailInfo.value.uid)
|
||||
window.$message.success('已删除好友')
|
||||
} catch (error) {
|
||||
window.$message.warning('删除失败')
|
||||
console.error('删除好友失败:', error)
|
||||
}
|
||||
} else {
|
||||
window.$message.warning('没有找到好友哦')
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const toEditProfile = () => {
|
||||
@@ -337,6 +399,14 @@ $font-family-sans: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
.fans-number {
|
||||
font-size: $text-font-size-base;
|
||||
font-family: $font-family-system, $font-family-windows, $font-family-chinese, $font-family-sans;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fans-title {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 13px;
|
||||
font-family: $font-family-system, $font-family-windows, $font-family-sans;
|
||||
color: #757775;
|
||||
}
|
||||
|
||||
.custom-rounded {
|
||||
|
||||
@@ -30,6 +30,7 @@ import type { MessageType } from '@/services/types'
|
||||
import { WsResponseMessageType } from '@/services/wsType'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useSettingStore } from '@/stores/setting'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { audioManager } from '@/utils/AudioManager'
|
||||
import { invokeSilently } from '@/utils/TauriInvokeHandler'
|
||||
@@ -39,8 +40,14 @@ const route = useRoute()
|
||||
const chatStore = useChatStore()
|
||||
const userStore = useUserStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const settingStore = useSettingStore()
|
||||
const userUid = computed(() => userStore.userInfo!.uid)
|
||||
const playMessageSound = async () => {
|
||||
// 检查是否开启了消息提示音
|
||||
if (!settingStore.notification?.messageSound) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const audio = new Audio('/sound/message.mp3')
|
||||
await audioManager.play(audio, 'message-notification')
|
||||
|
||||
+117
-2
@@ -26,28 +26,40 @@
|
||||
<script setup lang="ts">
|
||||
import { emitTo } from '@tauri-apps/api/event'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { info } from '@tauri-apps/plugin-log'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { useRoute } from 'vue-router'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
import type { default as TabBarType } from '#/layout/tabBar/index.vue'
|
||||
import TabBar from '#/layout/tabBar/index.vue'
|
||||
import { NotificationTypeEnum, TauriCommand } from '@/enums'
|
||||
import { ChangeTypeEnum, NotificationTypeEnum, TauriCommand } from '@/enums'
|
||||
import { useMitt } from '@/hooks/useMitt'
|
||||
import type { MessageType } from '@/services/types'
|
||||
import type { MessageType, UserItem } from '@/services/types'
|
||||
import { WsResponseMessageType } from '@/services/wsType'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { useSettingStore } from '@/stores/setting'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { audioManager } from '@/utils/AudioManager'
|
||||
import { invokeSilently } from '@/utils/TauriInvokeHandler'
|
||||
import { useContactStore } from '~/src/stores/contacts'
|
||||
|
||||
const route = useRoute()
|
||||
const tabBarElement = ref<InstanceType<typeof TabBarType>>()
|
||||
const chatStore = useChatStore()
|
||||
const userStore = useUserStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const groupStore = useGroupStore()
|
||||
const contactStore = useContactStore()
|
||||
const settingStore = useSettingStore()
|
||||
const userUid = computed(() => userStore.userInfo!.uid)
|
||||
const playMessageSound = async () => {
|
||||
// 检查是否开启了消息提示音
|
||||
if (!settingStore.notification?.messageSound) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const audio = new Audio('/sound/message.mp3')
|
||||
await audioManager.play(audio, 'message-notification')
|
||||
@@ -56,6 +68,109 @@ const playMessageSound = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理自己加入群聊
|
||||
const handleSelfAdd = async (roomId: string) => {
|
||||
info('本人加入群聊,加载该群聊的会话数据')
|
||||
await chatStore.addSession(roomId)
|
||||
}
|
||||
|
||||
// 处理其他成员加入群聊
|
||||
const handleOtherMemberAdd = async (user: UserItem, roomId: string) => {
|
||||
info('群成员加入群聊,添加群成员数据')
|
||||
groupStore.addUserItem(user, roomId)
|
||||
}
|
||||
|
||||
// 处理群成员添加
|
||||
const handleMemberAdd = async (userList: UserItem[], roomId: string) => {
|
||||
for (const user of userList) {
|
||||
if (isSelfUser(user.uid)) {
|
||||
await handleSelfAdd(roomId)
|
||||
} else {
|
||||
await handleOtherMemberAdd(user, roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useMitt.on(
|
||||
WsResponseMessageType.WS_MEMBER_CHANGE,
|
||||
async (param: {
|
||||
roomId: string
|
||||
changeType: ChangeTypeEnum
|
||||
userList: UserItem[]
|
||||
totalNum: number
|
||||
onlineNum: number
|
||||
}) => {
|
||||
info('监听到群成员变更消息')
|
||||
const isRemoveAction = param.changeType === ChangeTypeEnum.REMOVE || param.changeType === ChangeTypeEnum.EXIT_GROUP
|
||||
if (isRemoveAction) {
|
||||
await handleMemberRemove(param.userList, param.roomId)
|
||||
} else {
|
||||
await handleMemberAdd(param.userList, param.roomId)
|
||||
}
|
||||
|
||||
groupStore.addGroupDetail(param.roomId)
|
||||
// 更新群内的总人数
|
||||
groupStore.updateGroupNumber(param.roomId, param.totalNum, param.onlineNum)
|
||||
}
|
||||
)
|
||||
|
||||
// 检查是否为当前用户
|
||||
const isSelfUser = (uid: string): boolean => {
|
||||
return uid === userStore.userInfo!.uid
|
||||
}
|
||||
|
||||
// 处理自己被移除
|
||||
const handleSelfRemove = async (roomId: string) => {
|
||||
info('本人退出群聊,移除会话数据')
|
||||
|
||||
// 移除会话和群成员数据
|
||||
chatStore.removeSession(roomId)
|
||||
groupStore.removeAllUsers(roomId)
|
||||
|
||||
// 如果当前会话就是被移除的群聊,切换到其他会话
|
||||
if (globalStore.currentSession?.roomId === roomId) {
|
||||
globalStore.updateCurrentSessionRoomId(chatStore.sessionList[0].roomId)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他成员被移除
|
||||
const handleOtherMemberRemove = async (uid: string, roomId: string) => {
|
||||
info('群成员退出群聊,移除群内的成员数据')
|
||||
groupStore.removeUserItem(uid, roomId)
|
||||
}
|
||||
|
||||
useMitt.on(WsResponseMessageType.REQUEST_APPROVAL_FRIEND, async () => {
|
||||
// 刷新好友列表以获取最新状态
|
||||
await contactStore.getContactList(true)
|
||||
})
|
||||
useMitt.on(WsResponseMessageType.ROOM_INFO_CHANGE, async (data: { roomId: string; name: string; avatar: string }) => {
|
||||
// 根据roomId修改对应房间中的群名称和群头像
|
||||
const { roomId, name, avatar } = data
|
||||
|
||||
// 更新chatStore中的会话信息
|
||||
chatStore.updateSession(roomId, {
|
||||
name,
|
||||
avatar
|
||||
})
|
||||
})
|
||||
|
||||
// 处理群成员移除
|
||||
const handleMemberRemove = async (userList: UserItem[], roomId: string) => {
|
||||
for (const user of userList) {
|
||||
if (isSelfUser(user.uid)) {
|
||||
await handleSelfRemove(roomId)
|
||||
} else {
|
||||
await handleOtherMemberRemove(user.uid, roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useMitt.on(WsResponseMessageType.USER_STATE_CHANGE, async (data: { uid: string; userStateId: string }) => {
|
||||
groupStore.updateUserItem(data.uid, {
|
||||
userStateId: data.userStateId
|
||||
})
|
||||
})
|
||||
|
||||
/** 测试 */
|
||||
useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => {
|
||||
console.log('[mobile/layout] 收到的消息:', data)
|
||||
|
||||
@@ -15,22 +15,35 @@
|
||||
<div class="flex flex-col gap-15px py-15px px-20px">
|
||||
<div class="flex shadow bg-white rounded-10px w-full h-60px items-center gap-10px">
|
||||
<!-- 群头像 -->
|
||||
<div class="ms-15px self-center h-38px bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<n-badge>
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="rounded-full relative bg-white w-38px h-38px overflow-hidden"
|
||||
@click="openAvatarCropper(userStore.userInfo?.avatarUpdateTime)">
|
||||
<n-avatar
|
||||
:size="40"
|
||||
:src="AvatarUtils.getAvatarUrl(globalStore.currentSession.avatar)"
|
||||
class="absolute"
|
||||
:size="86"
|
||||
:src="AvatarUtils.getAvatarUrl(globalStore.currentSession.avatar!)"
|
||||
fallback-src="/logo.png"
|
||||
round />
|
||||
</n-badge>
|
||||
<div
|
||||
class="absolute h-50% w-full bottom-0 bg-[rgb(50,50,50)] bg-clip-padding backdrop-filter backdrop-blur-sm bg-opacity-15 backdrop-saturate-100 backdrop-contrast-100"></div>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
class="hidden"
|
||||
@change="handleFileChange" />
|
||||
<AvatarCropper
|
||||
ref="cropperRef"
|
||||
v-model:show="showCropper"
|
||||
:image-url="localImageUrl"
|
||||
@crop="handleCrop" />
|
||||
</div>
|
||||
|
||||
<div class="text-14px flex items-center h-full gap-5px">
|
||||
<span>
|
||||
{{
|
||||
globalStore.currentSession.remark
|
||||
? globalStore.currentSession.remark
|
||||
: globalStore.currentSession.name
|
||||
}}
|
||||
{{ globalStore.currentSession.name }}
|
||||
</span>
|
||||
<span>
|
||||
<svg class="w-18px h-18px iconpark-icon text-#1A9B83">
|
||||
@@ -46,7 +59,7 @@
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-14px">群聊成员</div>
|
||||
<div class="text-12px text-#6E6E6E flex flex-wrap gap-10px items-center">
|
||||
<div>{{ memberNum || 0 }}</div>
|
||||
<div>{{ groupStore.countInfo?.memberNum || 0 }}</div>
|
||||
<div>
|
||||
<svg class="w-14px h-14px iconpark-icon">
|
||||
<use href="#right"></use>
|
||||
@@ -137,7 +150,7 @@
|
||||
style="border-bottom: 1px solid; border-color: #ebebeb"
|
||||
@click="handleCopy(globalStore.currentSession.account)"
|
||||
class="flex justify-between py-15px items-center">
|
||||
<div class="text-14px">群号/二维码</div>
|
||||
<div class="text-14px">{{ isGroup ? '群号/二维码' : 'Hula号/二维码' }}</div>
|
||||
<div class="text-12px text-#6E6E6E flex flex-wrap gap-10px items-center">
|
||||
<div>{{ globalStore.currentSession.account }}</div>
|
||||
<div>
|
||||
@@ -152,8 +165,25 @@
|
||||
<div @click="goToNotice" class="pt-15px flex flex-col text-14px gap-10px">
|
||||
<div>群公告</div>
|
||||
<div class="text-#707070 line-clamp-2 text-12px line-height-20px">
|
||||
静夜思·李白 床前明月光,疑是地上霜。 举头望明月,低头思故乡。静夜思·李白 床前明月光,疑是地上霜。
|
||||
举头望明月,低头思故乡。
|
||||
{{ announList.length > 0 ? announList[0]?.content : '' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLord || isAdmin" class="flex justify-between py-15px items-center">
|
||||
<div class="text-14px">本群昵称</div>
|
||||
<div class="text-12px text-#6E6E6E flex flex-wrap gap-10px items-center">
|
||||
<input
|
||||
style="
|
||||
height: 17px;
|
||||
border: none;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
"
|
||||
v-model="nameValue"
|
||||
@blur="handleGroupInfoUpdate"
|
||||
placeholder="请输入群昵称" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -169,7 +199,9 @@
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
"
|
||||
placeholder="请输入群昵称" />
|
||||
v-model="nicknameValue"
|
||||
@blur="handleInfoUpdate"
|
||||
placeholder="请输入我的群昵称" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,34 +209,19 @@
|
||||
<!-- 群备注 -->
|
||||
<div class="w-full flex flex-col gap-15px rounded-10px">
|
||||
<div class="ps-15px text-14px">
|
||||
<span v-if="isEditingRemark">群备注</span>
|
||||
<span v-if="isEditingRemark" class="text-#6E6E6E">(仅自己可见)</span>
|
||||
|
||||
<div v-if="isEditingRemark" class="flex items-center">
|
||||
<n-input
|
||||
ref="remarkInputRef"
|
||||
v-model:value="item.remark"
|
||||
size="tiny"
|
||||
class="border-(1px solid #90909080)"
|
||||
placeholder="请输入群聊备注"
|
||||
clearable
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
@blur="handleRemarkUpdate"
|
||||
@keydown.enter="handleRemarkUpdate" />
|
||||
</div>
|
||||
<span v-else class="cursor-pointer text-gray-200 text-14px text-left" @click="startEditRemark">
|
||||
{{ item?.remark || '设置群聊备注' }} (仅自己可见)
|
||||
</span>
|
||||
<span>群备注</span>
|
||||
<span class="text-#6E6E6E">(仅自己可见)</span>
|
||||
</div>
|
||||
<!-- <div class="rounded-10px flex w-full bg-white shadow">
|
||||
<div class="rounded-10px flex w-full bg-white shadow">
|
||||
<div class="w-full px-15px">
|
||||
<input class="h-50px w-full" style="border: none; outline: none; font-size: 14px"
|
||||
placeholder="请输入群备注" />
|
||||
<input
|
||||
v-model="remarkValue"
|
||||
class="h-50px w-full"
|
||||
style="border: none; outline: none; font-size: 14px"
|
||||
placeholder="请输入群备注"
|
||||
@blur="handleInfoUpdate" />
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex bg-white rounded-10px w-full h-auto shadow">
|
||||
<div class="px-15px flex flex-col w-full">
|
||||
@@ -214,19 +231,23 @@
|
||||
style="border-bottom: 1px solid; border-color: #ebebeb"
|
||||
class="flex justify-between py-12px items-center">
|
||||
<div class="text-14px">设置为置顶</div>
|
||||
<n-switch v-model:value="active1" />
|
||||
<n-switch :value="activeItem.top" @update:value="handleTop" />
|
||||
</div>
|
||||
<div
|
||||
style="border-bottom: 1px solid; border-color: #ebebeb"
|
||||
class="flex justify-between py-12px items-center">
|
||||
<div class="text-14px">消息免打扰</div>
|
||||
<n-switch v-model:value="active2" />
|
||||
<n-switch :value="activeItem.muteNotification === NotificationTypeEnum.NOT_DISTURB" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex shadow bg-white cursor-pointer text-red text-14px rounded-10px w-full">
|
||||
<div class="p-15px">删除聊天记录</div>
|
||||
</div>
|
||||
<!-- 解散群聊按钮 -->
|
||||
<div class="mt-auto flex justify-center mb-20px">
|
||||
<n-button type="error" @click="handleExit">{{ isGroup ? '解散群聊' : '退出群聊' }}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -234,30 +255,65 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RoomTypeEnum } from '@/enums'
|
||||
import { MittEnum, NotificationTypeEnum, RoleEnum, RoomTypeEnum } from '@/enums'
|
||||
import { useAvatarUpload } from '@/hooks/useAvatarUpload'
|
||||
import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import router from '@/router'
|
||||
import type { UserItem } from '@/services/types'
|
||||
import { useCachedStore } from '@/stores/cached'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { getGroupDetail } from '@/utils/ImRequestUtils'
|
||||
import { getGroupDetail, setSessionTop, updateRoomInfo } from '@/utils/ImRequestUtils'
|
||||
|
||||
const dialog = useDialog()
|
||||
const userStore = useUserStore()
|
||||
const chatStore = useChatStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const groupStore = useGroupStore()
|
||||
const cacheStore = useCachedStore()
|
||||
|
||||
// TODO
|
||||
const memberNum = computed(() => globalStore.currentSession.memberNum)
|
||||
const isGroup = computed(() => globalStore.currentSession?.type === RoomTypeEnum.GROUP)
|
||||
const isLord = computed(() => {
|
||||
const currentUser = groupStore.userList.find((user) => user.uid === useUserStore().userInfo?.uid)
|
||||
return currentUser?.roleId === RoleEnum.LORD
|
||||
})
|
||||
const isAdmin = computed(() => {
|
||||
const currentUser = groupStore.userList.find((user) => user.uid === useUserStore().userInfo?.uid)
|
||||
return currentUser?.roleId === RoleEnum.ADMIN
|
||||
})
|
||||
|
||||
const announError = ref(false)
|
||||
const announNum = ref(0)
|
||||
const isAddAnnoun = ref(false)
|
||||
const announList = ref<any[]>([])
|
||||
const remarkValue = ref('')
|
||||
const item = ref<any>(null)
|
||||
const active1 = ref(false)
|
||||
const active2 = ref(false)
|
||||
const nameValue = ref('')
|
||||
const avatarValue = ref('')
|
||||
const nicknameValue = ref('')
|
||||
const options = ref<Array<{ name: string; src: string }>>([])
|
||||
// 编辑群备注相关状态
|
||||
const isEditingRemark = ref(false)
|
||||
const remarkInputRef = useTemplateRef('remarkInputRef')
|
||||
const { currentSession: activeItem } = storeToRefs(globalStore)
|
||||
|
||||
const {
|
||||
fileInput,
|
||||
localImageUrl,
|
||||
showCropper,
|
||||
cropperRef,
|
||||
openAvatarCropper,
|
||||
handleFileChange,
|
||||
handleCrop: onCrop
|
||||
} = useAvatarUpload({
|
||||
onSuccess: async (downloadUrl) => {
|
||||
avatarValue.value = downloadUrl
|
||||
}
|
||||
})
|
||||
|
||||
const handleCrop = async (cropBlob: Blob) => {
|
||||
await onCrop(cropBlob)
|
||||
}
|
||||
|
||||
const handleCopy = (val: string) => {
|
||||
if (val) {
|
||||
@@ -266,27 +322,150 @@ const handleCopy = (val: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编辑群备注
|
||||
const startEditRemark = () => {
|
||||
isEditingRemark.value = true
|
||||
nextTick(() => {
|
||||
remarkInputRef.value?.focus()
|
||||
const goToNotice = () => {
|
||||
router.push({
|
||||
path: '/mobile/chatRoom/notice',
|
||||
query: {
|
||||
announList: JSON.stringify(announList.value),
|
||||
roomId: globalStore.currentSession.roomId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goToNotice = () => {
|
||||
router.push('/mobile/chatRoom/notice')
|
||||
// 退出登录逻辑
|
||||
async function handleExit() {
|
||||
dialog.error({
|
||||
title: '提示',
|
||||
content: isGroup.value ? '确定要解散群聊吗?' : '确定要退出群聊吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
if (isGroup.value) {
|
||||
if (isLord.value) {
|
||||
if (activeItem.value.roomId === '1') {
|
||||
window.$message.warning('无法解散频道')
|
||||
return
|
||||
}
|
||||
|
||||
groupStore.exitGroup(activeItem.value.roomId).then(() => {
|
||||
window.$message.success('已解散群聊')
|
||||
// 删除当前的会话
|
||||
useMitt.emit(MittEnum.DELETE_SESSION, activeItem.value.roomId)
|
||||
})
|
||||
} else {
|
||||
if (activeItem.value.roomId === '1') {
|
||||
window.$message.warning('无法退出频道')
|
||||
return
|
||||
}
|
||||
|
||||
groupStore.exitGroup(activeItem.value.roomId).then(() => {
|
||||
window.$message.success('已退出群聊')
|
||||
// 删除当前的会话
|
||||
useMitt.emit(MittEnum.DELETE_SESSION, activeItem.value.roomId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
router.push('/mobile/login')
|
||||
} catch (error) {
|
||||
console.error('创建登录窗口失败:', error)
|
||||
}
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
console.log('用户点击了取消')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 判断当前用户是否拥有id为6的徽章 并且是频道 */
|
||||
const hasBadge6 = computed(() => {
|
||||
// 只有当 roomId 为 "1" 时才进行徽章判断(频道)
|
||||
if (globalStore.currentSession?.roomId !== '1') return false
|
||||
|
||||
const currentUser = groupStore.getUserInfo(userStore.userInfo!.uid!)!
|
||||
return currentUser?.itemIds?.includes('6')
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始化群公告所需要的信息
|
||||
*/
|
||||
const handleInitAnnoun = async () => {
|
||||
// 初始化时获取群公告
|
||||
if (isGroup.value) {
|
||||
const roomId = globalStore.currentSession?.roomId
|
||||
if (roomId) {
|
||||
await handleLoadGroupAnnoun(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载群公告
|
||||
*/
|
||||
const handleLoadGroupAnnoun = async (roomId: string) => {
|
||||
try {
|
||||
// 设置是否可以添加公告
|
||||
isAddAnnoun.value = isLord.value || isAdmin.value || hasBadge6.value!
|
||||
// 获取群公告列表
|
||||
const data = await cacheStore.getGroupAnnouncementList(roomId, 1, 10)
|
||||
if (data) {
|
||||
announList.value = data.records
|
||||
// 处理置顶公告
|
||||
if (announList.value && announList.value.length > 0) {
|
||||
const topAnnouncement = announList.value.find((item: any) => item.top)
|
||||
if (topAnnouncement) {
|
||||
announList.value = [topAnnouncement, ...announList.value.filter((item: any) => !item.top)]
|
||||
}
|
||||
}
|
||||
announNum.value = parseInt(data.total, 10)
|
||||
announError.value = false
|
||||
} else {
|
||||
announError.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载群公告失败:', error)
|
||||
announError.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/** 置顶 */
|
||||
const handleTop = (value: boolean) => {
|
||||
setSessionTop({ roomId: activeItem.value.roomId, top: value })
|
||||
.then(() => {
|
||||
// 更新本地会话状态
|
||||
chatStore.updateSession(activeItem.value.roomId, { top: value })
|
||||
window.$message.success(value ? '已置顶' : '已取消置顶')
|
||||
})
|
||||
.catch(() => {
|
||||
window.$message.error('置顶失败')
|
||||
})
|
||||
}
|
||||
|
||||
// 处理群备注更新
|
||||
const handleRemarkUpdate = async () => {
|
||||
const handleInfoUpdate = async () => {
|
||||
await cacheStore.updateMyRoomInfo({
|
||||
id: item.value!.roomId,
|
||||
remark: item.value!.remark,
|
||||
myName: item.value!.myName || ''
|
||||
id: globalStore.currentSession.roomId,
|
||||
remark: remarkValue.value,
|
||||
myName: nicknameValue.value
|
||||
})
|
||||
window.$message.success('群备注更新成功')
|
||||
isEditingRemark.value = false
|
||||
}
|
||||
|
||||
// 处理群名称更新
|
||||
const handleGroupInfoUpdate = async () => {
|
||||
await updateRoomInfo({
|
||||
id: activeItem.value.roomId,
|
||||
name: nameValue.value,
|
||||
avatar: avatarValue.value
|
||||
})
|
||||
// 更新本地会话状态
|
||||
// chatStore.updateSession(activeItem.value.roomId, {
|
||||
// avatar: ''
|
||||
// })
|
||||
|
||||
activeItem.value.avatar = avatarValue.value
|
||||
window.$message.success('群信息已更新')
|
||||
}
|
||||
|
||||
// 获取群组详情和成员信息
|
||||
@@ -312,14 +491,15 @@ const fetchGroupMembers = async (roomId: string) => {
|
||||
* 这里直接监听状态的值
|
||||
*/
|
||||
onMounted(async () => {
|
||||
console.log('globalStore.currentSession --> ', globalStore.currentSession)
|
||||
if (globalStore.currentSession.type === RoomTypeEnum.SINGLE) {
|
||||
item.value = groupStore.getUserInfo(globalStore.currentSession.detailId)!
|
||||
} else {
|
||||
await getGroupDetail(globalStore.currentSession.detailId)
|
||||
await handleInitAnnoun()
|
||||
if (isGroup.value) {
|
||||
await getGroupDetail(globalStore.currentSession.roomId)
|
||||
.then((response: any) => {
|
||||
item.value = response
|
||||
nameValue.value = response.groupName || ''
|
||||
avatarValue.value = response.avatar
|
||||
nicknameValue.value = response.myName || ''
|
||||
remarkValue.value = response.remark || ''
|
||||
if (item.value && item.value.roomId) {
|
||||
fetchGroupMembers(item.value.roomId)
|
||||
}
|
||||
@@ -327,8 +507,10 @@ onMounted(async () => {
|
||||
.catch((e: any) => {
|
||||
console.error('获取群组详情失败:', e)
|
||||
})
|
||||
} else {
|
||||
// 这里需要拿到好友的信息
|
||||
groupStore.getUserInfo(globalStore.currentSession.detailId)!
|
||||
}
|
||||
console.log('item --> ', item)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<AutoFixHeightPage>
|
||||
<template #header>
|
||||
<HeaderBar ref="header" :room-name="globalStore.currentSession!.name" :msg-count="1002" />
|
||||
<HeaderBar
|
||||
ref="header"
|
||||
:room-name="
|
||||
globalStore.currentSession.remark ? globalStore.currentSession.remark : globalStore.currentSession.name
|
||||
"
|
||||
:msg-count="1002" />
|
||||
</template>
|
||||
<template #container>
|
||||
<ChatMain />
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% relative top-0 z-1" alt="hula" />
|
||||
<div :style="{ height: height + 'px' }" class="z-2 absolute flex flex-col overflow-auto min-h-70vh w-full">
|
||||
<div class="flex flex-col p-20px">
|
||||
<RecycleScroller :items="testData" :item-size="15" key-field="id" class="flex flex-col gap-15px">
|
||||
<RecycleScroller :items="announList" :item-size="15" key-field="id" class="flex flex-col gap-15px">
|
||||
<template #default="{ item }">
|
||||
<!-- 公告内容块 -->
|
||||
<div @click="goToNoticeDetail(item.id)" class="shadow flex p-15px bg-white rounded-10px">
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="flex items-center justify-between text-14px">
|
||||
<span class="flex gap-5px">
|
||||
<span class="text-#717171">发布人:</span>
|
||||
<span class="text-black">卡仔</span>
|
||||
<span class="text-black">{{ groupStore.getUserInfo(item.uid)?.name }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="item.isTop"
|
||||
@@ -33,14 +33,12 @@
|
||||
</div>
|
||||
<!-- 公告内容 -->
|
||||
<div class="text-14px line-clamp-3 line-height-20px text-#717171 max-h-60px">
|
||||
静夜思·李白 床前明月光,疑是地上霜。 举头望明月,低头思故乡。 静夜思·李白 床前明月光,疑是地上霜。
|
||||
举头望明月,低头思故乡。 静夜思·李白 床前明月光,疑是地上霜。 举头望明月,低头思故乡。 静夜思·李白
|
||||
床前明月光,疑是地上霜。 举头望明月,低头思故乡。
|
||||
{{ item.content }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-12px">
|
||||
<span class="flex gap-5px text-#717171">2025年8月13日 17:25</span>
|
||||
<span class="text-#13987F">3人已读</span>
|
||||
<span class="flex gap-5px text-#717171">{{ formatTimestamp(item.createTime) }}</span>
|
||||
<span class="text-#13987F">128人已读</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,30 +51,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
import router from '@/router'
|
||||
|
||||
const testData = computed(() => {
|
||||
const tempList = []
|
||||
for (let i = 0; i < 200; i++) {
|
||||
tempList.push({
|
||||
id: i,
|
||||
msg: i,
|
||||
isTop: i % 15 === 0
|
||||
})
|
||||
}
|
||||
const topList = tempList.filter((item) => item.isTop)
|
||||
const notTopList = tempList.filter((item) => !item.isTop)
|
||||
|
||||
const sortedList = topList.concat(notTopList)
|
||||
return sortedList
|
||||
})
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { formatTimestamp } from '@/utils/ComputedTime.ts'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const announList = ref<any[]>([])
|
||||
const groupStore = useGroupStore()
|
||||
const goToNoticeDetail = (id: string) => {
|
||||
// 跳转到公告编辑页面
|
||||
console.log(`跳转到公告编辑页面,公告ID: ${id}`)
|
||||
router.push(`/mobile/chatRoom/notice/detail/${id}`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
announList.value = JSON.parse(route.query.announList as string)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -161,8 +161,8 @@ import { useSettingStore } from '@/stores/setting'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { searchFriend, searchGroup } from '@/utils/ImRequestUtils'
|
||||
import router from '~/src/router'
|
||||
|
||||
// const { createWebviewWindow } = useWindow()
|
||||
const contactStore = useContactStore()
|
||||
const userStore = useUserStore()
|
||||
const globalStore = useGlobalStore()
|
||||
@@ -398,15 +398,15 @@ const handleButtonClick = (item: any) => {
|
||||
// 处理添加好友或群聊
|
||||
const handleAddFriend = async (item: any) => {
|
||||
if (searchType.value === 'user' || searchType.value === 'recommend') {
|
||||
// await createWebviewWindow('申请加好友', 'addFriendVerify', 380, 300, '', false, 380, 300)
|
||||
globalStore.addFriendModalInfo.show = true
|
||||
globalStore.addFriendModalInfo.uid = item.uid
|
||||
|
||||
router.push('/mobile/mobileFriends/confirmAddFriend')
|
||||
} else {
|
||||
// await createWebviewWindow('申请加群', 'addGroupVerify', 380, 400, '', false, 380, 400)
|
||||
globalStore.addGroupModalInfo.show = true
|
||||
globalStore.addGroupModalInfo.account = item.account
|
||||
globalStore.addGroupModalInfo.name = item.name
|
||||
globalStore.addGroupModalInfo.avatar = item.avatar
|
||||
|
||||
router.push('/mobile/mobileFriends/confirmAddGroup')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="flex flex-1 flex-col">
|
||||
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% absolute top-0 z-1" alt="hula" />
|
||||
<AutoFixHeightPage :show-footer="false">
|
||||
<template #header>
|
||||
<HeaderBar
|
||||
:isOfficial="false"
|
||||
:hidden-right="true"
|
||||
:enable-default-background="false"
|
||||
:enable-shadow="false"
|
||||
room-name="添加好友" />
|
||||
</template>
|
||||
|
||||
<template #container="{ height }">
|
||||
<div
|
||||
:style="{ height: height + 'px' }"
|
||||
class="z-2 absolute flex flex-col gap-1 overflow-auto min-h-70vh w-full">
|
||||
<!-- 内容区域 -->
|
||||
<div class="w-full h-full box-border flex flex-col">
|
||||
<n-flex vertical justify="center" :size="20" class="p-[55px_20px] m-20px rounded-15px bg-white">
|
||||
<n-flex align="center" justify="center" :size="20">
|
||||
<n-avatar round size="large" :src="avatarSrc" />
|
||||
|
||||
<n-flex vertical :size="10">
|
||||
<p class="text-[--text-color]">{{ userInfo.name }}</p>
|
||||
<p class="text-(12px [--text-color])">账号: {{ userInfo.account }}</p>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<n-input
|
||||
v-model:value="requestMsg"
|
||||
:allow-input="(value: string) => !value.startsWith(' ') && !value.endsWith(' ')"
|
||||
:autosize="{
|
||||
minRows: 3,
|
||||
maxRows: 3
|
||||
}"
|
||||
:maxlength="60"
|
||||
:count-graphemes="countGraphemes"
|
||||
show-count
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
type="textarea"
|
||||
placeholder="输入几句话,对TA说些什么吧" />
|
||||
|
||||
<n-button class="mt-30px" color="#13987f" @click="addFriend">添加好友</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AutoFixHeightPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCommon } from '@/hooks/useCommon.ts'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { sendAddFriendRequest } from '@/utils/ImRequestUtils'
|
||||
import router from '~/src/router'
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const userStore = useUserStore()
|
||||
const groupStore = useGroupStore()
|
||||
const { countGraphemes } = useCommon()
|
||||
const userInfo = ref(groupStore.getUserInfo(globalStore.addFriendModalInfo.uid!)!)
|
||||
const avatarSrc = computed(() => AvatarUtils.getAvatarUrl(userInfo.value!.avatar as string))
|
||||
const requestMsg = ref()
|
||||
|
||||
watch(
|
||||
() => globalStore.addFriendModalInfo.uid,
|
||||
(newUid) => {
|
||||
userInfo.value = groupStore.getUserInfo(newUid!)!
|
||||
}
|
||||
)
|
||||
|
||||
const addFriend = async () => {
|
||||
await sendAddFriendRequest({
|
||||
msg: requestMsg.value,
|
||||
targetUid: globalStore.addFriendModalInfo.uid as string
|
||||
})
|
||||
window.$message.success('已发送好友申请')
|
||||
setTimeout(() => {
|
||||
router.push('/mobile/message')
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
console.log(userInfo.value)
|
||||
requestMsg.value = `我是${userStore.userInfo!.name}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="flex flex-1 flex-col">
|
||||
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% absolute top-0 z-1" alt="hula" />
|
||||
<AutoFixHeightPage :show-footer="false">
|
||||
<template #header>
|
||||
<HeaderBar
|
||||
:isOfficial="false"
|
||||
:hidden-right="true"
|
||||
:enable-default-background="false"
|
||||
:enable-shadow="false"
|
||||
room-name="添加好友" />
|
||||
</template>
|
||||
|
||||
<template #container="{ height }">
|
||||
<div
|
||||
:style="{ height: height + 'px' }"
|
||||
class="z-2 absolute flex flex-col gap-1 overflow-auto min-h-70vh w-full">
|
||||
<!-- 内容区域 -->
|
||||
<div class="w-full h-full box-border flex flex-col">
|
||||
<n-flex vertical justify="center" :size="20" class="p-[55px_20px] bg-white m-20px rounded-15px">
|
||||
<n-flex align="center" justify="center" :size="20">
|
||||
<n-avatar round size="large" :src="userInfo.avatar" />
|
||||
|
||||
<n-flex vertical :size="10">
|
||||
<p class="text-[--text-color]">{{ userInfo.name }}</p>
|
||||
<p class="text-(12px [--text-color])">群号: {{ userInfo.account }}</p>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<n-input
|
||||
v-model:value="requestMsg"
|
||||
:allow-input="(value: string) => !value.startsWith(' ') && !value.endsWith(' ')"
|
||||
:autosize="{
|
||||
minRows: 3,
|
||||
maxRows: 3
|
||||
}"
|
||||
:maxlength="60"
|
||||
:count-graphemes="countGraphemes"
|
||||
show-count
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
type="textarea"
|
||||
placeholder="输入验证消息" />
|
||||
|
||||
<n-button class="mt-120px" color="#13987f" @click="addFriend">申请加入</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AutoFixHeightPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCommon } from '@/hooks/useCommon.ts'
|
||||
import router from '@/router'
|
||||
import { useGlobalStore } from '@/stores/global.ts'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { applyGroup } from '@/utils/ImRequestUtils'
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const userStore = useUserStore()
|
||||
const { countGraphemes } = useCommon()
|
||||
const userInfo = ref(globalStore.addGroupModalInfo)
|
||||
const requestMsg = ref()
|
||||
|
||||
watch(
|
||||
() => globalStore.addGroupModalInfo,
|
||||
(newUid) => {
|
||||
userInfo.value = { ...newUid }
|
||||
}
|
||||
)
|
||||
|
||||
const addFriend = async () => {
|
||||
await applyGroup({
|
||||
msg: requestMsg.value,
|
||||
account: String(globalStore.addGroupModalInfo.account)
|
||||
})
|
||||
window.$message.success('已发送群聊申请')
|
||||
setTimeout(() => {
|
||||
router.push('/mobile/message')
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
console.log(userInfo.value)
|
||||
requestMsg.value = `我是${userStore.userInfo!.name}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<SafeAreaPlaceholder type="layout" direction="top" />
|
||||
<HeaderBar
|
||||
:isOfficial="false"
|
||||
:hidden-right="true"
|
||||
:enable-default-background="false"
|
||||
:enable-shadow="false"
|
||||
room-name="用户资料" />
|
||||
|
||||
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% fixed top-0" alt="hula" />
|
||||
|
||||
@@ -35,7 +40,6 @@
|
||||
import CommunityContent from '#/components/community/CommunityContent.vue'
|
||||
import CommunityTab from '#/components/community/CommunityTab.vue'
|
||||
import PersonalInfo from '#/components/my/PersonalInfo.vue'
|
||||
import SafeAreaPlaceholder from '#/components/placeholders/SafeAreaPlaceholder.vue'
|
||||
|
||||
const isShow = ref(true)
|
||||
const avatarBox = ref<HTMLElement | null>(null)
|
||||
@@ -49,34 +49,27 @@
|
||||
|
||||
<div class="flex flex-1 gap-2 flex-col bg-white z-1 custom-rounded">
|
||||
<!-- 我的消息条 -->
|
||||
<div class="grid grid-cols-[4rem_1fr_24px] h-64px px-16px border-b-[1px] border-b-solid border-b-[#e5e7eb]">
|
||||
<div class="h-full flex items-center">我的消息</div>
|
||||
<div class="h-full flex items-center justify-end overflow-hidden">
|
||||
<div class="rounded-full h-26px w-26px bg-purple-300 border-2 border-white"></div>
|
||||
<div class="rounded-full h-26px w-26px bg-orange-300 border-2 border-white -ml-2"></div>
|
||||
<div class="rounded-full h-26px w-26px bg-teal-300 border-2 border-white -ml-2"></div>
|
||||
<div class="rounded-full h-26px w-26px bg-orange-300 border-2 border-white -ml-2"></div>
|
||||
<div class="rounded-full h-26px w-26px bg-teal-300 border-2 border-white -ml-2"></div>
|
||||
<div
|
||||
class="rounded-full h-26px min-w-26px bg-gray-200 border-2 border-white -ml-2 flex items-center justify-center text-xs">
|
||||
99+
|
||||
</div>
|
||||
<div class="grid grid-cols-[4rem_1fr_24px] py-15px px-16px border-b-[1px] border-b-solid border-b-[#e5e7eb]">
|
||||
<div class="h-full flex items-center text-14px">我的消息</div>
|
||||
<div @click="toMessage" class="h-full flex items-center justify-end overflow-hidden">
|
||||
<n-avatar
|
||||
v-if="applyList.length > 0"
|
||||
:class="index > 0 ? '-ml-2' : ''"
|
||||
v-for="(item, index) in applyList.splice(0, 6)"
|
||||
:id="item.applyId"
|
||||
round
|
||||
size="small"
|
||||
:src="
|
||||
avatarSrc(
|
||||
groupStore.getUserInfo(isCurrentUser(item.receiverId) ? item.receiverId : item.senderId)?.avatar!
|
||||
)
|
||||
" />
|
||||
</div>
|
||||
<div class="h-full flex justify-end items-center">
|
||||
<div @click="toMessage" class="h-full flex justify-end items-center">
|
||||
<img src="@/assets/mobile/friend/right-arrow.webp" class="block h-20px" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <n-tabs class="px-16px" default-value="mutual-attention" justify-content="start" type="line">
|
||||
<n-tab-pane name="mutual-attention" tab="互相关注">
|
||||
|
||||
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="follow" tab="关注"> 关注 </n-tab-pane>
|
||||
<n-tab-pane name="fans" tab="粉丝"> 粉丝 </n-tab-pane>
|
||||
<n-tab-pane name="group-chat" tab="群聊"> 群聊 </n-tab-pane>
|
||||
</n-tabs> -->
|
||||
|
||||
<n-tabs type="segment" animated class="mt-4px p-[4px_10px_0px_8px]">
|
||||
<n-tab-pane name="1" tab="好友">
|
||||
<n-collapse :display-directive="'show'" accordion :default-expanded-names="['1']">
|
||||
@@ -179,13 +172,23 @@ import NavBar from '#/layout/navBar/index.vue'
|
||||
import addFriendIcon from '@/assets/mobile/chat-home/add-friend.webp'
|
||||
import groupChatIcon from '@/assets/mobile/chat-home/group-chat.webp'
|
||||
import { MittEnum, OnlineEnum, RoomTypeEnum } from '@/enums'
|
||||
import { useMessage } from '@/hooks/useMessage.ts'
|
||||
import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import router from '@/router'
|
||||
import { useContactStore } from '@/stores/contacts.ts'
|
||||
import { useGroupStore } from '@/stores/group'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useUserStatusStore } from '@/stores/userStatus'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await contactStore.getApplyPage(false)
|
||||
} catch (error) {
|
||||
console.log('请求好友申请列表失败')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 渲染图片图标的函数工厂
|
||||
* @param {string} src - 图标图片路径
|
||||
@@ -230,8 +233,24 @@ const shrinkStatus = ref(false)
|
||||
const groupStore = useGroupStore()
|
||||
const contactStore = useContactStore()
|
||||
const userStatusStore = useUserStatusStore()
|
||||
const userStore = useUserStore()
|
||||
const { stateList } = storeToRefs(userStatusStore)
|
||||
|
||||
//好友申请列表(只筛选好友申请消息)
|
||||
const applyList = computed(() => {
|
||||
return contactStore.requestFriendsList.filter((item) => item.type === 2)
|
||||
})
|
||||
|
||||
const avatarSrc = (url: string) => AvatarUtils.getAvatarUrl(url)
|
||||
|
||||
const isCurrentUser = (uid: string) => {
|
||||
return uid === userStore.userInfo!.uid
|
||||
}
|
||||
|
||||
const toMessage = () => {
|
||||
router.push('/mobile/mobileMy/myMessages')
|
||||
}
|
||||
|
||||
/** 群聊列表 */
|
||||
const groupChatList = computed(() => {
|
||||
return [...groupStore.groupDetails].sort((a, b) => {
|
||||
@@ -261,23 +280,35 @@ watchEffect(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const { preloadChatRoom } = useMessage()
|
||||
|
||||
/**
|
||||
*
|
||||
* @param uid 群聊id或好友uid
|
||||
* @param type 1 群聊 2 单聊
|
||||
*/
|
||||
const handleClick = (uid: string, type: number) => {
|
||||
const handleClick = async (id: string, type: number) => {
|
||||
detailsShow.value = true
|
||||
activeItem.value = uid
|
||||
activeItem.value = id
|
||||
const data = {
|
||||
context: {
|
||||
type: type,
|
||||
uid: uid
|
||||
uid: id
|
||||
},
|
||||
detailsShow: detailsShow.value
|
||||
}
|
||||
useMitt.emit(MittEnum.DETAILS_SHOW, data)
|
||||
router.push('/mobile/mobileMy/friendInfo')
|
||||
|
||||
if (type === 1) {
|
||||
try {
|
||||
await preloadChatRoom(id)
|
||||
router.push(`/mobile/chatRoom/chatMain`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
router.push(`/mobile/mobileFriends/friendInfo/${id}`)
|
||||
}
|
||||
}
|
||||
// todo 需要循环数组来展示分组
|
||||
const showMenu = (event: MouseEvent) => {
|
||||
|
||||
@@ -11,9 +11,20 @@
|
||||
|
||||
<template #container="{ height }">
|
||||
<div :style="{ height: height + 'px' }" class="z-2 flex flex-col absolute overflow-auto min-h-70vh w-full">
|
||||
<div class="flex flex-col p-20px gap-20px">
|
||||
<!-- <n-avatar :size="40" fallback-src="/logo.png" round /> -->
|
||||
<MyMessageItem v-for="item in fakeData" :key="item.id" :message="item" />
|
||||
<div class="flex flex-col p-[10px_20px_0px_20px] gap-20px">
|
||||
<CommunityTab
|
||||
:customHeight="height - 55"
|
||||
@update="onUpdate"
|
||||
:options="tabOptions"
|
||||
active-tab-name="friend-message">
|
||||
<template #friend-message>
|
||||
<MobileApplyList :close-header="true" type="friend" :custom-height="height - 50" />
|
||||
</template>
|
||||
|
||||
<template #group-message>
|
||||
<MobileApplyList :close-header="true" type="group" :custom-height="height - 50" />
|
||||
</template>
|
||||
</CommunityTab>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,127 +32,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
type MessageType = 'follow' | 'like' | 'comment' | 'thumbs' | 'system' | 'reply'
|
||||
|
||||
interface Message {
|
||||
id: number
|
||||
content: string
|
||||
type: MessageType
|
||||
const onUpdate = (newTab: string) => {
|
||||
console.log('已更新:', newTab)
|
||||
}
|
||||
|
||||
const fakeData: Ref<Message[]> = computed(() => [
|
||||
{ id: 1, content: '关注了你,想了解更多你的动态', type: 'follow' },
|
||||
{ id: 2, content: '点赞了你的动态「今天天气真好」', type: 'like' },
|
||||
const tabOptions = reactive([
|
||||
{
|
||||
id: 3,
|
||||
content:
|
||||
'评论了你的动态:我觉得这个观点非常有意思评论了你的动态:我觉得这个观点非常有意思评论了你的动态:我觉得这个观点非常有意思',
|
||||
type: 'comment'
|
||||
tab: '好友消息',
|
||||
name: 'friend-message'
|
||||
},
|
||||
{ id: 4, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 5, content: '系统消息:你有新的好友推荐', type: 'system' },
|
||||
{ id: 6, content: '关注了你,快去看看他的主页吧', type: 'follow' },
|
||||
{ id: 7, content: '点赞了你的动态「刚刚完成的项目分享」', type: 'like' },
|
||||
{ id: 8, content: '评论了你的动态:这个方法真的可行吗?', type: 'comment' },
|
||||
{ id: 9, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 10, content: '系统消息:你的设置已成功保存', type: 'system' },
|
||||
{
|
||||
id: 11,
|
||||
content:
|
||||
'回复了你的评论:我也遇到过类似问题,建议试试这个方法我也遇到过类似问题,建议试试这个方法我也遇到过类似问题,建议试试这个方法我也遇到过类似问题,建议试试这个方法',
|
||||
type: 'reply'
|
||||
},
|
||||
{ id: 12, content: '回复了你的动态:哈哈,这个真的太搞笑了', type: 'reply' },
|
||||
{ id: 13, content: '关注了你,希望能看到更多你的分享', type: 'follow' },
|
||||
{ id: 14, content: '点赞了你的动态「新年快乐!」', type: 'like' },
|
||||
{ id: 15, content: '评论了你的动态:这个思路很新颖,我很喜欢', type: 'comment' },
|
||||
{ id: 16, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 17, content: '系统消息:你已达到每日活跃任务', type: 'system' },
|
||||
{ id: 18, content: '回复了你的评论:你的建议我已经采纳了,谢谢!', type: 'reply' },
|
||||
{ id: 19, content: '关注了你,想看看你的最近分享', type: 'follow' },
|
||||
{ id: 20, content: '点赞了你的动态「今日学习笔记」', type: 'like' },
|
||||
{ id: 21, content: '关注了你', type: 'follow' },
|
||||
{ id: 22, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 23, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 24, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 25, content: '系统消息', type: 'system' },
|
||||
{ id: 26, content: '关注了你', type: 'follow' },
|
||||
{ id: 27, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 28, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 29, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 30, content: '系统消息', type: 'system' },
|
||||
{ id: 31, content: '关注了你', type: 'follow' },
|
||||
{ id: 32, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 33, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 34, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 35, content: '系统消息', type: 'system' },
|
||||
{ id: 36, content: '关注了你', type: 'follow' },
|
||||
{ id: 37, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 38, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 39, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 40, content: '系统消息', type: 'system' },
|
||||
{ id: 41, content: '关注了你', type: 'follow' },
|
||||
{ id: 42, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 43, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 44, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 45, content: '系统消息', type: 'system' },
|
||||
{ id: 46, content: '关注了你', type: 'follow' },
|
||||
{ id: 47, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 48, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 49, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 50, content: '系统消息', type: 'system' },
|
||||
{ id: 51, content: '关注了你', type: 'follow' },
|
||||
{ id: 52, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 53, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 54, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 55, content: '系统消息', type: 'system' },
|
||||
{ id: 56, content: '关注了你', type: 'follow' },
|
||||
{ id: 57, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 58, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 59, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 60, content: '系统消息', type: 'system' },
|
||||
{ id: 61, content: '关注了你', type: 'follow' },
|
||||
{ id: 62, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 63, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 64, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 65, content: '系统消息', type: 'system' },
|
||||
{ id: 66, content: '关注了你', type: 'follow' },
|
||||
{ id: 67, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 68, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 69, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 70, content: '系统消息', type: 'system' },
|
||||
{ id: 71, content: '关注了你', type: 'follow' },
|
||||
{ id: 72, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 73, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 74, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 75, content: '系统消息', type: 'system' },
|
||||
{ id: 76, content: '关注了你', type: 'follow' },
|
||||
{ id: 77, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 78, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 79, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 80, content: '系统消息', type: 'system' },
|
||||
{ id: 81, content: '关注了你', type: 'follow' },
|
||||
{ id: 82, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 83, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 84, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 85, content: '系统消息', type: 'system' },
|
||||
{ id: 86, content: '关注了你', type: 'follow' },
|
||||
{ id: 87, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 88, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 89, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 90, content: '系统消息', type: 'system' },
|
||||
{ id: 91, content: '关注了你', type: 'follow' },
|
||||
{ id: 92, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 93, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 94, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 95, content: '系统消息', type: 'system' },
|
||||
{ id: 96, content: '关注了你', type: 'follow' },
|
||||
{ id: 97, content: '点赞了你的动态', type: 'like' },
|
||||
{ id: 98, content: '评论了你的动态', type: 'comment' },
|
||||
{ id: 99, content: '给你发送了一个赞', type: 'thumbs' },
|
||||
{ id: 100, content: '系统消息', type: 'system' }
|
||||
tab: '群聊消息',
|
||||
name: 'group-message'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex flex-col h-full">
|
||||
<SafeAreaPlaceholder type="layout" direction="top" />
|
||||
|
||||
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% fixed top-0" alt="hula" />
|
||||
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% fixed z-0 top-0" alt="hula" />
|
||||
|
||||
<!-- 设置区 -->
|
||||
<Settings />
|
||||
@@ -13,7 +13,7 @@
|
||||
<div ref="measureRef" class="h-full w-full absolute top-0 z-0"></div>
|
||||
<!-- 动态内容 -->
|
||||
<div ref="scrollContainer" :style="{ height: tabHeight + 'px' }" class="z-1 overflow-y-auto mt-2 absolute z-3">
|
||||
<div class="custom-rounded flex px-24px flex-col gap-4 z-1 p-10px mt-4 shadow">
|
||||
<div class="custom-rounded bg-white flex px-24px flex-col gap-4 z-1 p-10px mt-4">
|
||||
<CommunityTab
|
||||
:style="{ height: tabHeight + 'px' }"
|
||||
:custom-height="tabHeight - 44"
|
||||
|
||||
+30
-8
@@ -21,13 +21,15 @@ import NoticeEdit from '#/views/chat-room/notice/NoticeEdit.vue'
|
||||
import NoticeList from '#/views/chat-room/notice/NoticeList.vue'
|
||||
import MobileCommunity from '#/views/community/index.vue'
|
||||
import AddFriends from '#/views/friends/AddFriends.vue'
|
||||
import ConfirmAddFriend from '#/views/friends/ConfirmAddFriend.vue'
|
||||
import ConfirmAddGroup from '#/views/friends/ConfirmAddGroup.vue'
|
||||
import FriendInfo from '#/views/friends/FriendInfo.vue'
|
||||
import MobileFriendPage from '#/views/friends/index.vue'
|
||||
import StartGroupChat from '#/views/friends/StartGroupChat.vue'
|
||||
import MobileMessagePage from '#/views/message/index.vue'
|
||||
import EditBio from '#/views/my/EditBio.vue'
|
||||
import EditBirthday from '#/views/my/EditBirthday.vue'
|
||||
import EditProfile from '#/views/my/EditProfile.vue'
|
||||
import FriendInfo from '#/views/my/FriendInfo.vue'
|
||||
import MobileMy from '#/views/my/index.vue'
|
||||
import MobileQRCode from '#/views/my/MobileQRCode.vue'
|
||||
import MobileSettings from '#/views/my/MobileSettings.vue'
|
||||
@@ -190,11 +192,6 @@ const getMobileRoutes = (): Array<RouteRecordRaw> => [
|
||||
path: 'SimpleBio',
|
||||
name: 'mobileSimpleBio',
|
||||
component: SimpleBio
|
||||
},
|
||||
{
|
||||
path: 'friendInfo',
|
||||
name: 'mobileFriendInfo',
|
||||
component: FriendInfo
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -217,6 +214,21 @@ const getMobileRoutes = (): Array<RouteRecordRaw> => [
|
||||
path: 'startGroupChat',
|
||||
name: 'mobileStartGroupChat',
|
||||
component: StartGroupChat
|
||||
},
|
||||
{
|
||||
path: 'confirmAddFriend',
|
||||
name: 'mobileConfirmAddFriend',
|
||||
component: ConfirmAddFriend
|
||||
},
|
||||
{
|
||||
path: 'confirmAddGroup',
|
||||
name: 'mobileConfirmAddGroup',
|
||||
component: ConfirmAddGroup
|
||||
},
|
||||
{
|
||||
path: 'friendInfo/:uid',
|
||||
name: 'mobileFriendInfo',
|
||||
component: FriendInfo
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -318,6 +330,11 @@ const getDesktopRoutes = (): Array<RouteRecordRaw> => [
|
||||
name: 'loginSetting',
|
||||
component: () => import('@/views/moreWindow/settings/LoginSetting.vue')
|
||||
},
|
||||
{
|
||||
path: '/notification',
|
||||
name: 'notification',
|
||||
component: () => import('@/views/moreWindow/settings/Notification.vue')
|
||||
},
|
||||
{
|
||||
path: '/versatile',
|
||||
name: 'versatile',
|
||||
@@ -355,6 +372,12 @@ const getDesktopRoutes = (): Array<RouteRecordRaw> => [
|
||||
name: 'rtcCall',
|
||||
component: () => import('@/views/callWindow/index.vue')
|
||||
},
|
||||
// 添加合并消息窗口路由
|
||||
{
|
||||
path: '/multiMsg',
|
||||
name: 'multiMsg',
|
||||
component: () => import('@/views/multiMsgWindow/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/searchFriend',
|
||||
name: 'searchFriend',
|
||||
@@ -464,8 +487,7 @@ const router: any = createRouter({
|
||||
|
||||
// 在创建路由后,添加全局前置守卫
|
||||
// 为解决 “已声明‘to’,但从未读取其值” 的问题,将 to 参数改为下划线开头表示该参数不会被使用
|
||||
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
console.log('进入了路由:', from, to)
|
||||
router.beforeEach(async (to: RouteLocationNormalized, _from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
// 桌面端直接放行
|
||||
if (!isMobile) {
|
||||
return next()
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export type Settings = {
|
||||
database: {
|
||||
sqlite_file: string
|
||||
}
|
||||
backend: {
|
||||
base_url: string
|
||||
ws_url: string
|
||||
}
|
||||
youdao: {
|
||||
app_key: string
|
||||
app_secret: string
|
||||
}
|
||||
tencent: {
|
||||
api_key: string
|
||||
secret_id: string
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdateSettingsParams = {
|
||||
baseUrl: string
|
||||
wsUrl: string
|
||||
}
|
||||
|
||||
export const getSettings = async (): Promise<Settings> => {
|
||||
return await invoke('get_settings')
|
||||
}
|
||||
|
||||
export const updateSettings = async (settings: UpdateSettingsParams) => {
|
||||
return await invoke('update_settings', { settings })
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { AppException } from '@/common/exception.ts'
|
||||
import { getSettings, type Settings } from '@/services/tauriCommand.ts'
|
||||
import type { TranslateProvider } from './types.ts'
|
||||
|
||||
// 有道云翻译接口响应类型
|
||||
@@ -133,11 +134,10 @@ export const translateText = async (text: string, provider: TranslateProvider =
|
||||
*/
|
||||
const youdaoTranslate = async (text: string): Promise<TranslateResult> => {
|
||||
// 从环境变量获取密钥信息
|
||||
const appKey = import.meta.env.VITE_YOUDAO_APP_KEY
|
||||
const appSecret = import.meta.env.VITE_YOUDAO_APP_SECRET
|
||||
const settings: Settings = await getSettings()
|
||||
|
||||
// 检查密钥是否为空
|
||||
if (!appKey || !appSecret) {
|
||||
if (!settings.youdao.app_key || !settings.youdao.app_secret) {
|
||||
throw window.$message?.error('有道翻译API密钥未配置')
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ const youdaoTranslate = async (text: string): Promise<TranslateResult> => {
|
||||
const curtime = Math.round(new Date().getTime() / 1000) // 当前时间戳
|
||||
|
||||
// 计算签名
|
||||
const sign = signUtils.getYoudaoSign(text, appKey, salt, curtime, appSecret)
|
||||
const sign = signUtils.getYoudaoSign(text, settings.youdao.app_key, salt, curtime, settings.youdao.app_secret)
|
||||
|
||||
// 发送翻译请求
|
||||
const response = await fetch('https://openapi.youdao.com/api', {
|
||||
@@ -157,7 +157,7 @@ const youdaoTranslate = async (text: string): Promise<TranslateResult> => {
|
||||
q: text, // 待翻译文本
|
||||
from: 'auto', // 源语言
|
||||
to: 'zh-CHS', // 目标语言
|
||||
appKey, // 应用ID
|
||||
appKey: settings.youdao.app_key, // 应用ID
|
||||
salt: salt.toString(), // 随机数
|
||||
sign, // 签名
|
||||
signType: 'v3', // 签名类型
|
||||
@@ -190,8 +190,9 @@ const youdaoTranslate = async (text: string): Promise<TranslateResult> => {
|
||||
*/
|
||||
const tencentTranslate = async (text: string): Promise<TranslateResult> => {
|
||||
// 从环境变量获取密钥信息
|
||||
const secretId = import.meta.env.VITE_TENCENT_SECRET_ID
|
||||
const secretKey = import.meta.env.VITE_TENCENT_API_KEY
|
||||
const settings: Settings = await getSettings()
|
||||
const secretId = settings.tencent.secret_id
|
||||
const secretKey = settings.tencent.api_key
|
||||
|
||||
// 检查密钥是否为空
|
||||
if (!secretId || !secretKey) {
|
||||
|
||||
+68
-13
@@ -323,12 +323,13 @@ export type MessageType = {
|
||||
/** 消息主体 */
|
||||
message: MsgType
|
||||
/** 发送时间 */
|
||||
sendTime: string
|
||||
sendTime: number
|
||||
/** 时间段(可选) */
|
||||
timeBlock?: string
|
||||
timeBlock?: number
|
||||
/** 是否加载中 */
|
||||
loading?: boolean
|
||||
uploadProgress?: number
|
||||
isCheck?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,6 +379,16 @@ export type VoiceBody = {
|
||||
second: number
|
||||
url: string
|
||||
}
|
||||
|
||||
export type MergeBodyBody = {
|
||||
messageId: string
|
||||
uid: string
|
||||
}
|
||||
|
||||
export type MergeBody = {
|
||||
body: MergeBodyBody[]
|
||||
content: string[]
|
||||
}
|
||||
/** 视频 */
|
||||
export type VideoBody = {
|
||||
size: number
|
||||
@@ -479,16 +490,32 @@ export type MessageReq = {
|
||||
}
|
||||
}
|
||||
|
||||
/** 申请状态 */
|
||||
export enum RequestFriendAgreeStatus {
|
||||
/** 1待审批 */
|
||||
Waiting = 1,
|
||||
/** 2同意 */
|
||||
Agree,
|
||||
/** 3拒绝 */
|
||||
Reject,
|
||||
/** 4忽略 */
|
||||
Ignore
|
||||
/** 通知状态 */
|
||||
export enum RequestNoticeAgreeStatus {
|
||||
/** 待审批 */
|
||||
UNTREATED = 0,
|
||||
/** 同意 */
|
||||
ACCEPTED,
|
||||
/** 拒绝 */
|
||||
REJECTED,
|
||||
/** 忽略 */
|
||||
IGNORE
|
||||
}
|
||||
|
||||
/** 通知事件 */
|
||||
export enum NoticeType {
|
||||
/** 好友申请 */
|
||||
FRIEND_APPLY = 1,
|
||||
/** 好友被申请 */
|
||||
ADD_ME = 6,
|
||||
/** 加群申请 */
|
||||
GROUP_APPLY = 2,
|
||||
/** 群邀请 */
|
||||
GROUP_INVITE = 3,
|
||||
/** 被邀请进群 */
|
||||
GROUP_INVITE_ME = 7,
|
||||
/** 移除群成员 */
|
||||
GROUP_MEMBER_DELETE = 5
|
||||
}
|
||||
|
||||
/** 请求添加好友的列表项 */
|
||||
@@ -498,7 +525,7 @@ export type RequestFriendItem = {
|
||||
/** 申请信息 */
|
||||
msg: string
|
||||
/** 申请状态 1待审批 2同意 3拒绝 4忽略 */
|
||||
status: RequestFriendAgreeStatus
|
||||
status: RequestNoticeAgreeStatus
|
||||
/** 申请类型 1加好友 */
|
||||
type: number
|
||||
/** 申请人uid */
|
||||
@@ -510,6 +537,32 @@ export type RequestFriendItem = {
|
||||
/** 会话 ID */
|
||||
roomId: string
|
||||
}
|
||||
|
||||
export interface NoticeItem {
|
||||
/** 实体ID */
|
||||
id?: string
|
||||
/** 通知类型:1-好友申请;2-群申请;3-群邀请;5-移除群成员;6-好友被申请;7-被邀请进群 */
|
||||
eventType: number
|
||||
/** 通知类型 1群聊 2加好友 */
|
||||
type: number
|
||||
/** 发起人UID */
|
||||
senderId: string
|
||||
/** 接收人UID */
|
||||
receiverId: string
|
||||
/** 申请ID */
|
||||
applyId: string
|
||||
/** 被操作的人 */
|
||||
operateId?: string
|
||||
/** 通知内容 申请时填写的 */
|
||||
content: string
|
||||
/** 处理状态:0-未处理;1-已同意;2-已拒绝;3-忽略 */
|
||||
status: number
|
||||
/** 是否已读 */
|
||||
isRead: boolean
|
||||
/** 创建时间 */
|
||||
createTime?: number
|
||||
}
|
||||
|
||||
/** 联系人的列表项 */
|
||||
export type ContactItem = {
|
||||
/** 在线状态 1在线 2离线 */
|
||||
@@ -569,6 +622,8 @@ export type SessionItem = {
|
||||
remark?: string
|
||||
/** 我的群昵称 */
|
||||
myName?: string
|
||||
/** 是否选中(非后端) */
|
||||
isCheck?: boolean
|
||||
}
|
||||
|
||||
/** 消息已读未读数列表项 */
|
||||
|
||||
@@ -104,21 +104,8 @@ class RustWebSocketClient {
|
||||
async initConnect(): Promise<void> {
|
||||
try {
|
||||
const clientId = localStorage.getItem('clientId')
|
||||
const savedProxy = localStorage.getItem('proxySettings')
|
||||
|
||||
let serverUrl = import.meta.env.VITE_WEBSOCKET_URL
|
||||
|
||||
// 处理代理设置
|
||||
if (savedProxy) {
|
||||
const settings = JSON.parse(savedProxy)
|
||||
const suffix = settings.wsIp + ':' + settings.wsPort + '/websocket/' + settings.wsSuffix
|
||||
if (settings.wsType === 'ws' || settings.wsType === 'wss') {
|
||||
serverUrl = settings.wsType + '://' + suffix
|
||||
}
|
||||
}
|
||||
|
||||
const params = {
|
||||
serverUrl,
|
||||
clientId: clientId || ''
|
||||
}
|
||||
|
||||
@@ -126,9 +113,6 @@ class RustWebSocketClient {
|
||||
|
||||
await invoke('ws_init_connection', { params })
|
||||
|
||||
// await this.setupEventListener()
|
||||
|
||||
// this.isInitialized = true
|
||||
info('[RustWS] WebSocket 连接初始化成功')
|
||||
} catch (err) {
|
||||
error(`[RustWS] 连接初始化失败: ${err}`)
|
||||
|
||||
+54
-67
@@ -30,10 +30,10 @@ let isFirstInit = false
|
||||
// 撤回消息的过期时间
|
||||
const RECALL_EXPIRATION_TIME = 2 * 60 * 1000 // 2分钟,单位毫秒
|
||||
|
||||
// 定义消息数量阈值
|
||||
const MESSAGE_THRESHOLD = 120
|
||||
// 定义保留的最新消息数量
|
||||
const KEEP_MESSAGE_COUNT = 60
|
||||
// // 定义消息数量阈值
|
||||
// const MESSAGE_THRESHOLD = 120
|
||||
// // 定义保留的最新消息数量
|
||||
// const KEEP_MESSAGE_COUNT = 60
|
||||
|
||||
// 创建src/workers/timer.worker.ts
|
||||
const timerWorker = new Worker(new URL('../workers/timer.worker.ts', import.meta.url))
|
||||
@@ -69,19 +69,11 @@ export const useChatStore = defineStore(
|
||||
const recalledMessages = reactive<Map<string, RecalledMessage>>(new Map())
|
||||
// 存储每条撤回消息的过期定时器
|
||||
const expirationTimers = new Map<string, boolean>()
|
||||
const isMsgMultiChoose = ref<boolean>(false)
|
||||
|
||||
// 当前聊天室的消息Map计算属性
|
||||
const currentMessageMap = computed({
|
||||
get: () => {
|
||||
const current = messageMap.get(globalStore.currentSession!.roomId)
|
||||
if (current === undefined) {
|
||||
messageMap.set(globalStore.currentSession!.roomId, new Map())
|
||||
}
|
||||
return messageMap.get(globalStore.currentSession!.roomId)
|
||||
},
|
||||
set: (val) => {
|
||||
messageMap.set(globalStore.currentSession!.roomId, val as Map<string, MessageType>)
|
||||
}
|
||||
const currentMessageMap = computed(() => {
|
||||
return messageMap.get(globalStore.currentSession!.roomId)
|
||||
})
|
||||
|
||||
// 当前聊天室的消息加载状态计算属性
|
||||
@@ -115,6 +107,11 @@ export const useChatStore = defineStore(
|
||||
}
|
||||
})
|
||||
|
||||
// 判断是否应该显示“没有更多消息”
|
||||
const shouldShowNoMoreMessage = computed(() => {
|
||||
return currentMessageOptions.value?.isLast
|
||||
})
|
||||
|
||||
// 判断当前是否为群聊
|
||||
const isGroup = computed(() => globalStore.currentSession?.type === RoomTypeEnum.GROUP)
|
||||
|
||||
@@ -183,17 +180,17 @@ export const useChatStore = defineStore(
|
||||
// 当前消息回复
|
||||
const currentMsgReply = ref<Partial<MessageType>>({})
|
||||
|
||||
// 将消息列表转换为数组
|
||||
// 将消息列表转换为数组并计算时间间隔
|
||||
const chatMessageList = computed(() => {
|
||||
return currentMessageMap.value
|
||||
? [...currentMessageMap.value.values()].sort((a, b) => Number(a.message.id) - Number(b.message.id))
|
||||
: []
|
||||
if (!currentMessageMap.value) return []
|
||||
|
||||
return [...currentMessageMap.value.values()].sort((a, b) => Number(a.message.id) - Number(b.message.id))
|
||||
})
|
||||
|
||||
const chatMessageListByRoomId = computed(() => (roomId: string) => {
|
||||
return messageMap.get(roomId)
|
||||
? [...messageMap.get(roomId)!.values()].sort((a, b) => Number(a.message.id) - Number(b.message.id))
|
||||
: []
|
||||
if (!messageMap.get(roomId)) return []
|
||||
|
||||
return [...messageMap.get(roomId)!.values()].sort((a, b) => Number(a.message.id) - Number(b.message.id))
|
||||
})
|
||||
|
||||
// 登录之后,加载一次所有会话的消息
|
||||
@@ -254,7 +251,6 @@ export const useChatStore = defineStore(
|
||||
try {
|
||||
if (sessionOptions.isLoading) return
|
||||
sessionOptions.isLoading = true
|
||||
console.log('获取会话列表')
|
||||
const response: any = await invokeWithErrorHandler(TauriCommand.LIST_CONTACTS, undefined, {
|
||||
customErrorMessage: '获取会话列表失败',
|
||||
errorType: ErrorType.Network
|
||||
@@ -356,16 +352,6 @@ export const useChatStore = defineStore(
|
||||
const current = messageMap.get(msg.message.roomId)
|
||||
current?.set(msg.message.id, msg)
|
||||
|
||||
// 检查消息数量是否超过阈值
|
||||
if (current && current.size > MESSAGE_THRESHOLD) {
|
||||
// 获取所有消息ID并按时间排序
|
||||
const messageIds = Array.from(current.keys())
|
||||
const messagesToDelete = messageIds.slice(0, messageIds.length - KEEP_MESSAGE_COUNT)
|
||||
|
||||
// 删除旧消息
|
||||
messagesToDelete.forEach((id) => current.delete(id))
|
||||
}
|
||||
|
||||
// 获取用户信息缓存
|
||||
const uid = msg.fromUser.uid
|
||||
const cacheUser = groupStore.getUserInfo(uid)
|
||||
@@ -412,6 +398,10 @@ export const useChatStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
const clearSessionCheck = () => {
|
||||
sessionList.value.forEach((item) => (item.isCheck = false))
|
||||
}
|
||||
|
||||
// 过滤掉拉黑用户的发言
|
||||
const filterUser = (uid: string) => {
|
||||
for (const messages of messageMap.values()) {
|
||||
@@ -573,14 +563,6 @@ export const useChatStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
// 添加一个工具函数来触发消息列表更新
|
||||
const triggerMessageMapUpdate = () => {
|
||||
if (currentMessageMap.value) {
|
||||
const newMap = new Map(currentMessageMap.value)
|
||||
currentMessageMap.value = newMap
|
||||
}
|
||||
}
|
||||
|
||||
// 获取撤回消息
|
||||
const getRecalledMessage = (msgId: string): RecalledMessage | undefined => {
|
||||
return recalledMessages.get(msgId)
|
||||
@@ -597,17 +579,20 @@ export const useChatStore = defineStore(
|
||||
status,
|
||||
newMsgId,
|
||||
body,
|
||||
uploadProgress
|
||||
uploadProgress,
|
||||
timeBlock
|
||||
}: {
|
||||
msgId: string
|
||||
status: MessageStatusEnum
|
||||
newMsgId?: string
|
||||
body?: any
|
||||
uploadProgress?: number
|
||||
timeBlock?: number
|
||||
}) => {
|
||||
const msg = currentMessageMap.value?.get(msgId)
|
||||
if (msg) {
|
||||
msg.message.status = status
|
||||
msg.timeBlock = timeBlock
|
||||
if (newMsgId) {
|
||||
msg.message.id = newMsgId
|
||||
}
|
||||
@@ -652,6 +637,10 @@ export const useChatStore = defineStore(
|
||||
const index = sessionList.value.findIndex((session) => session.roomId === roomId)
|
||||
if (index !== -1) {
|
||||
sessionList.value.splice(index, 1)
|
||||
if (globalStore.currentSessionRoomId === roomId) {
|
||||
globalStore.updateCurrentSessionRoomId(sessionList.value[0].roomId)
|
||||
}
|
||||
|
||||
// 删除会话后更新未读计数
|
||||
nextTick(() => {
|
||||
updateTotalUnreadCount()
|
||||
@@ -667,7 +656,6 @@ export const useChatStore = defineStore(
|
||||
console.log(`[Timeout] 消息ID: ${msgId} 已过期`)
|
||||
recalledMessages.delete(msgId)
|
||||
expirationTimers.delete(msgId)
|
||||
triggerMessageMapUpdate()
|
||||
} else if (type === 'allTimersCompleted') {
|
||||
// 所有定时器都完成了,可以安全地清理资源
|
||||
clearAllExpirationTimers()
|
||||
@@ -736,38 +724,34 @@ export const useChatStore = defineStore(
|
||||
const requestRoomId = globalStore.currentSession!.roomId
|
||||
|
||||
try {
|
||||
// 1. 清空当前消息列表
|
||||
if (currentMessageMap.value) {
|
||||
currentMessageMap.value.clear()
|
||||
}
|
||||
// 1. 清空消息数据 避免竞态条件
|
||||
const currentMessages = messageMap.get(requestRoomId)
|
||||
currentMessages?.clear() // 如果Map存在就清空,不存在getPageMsg会自动创建
|
||||
|
||||
// 2. 重置消息加载状态
|
||||
if (currentMessageOptions.value) {
|
||||
currentMessageOptions.value = {
|
||||
isLast: false,
|
||||
isLoading: true,
|
||||
cursor: ''
|
||||
}
|
||||
}
|
||||
// 2. 重置消息加载状态,强制cursor为空以获取最新消息
|
||||
messageOptions.set(requestRoomId, {
|
||||
isLast: false,
|
||||
isLoading: true,
|
||||
cursor: ''
|
||||
})
|
||||
|
||||
// 3. 清空回复映射
|
||||
if (currentReplyMap.value) {
|
||||
currentReplyMap.value.clear()
|
||||
}
|
||||
const currentReplyMapping = replyMapping.get(requestRoomId)
|
||||
currentReplyMapping?.clear()
|
||||
|
||||
// 4. 从服务器获取最新的消息(默认20条)
|
||||
await getMsgList(pageSize)
|
||||
// 4. 直接调用getPageMsg获取最新消息,强制使用空cursor
|
||||
await getPageMsg(pageSize, requestRoomId, '')
|
||||
|
||||
console.log('[Network] 已重置并刷新当前聊天室的消息列表')
|
||||
} catch (error) {
|
||||
console.error('[Network] 重置并刷新消息列表失败:', error)
|
||||
// 如果获取失败,确保重置加载状态
|
||||
if (globalStore.currentSession!.roomId === requestRoomId && currentMessageOptions.value) {
|
||||
currentMessageOptions.value = {
|
||||
if (globalStore.currentSession!.roomId === requestRoomId) {
|
||||
messageOptions.set(requestRoomId, {
|
||||
isLast: false,
|
||||
isLoading: false,
|
||||
cursor: ''
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -777,9 +761,9 @@ export const useChatStore = defineStore(
|
||||
return sessionList.value.filter((session) => session.type === RoomTypeEnum.GROUP)
|
||||
}
|
||||
|
||||
const shouldShowNoMoreMessage = computed(() => {
|
||||
return currentMessageOptions.value?.isLast
|
||||
})
|
||||
const setMsgMultiChoose = (flag: boolean) => {
|
||||
isMsgMultiChoose.value = flag
|
||||
}
|
||||
|
||||
return {
|
||||
getMsgIndex,
|
||||
@@ -822,7 +806,10 @@ export const useChatStore = defineStore(
|
||||
addSession,
|
||||
setAllSessionMsgList,
|
||||
chatMessageListByRoomId,
|
||||
shouldShowNoMoreMessage
|
||||
shouldShowNoMoreMessage,
|
||||
isMsgMultiChoose,
|
||||
clearSessionCheck,
|
||||
setMsgMultiChoose
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { StoresEnum } from '@/enums'
|
||||
import type { ContactItem, RequestFriendItem } from '@/services/types'
|
||||
import { RequestFriendAgreeStatus } from '@/services/types'
|
||||
import type { ContactItem, NoticeItem } from '@/services/types'
|
||||
import { RequestNoticeAgreeStatus } from '@/services/types'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import {
|
||||
deleteFriend,
|
||||
getApplyUnreadCount,
|
||||
getFriendPage,
|
||||
handleInvite,
|
||||
requestApplyPage
|
||||
requestNoticePage
|
||||
} from '@/utils/ImRequestUtils'
|
||||
// 定义分页大小常量
|
||||
export const pageSize = 20
|
||||
@@ -18,7 +18,7 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
|
||||
/** 联系人列表 */
|
||||
const contactsList = ref<ContactItem[]>([])
|
||||
/** 好友请求列表 */
|
||||
const requestFriendsList = ref<RequestFriendItem[]>([])
|
||||
const requestFriendsList = ref<NoticeItem[]>([])
|
||||
|
||||
/** 联系人列表分页选项 */
|
||||
const contactsOptions = ref({ isLast: false, isLoading: false, cursor: '' })
|
||||
@@ -79,7 +79,7 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await requestApplyPage({
|
||||
const res = await requestNoticePage({
|
||||
pageNo: applyPageOptions.value.pageNo,
|
||||
pageSize: 30,
|
||||
cursor: isFresh ? '' : applyPageOptions.value.cursor
|
||||
@@ -125,7 +125,7 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
|
||||
if (globalStore.currentSelectedContact) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
globalStore.currentSelectedContact.status = RequestFriendAgreeStatus.Agree
|
||||
globalStore.currentSelectedContact.status = RequestNoticeAgreeStatus.ACCEPTED
|
||||
}
|
||||
// 获取最新的未读数
|
||||
await getApplyUnReadCount()
|
||||
|
||||
+9
-2
@@ -206,10 +206,16 @@ export const useGroupStore = defineStore(
|
||||
return groupDetails.value.find((item: GroupDetailReq) => item.roomId === globalStore.currentSession!.roomId)
|
||||
})
|
||||
|
||||
const getGroupDetailByRoomId = computed(() => (roomId: string) => {
|
||||
return groupDetails.value.find((item: GroupDetailReq) => item.roomId === roomId)
|
||||
})
|
||||
|
||||
const updateGroupNumber = (roomId: string, totalNum: number, onlineNum: number) => {
|
||||
const group = groupDetails.value.find((item) => item.roomId === roomId)
|
||||
group!.memberNum = totalNum
|
||||
group!.onlineNum = onlineNum
|
||||
if (group) {
|
||||
group.memberNum = totalNum
|
||||
group.onlineNum = onlineNum
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -526,6 +532,7 @@ export const useGroupStore = defineStore(
|
||||
setGroupDetails,
|
||||
updateGroupDetail,
|
||||
groupDetails,
|
||||
getGroupDetailByRoomId,
|
||||
updateGroupNumber,
|
||||
removeGroupDetail,
|
||||
addGroupDetail,
|
||||
|
||||
+12
-2
@@ -37,7 +37,7 @@ export const useSettingStore = defineStore(StoresEnum.SETTING, {
|
||||
chat: {
|
||||
sendKey: 'Enter',
|
||||
isDouble: true,
|
||||
translate: 'tencent'
|
||||
translate: 'youdao'
|
||||
},
|
||||
shortcuts: getDefaultShortcuts(),
|
||||
page: {
|
||||
@@ -50,6 +50,9 @@ export const useSettingStore = defineStore(StoresEnum.SETTING, {
|
||||
},
|
||||
screenshot: {
|
||||
isConceal: false
|
||||
},
|
||||
notification: {
|
||||
messageSound: true
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
@@ -98,7 +101,7 @@ export const useSettingStore = defineStore(StoresEnum.SETTING, {
|
||||
/** 设置发送消息快捷键 */
|
||||
setSendMessageShortcut(shortcut: string) {
|
||||
if (!this.chat) {
|
||||
this.chat = { sendKey: 'Enter', isDouble: true, translate: 'tencent' }
|
||||
this.chat = { sendKey: 'Enter', isDouble: true, translate: 'youdao' }
|
||||
}
|
||||
this.chat.sendKey = shortcut
|
||||
},
|
||||
@@ -118,6 +121,13 @@ export const useSettingStore = defineStore(StoresEnum.SETTING, {
|
||||
this.screenshot = { isConceal: false }
|
||||
}
|
||||
this.screenshot.isConceal = isConceal
|
||||
},
|
||||
/** 设置消息提示音开关 */
|
||||
setMessageSoundEnabled(enabled: boolean) {
|
||||
if (!this.notification) {
|
||||
this.notification = { messageSound: true }
|
||||
}
|
||||
this.notification.messageSound = enabled
|
||||
}
|
||||
},
|
||||
share: {
|
||||
|
||||
+5
-1
@@ -18,7 +18,11 @@ export const useUserStore = defineStore(
|
||||
})
|
||||
}
|
||||
|
||||
return { userInfo, getUserDetailAction }
|
||||
const isMe = computed(() => (id: string) => {
|
||||
return userInfo.value?.uid === id
|
||||
})
|
||||
|
||||
return { userInfo, getUserDetailAction, isMe }
|
||||
},
|
||||
{
|
||||
share: {
|
||||
|
||||
@@ -59,7 +59,7 @@ abstract class AbstractMessageStrategy implements MessageStrategy {
|
||||
body: messageBody,
|
||||
messageMarks: {}
|
||||
},
|
||||
sendTime: new Date(currentTime).toISOString(),
|
||||
sendTime: Date.now(),
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+9
-1
@@ -16,4 +16,12 @@ body {
|
||||
left: 0;
|
||||
transition: all 0.9s ease;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-max-width{
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.chat-bot-message-gap{
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
Vendored
+8
@@ -27,3 +27,11 @@
|
||||
.n-message-container.n-message-container--top {
|
||||
top: var(--safe-area-inset-top);
|
||||
}
|
||||
|
||||
.chat-message-max-width{
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.chat-bot-message-gap{
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
Vendored
+2
-1
@@ -37,7 +37,7 @@ svg {
|
||||
--action-bar-icon-color: #404040;
|
||||
--action-bar-icon-hover: #e7e7e7;
|
||||
// 搜索框背景颜色
|
||||
--search-bg-color: #f3f3f3;
|
||||
--search-bg-color: #EAEAEA;
|
||||
// 消息列表样式
|
||||
--bg-msg-first-child: #eeeeee;
|
||||
--bg-msg-hover: #f3f3f3;
|
||||
@@ -129,6 +129,7 @@ svg {
|
||||
--safe-area-inset-right: 0px;
|
||||
--safe-area-inset-bottom: 16px;
|
||||
--safe-area-inset-left: 0px;
|
||||
--van-dialog-confirm-button-text-color: #169b80;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
|
||||
Vendored
+12
-8
@@ -19,6 +19,8 @@ declare module 'vue' {
|
||||
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']
|
||||
ChatMsgMultiChoose: typeof import('./../components/rightBox/chatBox/ChatMsgMultiChoose.vue')['default']
|
||||
ChatMultiMsg: typeof import('./../components/rightBox/chatBox/ChatMultiMsg.vue')['default']
|
||||
ChatSidebar: typeof import('./../components/rightBox/chatBox/ChatSidebar.vue')['default']
|
||||
Community: typeof import('./../mobile/components/my/community.vue')['default']
|
||||
CommunityContent: typeof import('./../mobile/components/community/CommunityContent.vue')['default']
|
||||
@@ -36,11 +38,13 @@ declare module 'vue' {
|
||||
InfoPopover: typeof import('./../components/common/InfoPopover.vue')['default']
|
||||
LoadingSpinner: typeof import('./../components/common/LoadingSpinner.vue')['default']
|
||||
MeasuredItem: typeof import('./../mobile/components/virtual-scroll/MeasuredItem.vue')['default']
|
||||
MergeMessage: typeof import('./../components/rightBox/renderMessage/MergeMessage.vue')['default']
|
||||
MessageContainer: typeof import('./../mobile/components/chat-room/MessageContainer.vue')['default']
|
||||
MessageContent: typeof import('./../components/rightBox/chatBox/MessageContent.vue')['default']
|
||||
MobileApplyList: typeof import('./../mobile/components/my/MobileApplyList.vue')['default']
|
||||
MsgInput: typeof import('./../components/rightBox/MsgInput.vue')['default']
|
||||
MyMessageItem: typeof import('./../mobile/components/my/MyMessageItem.vue')['default']
|
||||
NaiveProvider: typeof import('./../components/common/NaiveProvider.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NAvatarGroup: typeof import('naive-ui')['NAvatarGroup']
|
||||
@@ -55,13 +59,13 @@ declare module 'vue' {
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NFloatButton: typeof import('naive-ui')['NFloatButton']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NHighlight: typeof import('naive-ui')['NHighlight']
|
||||
@@ -80,6 +84,7 @@ declare module 'vue' {
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NPopselect: typeof import('naive-ui')['NPopselect']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
@@ -88,6 +93,10 @@ declare module 'vue' {
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSpliter: typeof import('naive-ui')['NSpliter']
|
||||
NSplitter: typeof import('naive-ui')['NSplitter']
|
||||
NStep: typeof import('naive-ui')['NStep']
|
||||
NSteps: typeof import('naive-ui')['NSteps']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
@@ -111,16 +120,11 @@ declare module 'vue' {
|
||||
Settings: typeof import('./../mobile/components/my/Settings.vue')['default']
|
||||
ShareModal: typeof import('./../mobile/components/my/ShareModal.vue')['default']
|
||||
SmartVirtualList: typeof import('./../mobile/components/virtual-scroll/SmartVirtualList.vue')['default']
|
||||
SwipContainer: typeof import('./../mobile/components/swipe/SwipContainer.vue')['default']
|
||||
SwipeContainer: typeof import('./../mobile/components/swipe/SwipeContainer.vue')['default']
|
||||
SystemMessage: typeof import('./../components/rightBox/renderMessage/special/SystemMessage.vue')['default']
|
||||
Text: typeof import('./../components/rightBox/renderMessage/Text.vue')['default']
|
||||
Validation: typeof import('./../components/common/Validation.vue')['default']
|
||||
VanCell: typeof import('vant/es')['Cell']
|
||||
VanCellGroup: typeof import('vant/es')['CellGroup']
|
||||
VanFloatingPopup: typeof import('vant/es')['FloatingPopup']
|
||||
VanPasswordInput: typeof import('vant/es')['PasswordInput']
|
||||
VanSwipeCell: typeof import('vant/es')['SwipeCell']
|
||||
VanUploader: typeof import('vant/es')['Uploader']
|
||||
Video: typeof import('./../components/rightBox/renderMessage/Video.vue')['default']
|
||||
VideoCall: typeof import('./../components/rightBox/renderMessage/VideoCall.vue')['default']
|
||||
VirtualList: typeof import('./../components/common/VirtualList.vue')['default']
|
||||
|
||||
Vendored
+16
@@ -20,3 +20,19 @@ declare namespace Common {
|
||||
|
||||
/** 构建时间 */
|
||||
declare const PROJECT_BUILD_TIME: string
|
||||
|
||||
export type ProxySettings = {
|
||||
apiType: string
|
||||
apiIp: string
|
||||
apiPort: string
|
||||
apiSuffix: string
|
||||
wsType: string
|
||||
wsIp: string
|
||||
wsPort: string
|
||||
wsSuffix: string
|
||||
}
|
||||
|
||||
export type MsgId = {
|
||||
msgId: string
|
||||
fromUid: string
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -80,6 +80,11 @@ declare namespace STO {
|
||||
/** 截图时是否隐藏窗口 */
|
||||
isConceal: boolean
|
||||
}
|
||||
/** 消息通知设置 */
|
||||
notification: {
|
||||
/** 是否开启消息提示音 */
|
||||
messageSound: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,21 +2,14 @@ import dayjs from 'dayjs'
|
||||
import weekday from 'dayjs/plugin/weekday'
|
||||
import 'dayjs/locale/zh-cn' // 导入中文语言包
|
||||
import type { ConfigType, Dayjs, OpUnitType } 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) => {
|
||||
export const timeToStr = (time: number) => {
|
||||
const sendTime = dayjs(time)
|
||||
// 计算今天和消息的发送时间间隔多少天
|
||||
const gapDay = dayjs().endOf('day').diff(sendTime, 'day')
|
||||
@@ -30,36 +23,6 @@ const timeToStr = (time: number) => {
|
||||
: 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 时间戳
|
||||
|
||||
@@ -129,10 +129,10 @@ export async function getUserDetail() {
|
||||
/**
|
||||
* 快捷方法:获取群组详情
|
||||
*/
|
||||
export async function getGroupDetail(groupId: string) {
|
||||
export async function getGroupDetail(roomId: string) {
|
||||
return await imRequest({
|
||||
url: ImUrlEnum.GROUP_DETAIL,
|
||||
params: { id: groupId }
|
||||
params: { id: roomId }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,10 +215,10 @@ export async function getBadgesBatch(body: CacheBadgeReq[]) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function getMsgList(params: any) {
|
||||
export async function getMsgList(body: { msgIds?: string[]; lastOptTime?: number }) {
|
||||
return await imRequest({
|
||||
url: ImUrlEnum.GET_MSG_LIST,
|
||||
params
|
||||
body
|
||||
})
|
||||
}
|
||||
|
||||
@@ -318,6 +318,13 @@ export async function sendAddFriendRequest(body: { targetUid: string; msg: strin
|
||||
})
|
||||
}
|
||||
|
||||
export async function requestNoticePage(params: { pageSize: number; pageNo: number; cursor: string }) {
|
||||
return await imRequest({
|
||||
url: ImUrlEnum.REQUEST_NOTICE_PAGE,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export async function handleInvite(body: { applyId: string; state: number }) {
|
||||
return await imRequest({
|
||||
url: ImUrlEnum.HANDLE_INVITE,
|
||||
@@ -542,3 +549,17 @@ export async function forgetPassword(body: {
|
||||
body
|
||||
})
|
||||
}
|
||||
|
||||
export async function mergeMsg(body: { fromRoomId: string; type: number; roomIds: string[]; messageIds: string[] }) {
|
||||
return await imRequest({
|
||||
url: ImUrlEnum.MERGE_MSG,
|
||||
body
|
||||
})
|
||||
}
|
||||
|
||||
export async function getUserByIds(body: { uidList: string[] }) {
|
||||
return await imRequest({
|
||||
url: ImUrlEnum.GET_USER_BY_IDS,
|
||||
body
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,11 +8,7 @@
|
||||
:current-label="WebviewWindow.getCurrent().label" />
|
||||
|
||||
<!-- 标题 -->
|
||||
<p
|
||||
class="absolute-x-center h-fit pt-6px text-(13px [--text-color]) select-none cursor-default"
|
||||
data-tauri-drag-region>
|
||||
申请加好友
|
||||
</p>
|
||||
<p class="absolute-x-center h-fit pt-6px text-(13px [--text-color]) select-none cursor-default">申请加好友</p>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="bg-[--bg-edit] w-380px h-full box-border flex flex-col">
|
||||
|
||||
@@ -107,6 +107,8 @@
|
||||
<script setup lang="ts">
|
||||
import { lightTheme } from 'naive-ui'
|
||||
import router from '@/router'
|
||||
import { updateSettings } from '@/services/tauriCommand'
|
||||
import type { ProxySettings } from '@/typings/global'
|
||||
|
||||
const apiOptions = [
|
||||
{
|
||||
@@ -192,7 +194,7 @@ const handleSave = async () => {
|
||||
apiType: savedProxy.apiType,
|
||||
apiIp: savedProxy.apiIp,
|
||||
apiPort: savedProxy.apiPort,
|
||||
apiSuffix: savedProxy.apiSuffix
|
||||
apiSuffix: savedProxy.apiSuffix ? '/' + savedProxy.apiSuffix : ''
|
||||
}
|
||||
} else {
|
||||
// 保存到本地存储
|
||||
@@ -201,20 +203,29 @@ const handleSave = async () => {
|
||||
wsType: savedProxy.wsType,
|
||||
wsIp: savedProxy.wsIp,
|
||||
wsPort: savedProxy.wsPort,
|
||||
wsSuffix: savedProxy.wsSuffix
|
||||
wsSuffix: savedProxy.wsSuffix ? '/' + savedProxy.wsSuffix : ''
|
||||
}
|
||||
}
|
||||
const settings = JSON.stringify(proxySettings)
|
||||
localStorage.setItem('proxySettings', settings)
|
||||
await updateTauriSettings(proxySettings)
|
||||
console.log('settings', proxySettings)
|
||||
|
||||
window.$message.success('网络设置保存成功')
|
||||
setTimeout(() => {
|
||||
location.reload()
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
window.$message.error('网络设置失败:' + error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateTauriSettings = async (proxySettings: ProxySettings) => {
|
||||
const baseUrl =
|
||||
proxySettings.apiType + '://' + proxySettings.apiIp + ':' + proxySettings.apiPort + proxySettings.apiSuffix
|
||||
const wsUrl = proxySettings.wsType + '://' + proxySettings.wsIp + ':' + proxySettings.wsPort + proxySettings.wsSuffix
|
||||
|
||||
await updateSettings({ baseUrl, wsUrl }).catch((err) => {
|
||||
window.$message.error('网络设置失败:' + err)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
<template>
|
||||
<n-flex vertical :size="40">
|
||||
<!-- 消息通知设置 -->
|
||||
<n-flex vertical class="text-(14px [--text-color])" :size="16">
|
||||
<span class="pl-10px">提示音</span>
|
||||
|
||||
<n-flex class="item p-12px" :size="12" vertical>
|
||||
<!-- 消息提示音 -->
|
||||
<n-flex align="center" justify="space-between">
|
||||
<n-flex vertical :size="8">
|
||||
<span>消息提示音</span>
|
||||
<span class="text-(12px #909090)">开启后,收到新消息时会播放提示音</span>
|
||||
</n-flex>
|
||||
|
||||
<n-switch size="small" v-model:value="messageSound" />
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<!-- 群消息设置 -->
|
||||
<n-flex vertical class="text-(14px [--text-color])" :size="16">
|
||||
<n-flex align="center" justify="space-between" class="pl-10px pr-10px">
|
||||
<span>群消息设置</span>
|
||||
<n-input v-model:value="searchKeyword" size="small" placeholder="搜索群聊" clearable style="width: 200px">
|
||||
<template #prefix>
|
||||
<svg class="size-14px"><use href="#search"></use></svg>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-flex>
|
||||
|
||||
<n-flex class="item" :size="0" vertical>
|
||||
<div v-if="filteredGroupSessions.length === 0" class="text-(12px #909090) text-center py-20px">
|
||||
{{ searchKeyword ? '未找到匹配的群聊' : '暂无群聊' }}
|
||||
</div>
|
||||
|
||||
<!-- 批量操作栏 -->
|
||||
<n-flex v-if="filteredGroupSessions.length > 0" align="center" justify="space-between" class="p-12px h-28px">
|
||||
<n-flex align="center" :size="12">
|
||||
<n-checkbox
|
||||
v-model:checked="selectAll"
|
||||
:indeterminate="selectedSessions.length > 0 && selectedSessions.length < filteredGroupSessions.length"
|
||||
@update:checked="handleSelectAll">
|
||||
全选 ({{ selectedSessions.length }}/{{ filteredGroupSessions.length }})
|
||||
</n-checkbox>
|
||||
</n-flex>
|
||||
|
||||
<n-flex v-if="selectedSessions.length > 0" align="center" :size="8">
|
||||
<span class="text-(12px #909090)">批量设置:</span>
|
||||
<n-button
|
||||
size="small"
|
||||
type="primary"
|
||||
secondary
|
||||
:disabled="isProcessing"
|
||||
@click="batchSetNotification('allow')">
|
||||
允许提醒
|
||||
</n-button>
|
||||
<n-button size="small" secondary :disabled="isProcessing" @click="batchSetNotification('mute')">
|
||||
免打扰
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="error"
|
||||
secondary
|
||||
:disabled="isProcessing"
|
||||
@click="batchSetNotification('shield')">
|
||||
屏蔽
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<!-- 进度条显示 -->
|
||||
<n-flex v-if="isProcessing" vertical :size="12" class="p-12px">
|
||||
<n-flex align="center" justify="space-between">
|
||||
<span class="text-(12px #909090)">正在处理:{{ processedCount }}/{{ totalCount }}</span>
|
||||
<span class="text-(12px #909090)">{{ progress }}%</span>
|
||||
</n-flex>
|
||||
<n-progress
|
||||
type="line"
|
||||
:color="'#13987f'"
|
||||
:rail-color="'#13987f30'"
|
||||
:percentage="progress"
|
||||
:show-indicator="false" />
|
||||
</n-flex>
|
||||
|
||||
<span v-if="filteredGroupSessions.length > 0" class="w-full h-1px bg-[--line-color] block"></span>
|
||||
|
||||
<n-scrollbar
|
||||
style="max-height: 420px; padding: 0 12px; box-sizing: border-box"
|
||||
:style="{ pointerEvents: isDropdownShow ? 'none' : 'auto' }">
|
||||
<template v-for="(session, index) in filteredGroupSessions" :key="session.roomId">
|
||||
<n-flex align="center" justify="space-between" class="py-14px">
|
||||
<n-flex align="center" :size="12">
|
||||
<n-checkbox
|
||||
:checked="selectedSessions.includes(session.roomId)"
|
||||
@update:checked="(checked) => handleSessionSelect(session.roomId, checked)" />
|
||||
<img
|
||||
:src="AvatarUtils.getAvatarUrl(session.avatar) || '/imgs/avatar.png'"
|
||||
:alt="session.name"
|
||||
class="w-32px h-32px rounded-6px object-cover" />
|
||||
<n-flex vertical :size="2">
|
||||
<span class="text-14px">{{ session.name }}</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<n-dropdown
|
||||
:options="getNotificationOptions(session)"
|
||||
@select="(key) => handleNotificationChange(session, key)"
|
||||
trigger="click"
|
||||
:scrollable="false"
|
||||
@update:show="(show) => (isDropdownShow = show)">
|
||||
<n-button size="small" :color="'#13987f'" text class="text-(12px [--text-color])">
|
||||
{{ getNotificationStatusText(session) }}
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</n-flex>
|
||||
|
||||
<span v-if="index < filteredGroupSessions.length - 1" class="w-full h-1px bg-[--line-color] block"></span>
|
||||
</template>
|
||||
</n-scrollbar>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NotificationTypeEnum, RoomTypeEnum } from '@/enums'
|
||||
import type { SessionItem } from '@/services/types'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useSettingStore } from '@/stores/setting'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { notification, shield } from '@/utils/ImRequestUtils'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 选中的会话列表
|
||||
const selectedSessions = ref<string[]>([])
|
||||
|
||||
// 批量操作进度状态
|
||||
const isProcessing = ref(false)
|
||||
const progress = ref(0)
|
||||
const processedCount = ref(0)
|
||||
const totalCount = ref(0)
|
||||
const processingResults = ref<{ roomId: string; name: string; success: boolean; error?: string }[]>([])
|
||||
|
||||
// 下拉菜单显示状态
|
||||
const isDropdownShow = ref(false)
|
||||
|
||||
const messageSound = computed({
|
||||
get: () => settingStore.notification.messageSound,
|
||||
set: (value: boolean) => {
|
||||
settingStore.setMessageSoundEnabled(value)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取所有群聊会话(包含官方频道)
|
||||
const groupSessions = computed(() => {
|
||||
return chatStore.sessionList.filter((session) => session.type === RoomTypeEnum.GROUP)
|
||||
})
|
||||
|
||||
// 过滤后的群聊列表(搜索功能)
|
||||
const filteredGroupSessions = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return groupSessions.value
|
||||
}
|
||||
return groupSessions.value.filter((session) => session.name.toLowerCase().includes(searchKeyword.value.toLowerCase()))
|
||||
})
|
||||
|
||||
// 全选状态
|
||||
const selectAll = computed({
|
||||
get: () =>
|
||||
selectedSessions.value.length === filteredGroupSessions.value.length && filteredGroupSessions.value.length > 0,
|
||||
set: (value: boolean) => {
|
||||
if (value) {
|
||||
selectedSessions.value = filteredGroupSessions.value.map((session) => session.roomId)
|
||||
} else {
|
||||
selectedSessions.value = []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 获取通知状态文本
|
||||
const getNotificationStatusText = (session: SessionItem) => {
|
||||
if (session.shield) {
|
||||
return '已屏蔽消息'
|
||||
}
|
||||
|
||||
switch (session.muteNotification) {
|
||||
case NotificationTypeEnum.RECEPTION:
|
||||
return '允许消息提醒'
|
||||
case NotificationTypeEnum.NOT_DISTURB:
|
||||
return '接收消息但不提醒'
|
||||
default:
|
||||
return '允许消息提醒'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取下拉菜单选项
|
||||
const getNotificationOptions = (session: SessionItem) => {
|
||||
return [
|
||||
{
|
||||
label: '允许消息提醒',
|
||||
key: 'allow',
|
||||
icon:
|
||||
!session.shield && session.muteNotification === NotificationTypeEnum.RECEPTION
|
||||
? () => h('svg', { class: 'size-14px text-#13987f' }, [h('use', { href: '#check-small' })])
|
||||
: undefined
|
||||
},
|
||||
{
|
||||
label: '接收消息但不提醒',
|
||||
key: 'mute',
|
||||
icon:
|
||||
!session.shield && session.muteNotification === NotificationTypeEnum.NOT_DISTURB
|
||||
? () => h('svg', { class: 'size-14px text-#13987f' }, [h('use', { href: '#check-small' })])
|
||||
: undefined
|
||||
},
|
||||
{
|
||||
label: '屏蔽群消息',
|
||||
key: 'shield',
|
||||
icon: session.shield
|
||||
? () => h('svg', { class: 'size-14px text-#13987f' }, [h('use', { href: '#check-small' })])
|
||||
: undefined
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 处理全选
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
selectAll.value = checked
|
||||
}
|
||||
|
||||
// 处理单个会话选择
|
||||
const handleSessionSelect = (roomId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
if (!selectedSessions.value.includes(roomId)) {
|
||||
selectedSessions.value.push(roomId)
|
||||
}
|
||||
} else {
|
||||
const index = selectedSessions.value.indexOf(roomId)
|
||||
if (index > -1) {
|
||||
selectedSessions.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量设置通知
|
||||
const batchSetNotification = async (type: string) => {
|
||||
if (selectedSessions.value.length === 0) {
|
||||
window.$message?.warning('请先选择要设置的群聊')
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化进度状态
|
||||
isProcessing.value = true
|
||||
progress.value = 0
|
||||
processedCount.value = 0
|
||||
totalCount.value = selectedSessions.value.length
|
||||
processingResults.value = []
|
||||
|
||||
try {
|
||||
for (let i = 0; i < selectedSessions.value.length; i++) {
|
||||
const roomId = selectedSessions.value[i]
|
||||
const session = groupSessions.value.find((s) => s.roomId === roomId)
|
||||
|
||||
if (!session) {
|
||||
processingResults.value.push({
|
||||
roomId,
|
||||
name: '未知群聊',
|
||||
success: false,
|
||||
error: '找不到群聊信息'
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
await handleNotificationChange(session, type)
|
||||
processingResults.value.push({
|
||||
roomId,
|
||||
name: session.name,
|
||||
success: true
|
||||
})
|
||||
} catch (error: any) {
|
||||
processingResults.value.push({
|
||||
roomId,
|
||||
name: session.name,
|
||||
success: false,
|
||||
error: error.message || '设置失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
processedCount.value = i + 1
|
||||
progress.value = Math.round(((i + 1) / selectedSessions.value.length) * 100)
|
||||
}
|
||||
|
||||
// 显示结果统计
|
||||
const successCount = processingResults.value.filter((r) => r.success).length
|
||||
const failCount = processingResults.value.length - successCount
|
||||
|
||||
const typeText =
|
||||
{
|
||||
allow: '允许消息提醒',
|
||||
mute: '免打扰',
|
||||
shield: '屏蔽'
|
||||
}[type] || '未知'
|
||||
|
||||
if (failCount === 0) {
|
||||
window.$message?.success(`批量设置完成!${successCount} 个群聊已设置为${typeText}`)
|
||||
} else {
|
||||
window.$message?.warning(`批量设置完成:${successCount} 个成功,${failCount} 个失败`)
|
||||
}
|
||||
|
||||
// 清空选择
|
||||
selectedSessions.value = []
|
||||
} catch (error) {
|
||||
console.error('批量设置失败:', error)
|
||||
window.$message?.error('批量设置失败,请重试')
|
||||
} finally {
|
||||
// 延迟隐藏进度条,让用户看到完成状态
|
||||
setTimeout(() => {
|
||||
isProcessing.value = false
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理通知设置变更
|
||||
const handleNotificationChange = async (session: SessionItem, key: string) => {
|
||||
try {
|
||||
switch (key) {
|
||||
case 'allow':
|
||||
// 如果当前是屏蔽状态,需要先取消屏蔽
|
||||
if (session.shield) {
|
||||
await shield({
|
||||
roomId: session.roomId,
|
||||
state: false
|
||||
})
|
||||
chatStore.updateSession(session.roomId, { shield: false })
|
||||
}
|
||||
|
||||
await notification({
|
||||
roomId: session.roomId,
|
||||
type: NotificationTypeEnum.RECEPTION
|
||||
})
|
||||
|
||||
chatStore.updateSession(session.roomId, {
|
||||
muteNotification: NotificationTypeEnum.RECEPTION
|
||||
})
|
||||
|
||||
window.$message?.success('已允许消息提醒')
|
||||
break
|
||||
|
||||
case 'mute':
|
||||
// 如果当前是屏蔽状态,需要先取消屏蔽
|
||||
if (session.shield) {
|
||||
await shield({
|
||||
roomId: session.roomId,
|
||||
state: false
|
||||
})
|
||||
chatStore.updateSession(session.roomId, { shield: false })
|
||||
}
|
||||
|
||||
await notification({
|
||||
roomId: session.roomId,
|
||||
type: NotificationTypeEnum.NOT_DISTURB
|
||||
})
|
||||
|
||||
chatStore.updateSession(session.roomId, {
|
||||
muteNotification: NotificationTypeEnum.NOT_DISTURB
|
||||
})
|
||||
|
||||
// 设置免打扰时更新全局未读数
|
||||
chatStore.updateTotalUnreadCount()
|
||||
window.$message?.success('已设置接收消息但不提醒')
|
||||
break
|
||||
|
||||
case 'shield':
|
||||
await shield({
|
||||
roomId: session.roomId,
|
||||
state: !session.shield
|
||||
})
|
||||
|
||||
chatStore.updateSession(session.roomId, {
|
||||
shield: !session.shield
|
||||
})
|
||||
|
||||
window.$message?.success(session.shield ? '已取消屏蔽' : '已屏蔽消息')
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('设置群消息通知失败:', error)
|
||||
window.$message?.error('设置失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听搜索关键词变化,清空选择
|
||||
watch(searchKeyword, () => {
|
||||
selectedSessions.value = []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item {
|
||||
@apply bg-[--bg-setting-item] rounded-12px size-full box-border border-(solid 1px [--line-color]) custom-shadow;
|
||||
}
|
||||
|
||||
.n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--vertical > .n-scrollbar-rail__scrollbar,
|
||||
.n-scrollbar + .n-scrollbar-rail.n-scrollbar-rail--vertical > .n-scrollbar-rail__scrollbar {
|
||||
left: -2px;
|
||||
}
|
||||
</style>
|
||||
@@ -12,15 +12,20 @@ const sideOptions = ref<OPT.L.SettingSide[]>([
|
||||
icon: 'setting-config'
|
||||
},
|
||||
{
|
||||
url: '/manageStore',
|
||||
label: '存储管理',
|
||||
icon: 'mini-sd-card'
|
||||
url: '/notification',
|
||||
label: '消息通知',
|
||||
icon: 'remind'
|
||||
},
|
||||
{
|
||||
url: '/shortcut',
|
||||
label: '快捷键管理',
|
||||
icon: 'enter-the-keyboard'
|
||||
},
|
||||
{
|
||||
url: '/manageStore',
|
||||
label: '存储管理',
|
||||
icon: 'mini-sd-card'
|
||||
},
|
||||
{
|
||||
url: '/loginSetting',
|
||||
label: '登录设置',
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<ActionBar :shrink="false" :max-w="false" />
|
||||
<div v-for="msg in msgs" :key="msg.message.id" class="flex ml-8 py-2">
|
||||
<n-avatar
|
||||
class="rounded-8px flex-shrink-0 mr-2"
|
||||
:size="30"
|
||||
:src="AvatarUtils.getAvatarUrl(computedUser(msg.fromUser.uid)!.avatar)" />
|
||||
<div class="flex-1">
|
||||
<div>{{ computedUser(msg.fromUser.uid)?.name }}</div>
|
||||
<p class="w-100% whitespace-pre-wrap break-words">{{ msg.message.body.content }}</p>
|
||||
<n-divider class="!my-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWindow } from '@/hooks/useWindow'
|
||||
import type { MessageType, UserItem } from '@/services/types'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { getMsgList, getUserByIds } from '@/utils/ImRequestUtils'
|
||||
|
||||
type Msg = {
|
||||
msgId: string
|
||||
fromUid: string
|
||||
}
|
||||
|
||||
const { getWindowPayload } = useWindow()
|
||||
const choosedMsgs = ref<Msg[]>([])
|
||||
const msgs = ref<MessageType[]>()
|
||||
const users = ref<UserItem[]>()
|
||||
const route = useRoute()
|
||||
|
||||
const computedUser = computed(() => (uid: string) => {
|
||||
return users.value?.find((user) => user.uid === uid)
|
||||
})
|
||||
|
||||
const getAllMsg = async () => {
|
||||
const msgIds = choosedMsgs.value.map((msg) => msg.msgId)
|
||||
await getMsgList({ msgIds }).then((data) => {
|
||||
msgs.value = data
|
||||
})
|
||||
}
|
||||
|
||||
const getAllUserInfo = async () => {
|
||||
const uids = choosedMsgs.value.map((msg) => msg.fromUid)
|
||||
await getUserByIds({ uidList: uids }).then((data) => {
|
||||
users.value = data
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getWindowPayload<Msg[]>(route.query.key as string)
|
||||
.then(async (data) => {
|
||||
choosedMsgs.value = data
|
||||
await getAllMsg()
|
||||
await getAllUserInfo()
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
|
||||
+33
-11
@@ -4,6 +4,7 @@ import terser from '@rollup/plugin-terser'
|
||||
import UnoCSS from '@unocss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import { internalIpV4 } from 'internal-ip'
|
||||
import postcsspxtorem from 'postcss-pxtorem'
|
||||
import AutoImport from 'unplugin-auto-import/vite' //自动导入
|
||||
import { NaiveUiResolver, VantResolver } from 'unplugin-vue-components/resolvers'
|
||||
@@ -16,12 +17,35 @@ import { getRootPath, getSrcPath } from './build/config/getPath'
|
||||
const packageJson = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8'))
|
||||
const dependencies = Object.keys(packageJson.dependencies || {})
|
||||
|
||||
// 预先获取本地IP
|
||||
const rawIP = await internalIpV4()
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
/**! 不需要优化前端打包(如开启gzip) */
|
||||
export default defineConfig(({ mode }: ConfigEnv) => {
|
||||
// 获取当前环境的配置,如何设置第三个参数则加载所有变量,而不是以"VITE_"前缀的变量
|
||||
const config = loadEnv(mode, process.cwd(), '')
|
||||
const host = config.TAURI_DEV_HOST
|
||||
const isPC =
|
||||
config.TAURI_ENV_PLATFORM === 'windows' ||
|
||||
config.TAURI_ENV_PLATFORM === 'darwin' ||
|
||||
config.TAURI_ENV_PLATFORM === 'linux'
|
||||
|
||||
// 根据平台决定host地址
|
||||
const host = (() => {
|
||||
if (isPC) {
|
||||
return '127.0.0.1'
|
||||
}
|
||||
|
||||
// 移动端逻辑:检查是否为有效的内网IP地址
|
||||
if (rawIP && !rawIP.endsWith('.0') && !rawIP.endsWith('.255')) {
|
||||
return rawIP // 有效IP且非网段/广播地址
|
||||
}
|
||||
|
||||
// 无效IP或特殊地址的情况
|
||||
return config.TAURI_ENV_PLATFORM === 'android' ? '0.0.0.0' : '127.0.0.1'
|
||||
})()
|
||||
|
||||
console.log('Host:', host)
|
||||
|
||||
return {
|
||||
resolve: {
|
||||
@@ -90,11 +114,18 @@ export default defineConfig(({ mode }: ConfigEnv) => {
|
||||
})
|
||||
],
|
||||
build: {
|
||||
target: ['chrome87', 'edge88', 'firefox78', 'safari14'], // 兼容低版本浏览器
|
||||
// 设置兼容低版本浏览器的目标
|
||||
target: ['chrome87', 'edge88', 'firefox78', 'safari14'],
|
||||
cssCodeSplit: true, // 启用 CSS 代码拆分
|
||||
minify: 'terser', // 指定使用哪种混淆器
|
||||
// chunk 大小警告的限制(kb)
|
||||
chunkSizeWarningLimit: 1200,
|
||||
// esbuild配置,解决低版本浏览器兼容性问题
|
||||
esbuild: {
|
||||
supported: {
|
||||
'top-level-await': false
|
||||
}
|
||||
},
|
||||
// 分包配置
|
||||
rollupOptions: {
|
||||
output: {
|
||||
@@ -123,15 +154,6 @@ 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: {
|
||||
protocol: 'ws',
|
||||
host: host,
|
||||
|
||||
Reference in New Issue
Block a user