@ -35,6 +35,8 @@ module.exports = {
|
||||
plugins: ['@typescript-eslint', 'vue'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
/* 允许ts使用命名空间 */
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
/**
|
||||
* 使用新vue3.3的defineProps解构语法需要关闭这个校验
|
||||
* eslint-plugin-vue新版本已废弃 vue/no-setup-props-destructure,如果新版本eslint-plugin-vue
|
||||
|
@ -1,3 +1,11 @@
|
||||
## 1.3.0-beta (2024-03-12)
|
||||
|
||||
* :bug: fix(custom): 修改页面样式没有加载问题,修复打包后静态文件路径问题 ([63ce821](https://gitee.com/nongyehong/HuLa-IM-Tauri/commits/63ce821))
|
||||
* :pencil2: docs(style): 更新README文档 ([a4c1712](https://gitee.com/nongyehong/HuLa-IM-Tauri/commits/a4c1712))
|
||||
* :zap: perf(system): 使用原生event进行窗口通信 ([8adefce](https://gitee.com/nongyehong/HuLa-IM-Tauri/commits/8adefce))
|
||||
|
||||
|
||||
|
||||
## <small>1.2.9-alpha (2024-03-08)</small>
|
||||
|
||||
* :bug: fix(custom): 回退vite版本5.0.12导致启动不了端口服务的问题 ([ba99bb2](https://gitee.com/nongyehong/HuLa-IM-Tauri/commits/ba99bb2))
|
||||
|
28
README.md
@ -1,10 +1,30 @@
|
||||
<p align="center">
|
||||
<img width="144px" src="logo.png" />
|
||||
<img width="144px" src="public/logo.png" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">HuLa</h1>
|
||||
<p align="center">An Instant Messaging System Built with Tauri, Vite 5, Vue 3, and TypeScript</p>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/badge/TypeScript-blue?logo=Typescript&style=flat&logoColor=fff">
|
||||
<img src="https://img.shields.io/badge/Vue3-35495E?logo=vue.js&logoColor=4FC08D">
|
||||
<img src="https://img.shields.io/badge/Tauri-24C8DB?logo=tauri&logoColor=FFC131">
|
||||
<img src="https://img.shields.io/badge/Rust-c57c54?logo=rust&logoColor=E34F26">
|
||||
<img src="https://img.shields.io/badge/Vite5-35495E?logo=vite&logoColor=41D1FF">
|
||||
<img src="https://img.shields.io/badge/UnoCss-efefef?logo=UnoCss&logoColor=606060">
|
||||
<img src="https://img.shields.io/badge/pnpm-909090?logo=pnpm&logoColor=FFC131">
|
||||
<img src="https://img.shields.io/badge/Sass-CC6699?logo=sass&logoColor=fff">
|
||||
<img src="https://img.shields.io/badge/Design-Naive-059669">
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
gitee:<a target="_blank" href="https://gitee.com/nongyehong/HuLa-IM-Tauri" title="HuLa"><img src="https://img.shields.io/badge/-Gitee-A80025?logo=gitee&logoColor=F16061"></a>
|
||||
github:<a target="_blank" href="https://github.com/nongyehong/HuLa-IM-Tauri" title="HuLa"><img src="https://img.shields.io/badge/-GitHub-181717?style=plastic&logo=github"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
WeChat: <img src="https://img.shields.io/badge/cy2439646234-07C160?logo=wechat&logoColor=fff">
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="README.zh-CN.md">中文</a> | English</p>
|
||||
|
||||
## Project Introduction
|
||||
@ -40,4 +60,8 @@ pnpm install
|
||||
pnpm run tauri:dev
|
||||
|
||||
# Build for production
|
||||
pnpm run tauri:build
|
||||
pnpm run tauri:build
|
||||
```
|
||||
|
||||
## SubmissionSpecification
|
||||
use **pnpm run commit** to invoke the _git commit_ interaction and follow the prompts to complete the input and selection of information
|
@ -1,10 +1,30 @@
|
||||
<p align="center">
|
||||
<img width="144px" src="logo.png" />
|
||||
<img width="144px" src="public/logo.png" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">HuLa</h1>
|
||||
<p align="center">一个基于Tauri、Vite 5、Vue 3 和 TypeScript 构建的即时通讯系统</p>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/badge/TypeScript-blue?logo=Typescript&style=flat&logoColor=fff">
|
||||
<img src="https://img.shields.io/badge/Vue3-35495E?logo=vue.js&logoColor=4FC08D">
|
||||
<img src="https://img.shields.io/badge/Tauri-24C8DB?logo=tauri&logoColor=FFC131">
|
||||
<img src="https://img.shields.io/badge/Rust-c57c54?logo=rust&logoColor=E34F26">
|
||||
<img src="https://img.shields.io/badge/Vite5-35495E?logo=vite&logoColor=41D1FF">
|
||||
<img src="https://img.shields.io/badge/UnoCss-efefef?logo=UnoCss&logoColor=606060">
|
||||
<img src="https://img.shields.io/badge/pnpm-909090?logo=pnpm&logoColor=FFC131">
|
||||
<img src="https://img.shields.io/badge/Sass-CC6699?logo=sass&logoColor=fff">
|
||||
<img src="https://img.shields.io/badge/UI组件库-Naive-059669">
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
gitee:<a target="_blank" href="https://gitee.com/nongyehong/HuLa-IM-Tauri" title="HuLa"><img src="https://img.shields.io/badge/-Gitee-A80025?logo=gitee&logoColor=F16061"></a>
|
||||
github:<a target="_blank" href="https://github.com/nongyehong/HuLa-IM-Tauri" title="HuLa"><img src="https://img.shields.io/badge/-GitHub-181717?style=plastic&logo=github"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
微信: <img src="https://img.shields.io/badge/cy2439646234-07C160?logo=wechat&logoColor=fff">
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="README.md">English</a> | 中文</p>
|
||||
|
||||
## 项目介绍
|
||||
@ -40,4 +60,8 @@ pnpm install
|
||||
pnpm run tauri:dev
|
||||
|
||||
# 构建生产版本
|
||||
pnpm run tauri:build
|
||||
pnpm run tauri:build
|
||||
```
|
||||
|
||||
## 提交规范
|
||||
执行 **pnpm run commit** 唤起 _git commit_ 交互,根据提示完成信息的输入和选择
|
@ -4,10 +4,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + Vue + TS</title>
|
||||
<title>HuLa</title>
|
||||
|
||||
<!--引入iconpark图标库-->
|
||||
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_65.b5d5f1a1bd93fac2f65940a74ac82f80.js"></script>
|
||||
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_70.224ae5c926a3c7f59fc8f9cbe5bfaa9c.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
12
package.json
@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "hula-im-tauri",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "v1.3.0-beta",
|
||||
"packageManager": "pnpm@8.15.4",
|
||||
"engines": {
|
||||
"node": ">=18.12.0",
|
||||
"pnpm": ">=8.10.0"
|
||||
},
|
||||
"type": "module",
|
||||
"author": {
|
||||
"name": "nongyehong",
|
||||
"email": "2439646234@qq.com",
|
||||
"url": "https://xxxx/xxx"
|
||||
"url": "https://github.com/nongyehong/HuLa-IM-Tauri"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,vue,ts,tsx}": [
|
||||
@ -26,7 +26,7 @@
|
||||
"preview": "vite preview",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build",
|
||||
"tauri:icon": "tauri icon logo.png",
|
||||
"tauri:icon": "tauri icon /logo.png",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"commit": "git add . && lint-staged && git-cz && conventional-changelog -p cz-config.cjs -i CHANGELOG.md -s -r 0",
|
||||
"changelog": "conventional-changelog -p cz-config.cjs -i CHANGELOG.md -s -r 0",
|
||||
@ -34,13 +34,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"axios": "^1.6.7",
|
||||
"axios": "^1.6.8",
|
||||
"colorthief": "^2.4.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"naive-ui": "^2.38.1",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"pinia-shared-state": "^0.5.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-draggable-plus": "^0.3.5",
|
||||
"vue-router": "^4.3.0"
|
||||
@ -79,7 +81,7 @@
|
||||
"typescript": "^5.4.2",
|
||||
"unplugin-auto-import": "^0.17.2",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "5.1.4",
|
||||
"vite": "5.2.6",
|
||||
"vue-tsc": "^2.0.6"
|
||||
},
|
||||
"config": {
|
||||
|
783
pnpm-lock.yaml
BIN
public/msg.png
Normal file
After Width: | Height: | Size: 601 B |
BIN
public/status/IonBan.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
public/status/aiziji@2x.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
public/status/bangbangtang@2x.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/status/banzhuan.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
public/status/bequiet@3x.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
public/status/boring@3x.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
public/status/busy.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
public/status/chigua@2x.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
public/status/chuqulang2.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
public/status/cloaking.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
public/status/crush.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/status/eating01@3x.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
public/status/emonew@2x.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
public/status/fish@2x.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
public/status/fullofyuanqi@3x.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
public/status/game_3x.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
public/status/ganzuoye.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
public/status/gototravel.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
public/status/guodongzhi.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
public/status/guonianhuijia.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
public/status/happytofly@3x.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
public/status/hardtosay@3x.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
public/status/imfine_3x.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
public/status/jinli@2x.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
public/status/leave.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
public/status/luck@2x.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
public/status/meizizi@3x.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
public/status/music@2x.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
public/status/nandehutu.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
public/status/offline.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/status/online.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/status/qiuxingdazi.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
public/status/relationship_3x.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
public/status/signal_3x.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
public/status/sleeping_3x.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
public/status/stayup_3x.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
public/status/study_3x.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
public/status/timi_3x.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
public/status/tkong.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
public/status/toohard@3x.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
public/status/tv_3x.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
public/status/wang_3x.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
public/status/weather_3x.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
public/status/woxiangkaile.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
public/status/xiadaxue.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
public/status/xiaojindou.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
public/status/ximao.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
public/status/xinnianhao-xin1.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
public/status/xinnianhao.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
public/status/yiqiyuanmeng.png
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
public/status/youzaizai@3x.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
public/status/yundongzhong@2x.png
Normal file
After Width: | Height: | Size: 11 KiB |
829
src-tauri/Cargo.lock
generated
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "hula-im-tauri"
|
||||
version = "0.0.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
name = "hula"
|
||||
version = "1.3.0"
|
||||
description = "hula"
|
||||
authors = ["nongyehong"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
@ -20,11 +20,12 @@ strip = true # 删除调试符号
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.5", features = [ "macos-private-api", "fs-all", "window-all", "system-tray", "shell-open", "icon-png"] }
|
||||
tauri = { version = "1.6.1", features = [ "notification-all", "macos-private-api", "fs-all", "window-all", "system-tray", "shell-open", "icon-png", "icon-ico"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
window-shadows = "0.2.1" # windows 阴影和圆角
|
||||
window-vibrancy = "0.4.3" # windows 磨砂背景
|
||||
window-shadows = "0.2.2" # windows 阴影和圆角
|
||||
window-vibrancy = "0.4.3" # windows 磨砂背景 tauri-v1.x版本最好使用0.4.3版本
|
||||
tokio = { version = "1.36.0", features = ["macros", "windows-sys", "rt-multi-thread", "time"] }
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
|
@ -1,2 +1,3 @@
|
||||
pub mod tray;
|
||||
pub mod window;
|
||||
pub mod window;
|
||||
pub mod plugins;
|
79
src-tauri/src/common/plugins.rs
Normal file
@ -0,0 +1,79 @@
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
/// 重新设置窗口属性
|
||||
#[tauri::command]
|
||||
pub fn reset_set_window<R: Runtime>(app: tauri::AppHandle<R>, label: String) {
|
||||
let window = app.get_window(&label).unwrap();
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
set_shadow(&window, true).unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
window_vibrancy::apply_vibrancy(&window, NSVisualEffectMaterial::Sidebar)
|
||||
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
|
||||
.expect("Unsupported platform! 'apply_blur' is only supported on Windows");
|
||||
}
|
||||
|
||||
/// 退出程序
|
||||
#[tauri::command]
|
||||
pub fn exit(app: AppHandle) {
|
||||
app.exit(0)
|
||||
}
|
||||
|
||||
pub struct TrayState {
|
||||
// 用来缓存上一次线程的句柄
|
||||
pub id: Mutex<Option<tokio::task::JoinHandle<()>>>
|
||||
}
|
||||
|
||||
/// 设置托盘图标闪烁
|
||||
// TODO 现在只能实现两张图片交换实现闪烁效果 (nyh -> 2024-03-23 17:57:45)
|
||||
#[tauri::command]
|
||||
pub fn tray_blink(
|
||||
app: AppHandle,
|
||||
state: tauri::State<TrayState>,
|
||||
is_run: bool, // 是否运行
|
||||
ms: Option<u64>, // 可选, 时间间隔,单位毫秒
|
||||
icon_path_1: Option<String>, // 可选,图标1
|
||||
icon_path_2: Option<String>, // 可选,图标2
|
||||
) {
|
||||
// 如果存在就取消上一次的线程
|
||||
match state.id.lock().as_deref_mut().map(|x| x.as_mut()) {
|
||||
Ok(Some(v)) => v.abort(),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// 是否停止运行,如果停止,则取消上一次线程后的返回
|
||||
if !is_run {
|
||||
return;
|
||||
}
|
||||
|
||||
// 缓存线程 id 到 tauri state
|
||||
*state.id.lock().unwrap() = Some(tokio::spawn(async move {
|
||||
let path1 = &icon_path_1.unwrap();
|
||||
let path2 = &icon_path_2.unwrap();
|
||||
let mut count = 0;
|
||||
|
||||
loop {
|
||||
// 如果 ms 不存在 则默认 500ms
|
||||
tokio::time::sleep(std::time::Duration::from_millis(ms.unwrap_or_else(|| 500))).await;
|
||||
count += 1;
|
||||
|
||||
// 两张图片交替显示, 所以对2取模运算
|
||||
let path = if count % 2 == 0 {
|
||||
path2
|
||||
} else {
|
||||
path1
|
||||
};
|
||||
|
||||
// tauri api 进行图标设置
|
||||
app
|
||||
.tray_handle()
|
||||
.set_icon(tauri::Icon::File(std::path::PathBuf::from(path)))
|
||||
.unwrap();
|
||||
}
|
||||
}));
|
||||
}
|
@ -1,87 +1,80 @@
|
||||
use tauri::{AppHandle, CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager, PhysicalPosition, SystemTrayEvent, WindowEvent};
|
||||
|
||||
/// 托盘菜单
|
||||
pub fn menu() -> SystemTray {
|
||||
let quit = CustomMenuItem::new("quit".to_string(), "退出");
|
||||
let show = CustomMenuItem::new("show".to_string(), "打开主板");
|
||||
let hide = CustomMenuItem::new("hide".to_string(), "隐藏");
|
||||
let change_ico = CustomMenuItem::new("change_ico".to_string(), "更改图标").disabled();
|
||||
let tray_menu = SystemTrayMenu::new()
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
"Language", // 语言菜单
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("lang_english".to_string(), "English"))
|
||||
.add_item(CustomMenuItem::new("lang_zh_CN".to_string(), "简体中文"))
|
||||
.add_item(CustomMenuItem::new("lang_zh_HK".to_string(), "繁体中文")),
|
||||
|
||||
/// 打开主页
|
||||
fn open_home(app: &AppHandle) {
|
||||
let window = app.get_window("home").unwrap();
|
||||
let hide = window.is_visible().unwrap();
|
||||
let min = window.is_minimized().unwrap();
|
||||
if !hide {
|
||||
window.show().unwrap();
|
||||
}
|
||||
if min {
|
||||
window.unminimize().unwrap();
|
||||
}
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
|
||||
/// 还原图标
|
||||
fn red_icon(app: &AppHandle) {
|
||||
app.tray_handle()
|
||||
.set_icon(tauri::Icon::Raw(
|
||||
include_bytes!("../../icons/icon.ico").to_vec(),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator) // 分割线
|
||||
.add_item(change_ico)
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(hide)
|
||||
.add_item(show)
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(quit);
|
||||
|
||||
SystemTray::new().with_menu(tray_menu)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// 托盘事件
|
||||
pub fn handler(app: &AppHandle, event: SystemTrayEvent) {
|
||||
match event {
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
// 当我点击闪烁的图标的时候就停止闪烁
|
||||
let window = app.get_window("tray").unwrap();
|
||||
window.emit("stop", false).unwrap();
|
||||
open_home(app);
|
||||
red_icon(app);
|
||||
},
|
||||
SystemTrayEvent::RightClick { position: p, size: _, .. } => {
|
||||
// TODO 这里需要根据鼠标位置来确定窗口的位置 (nyh -> 2024-03-20 13:51:01)
|
||||
let tray_window = Arc::new(app.get_window("tray").expect("没有该窗口"));
|
||||
let size = tray_window.outer_size().unwrap();
|
||||
let position_y = p.y - size.height as f64;
|
||||
let position_x = p.x + 10.0;
|
||||
tray_window.set_position(PhysicalPosition::new(position_x,position_y)).unwrap();
|
||||
tray_window.show().unwrap();
|
||||
tray_window.set_always_on_top(true).unwrap();//不置顶这个窗口会被挡住
|
||||
tray_window.set_focus().unwrap();
|
||||
let arc_tray = tray_window.clone();
|
||||
tray_window.on_window_event(move |event|match event{
|
||||
WindowEvent::CloseRequested { .. } => {}
|
||||
WindowEvent::Destroyed => {}
|
||||
WindowEvent::Focused(b) => {
|
||||
if !b {
|
||||
arc_tray.hide().unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
},
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
"change_ico" => { // 更新托盘图标
|
||||
app.tray_handle()
|
||||
.set_icon(tauri::Icon::Raw(
|
||||
include_bytes!("../../icons/128x128.png").to_vec(),
|
||||
include_bytes!("../../../public/status/weather_3x.png").to_vec(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
lang if lang.contains("lang_") => { // 选择语言,匹配 id 前缀包含 `lang_` 的事件
|
||||
Lang::new(
|
||||
app,
|
||||
id, // 点击菜单的 id
|
||||
vec![
|
||||
Lang {
|
||||
name: "English",
|
||||
id: "lang_english",
|
||||
},
|
||||
Lang {
|
||||
name: "繁体中文",
|
||||
id: "lang_zh_HK",
|
||||
},
|
||||
Lang {
|
||||
name: "简体中文",
|
||||
id: "lang_zh_CN",
|
||||
},
|
||||
],
|
||||
);
|
||||
"show" => {
|
||||
open_home(app);
|
||||
}
|
||||
"quit" => {
|
||||
let window = app.get_window("home").unwrap();
|
||||
window.close().unwrap();
|
||||
window.emit("exit", ()).unwrap()
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
struct Lang<'a> {
|
||||
name: &'a str,
|
||||
id: &'a str,
|
||||
}
|
||||
|
||||
impl Lang<'static> {
|
||||
fn new(app: &AppHandle, id: String, langs: Vec<Lang>) {
|
||||
// 获取点击的菜单项的句柄
|
||||
// 注意 `tray_handle` 可以在任何地方调用,只需在 setup 钩子上使用 `app.handle()` 获取 `AppHandle` 实例,将其移动到另一个函数或线程
|
||||
langs.iter().for_each(|lang| {
|
||||
let handle = app.tray_handle().get_item(lang.id);
|
||||
if lang.id.to_string() == id.as_str() {
|
||||
// 设置菜单名称
|
||||
handle.set_title(format!(" {}", lang.name)).unwrap();
|
||||
// 还可以使用 `set_selected`、`set_enabled` 和 `set_native_image`(仅限 macOS)
|
||||
handle.set_selected(true).unwrap();
|
||||
} else {
|
||||
handle.set_title(lang.name).unwrap();
|
||||
handle.set_selected(false).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -5,11 +5,12 @@ use window_shadows::set_shadow;
|
||||
pub fn set_window_attribute<R: Runtime>(app: &tauri::App<R>) {
|
||||
for (_, window) in app.windows() {
|
||||
// 设置窗口阴影和圆角
|
||||
set_shadow(&window, true).expect("Failed to set window shadow");
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
set_shadow(&window, true).unwrap();
|
||||
|
||||
// 设置窗口的磨砂背景
|
||||
#[cfg(target_os = "macos")]
|
||||
window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
|
||||
window_vibrancy::apply_vibrancy(&window, NSVisualEffectMaterial::Sidebar)
|
||||
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
@ -1,38 +1,39 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use tauri::{Manager, Runtime};
|
||||
use window_shadows::set_shadow;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{SystemTray};
|
||||
use crate::common::window::set_window_attribute;
|
||||
use crate::common::plugins::{ exit, reset_set_window, tray_blink, TrayState };
|
||||
|
||||
mod common;
|
||||
|
||||
/// 重新设置窗口属性
|
||||
#[tauri::command]
|
||||
fn reset_set_window<R: Runtime>(app: tauri::AppHandle<R>, label: String) {
|
||||
let window = app.get_window(&label).unwrap();
|
||||
set_shadow(&window, true).expect("Unsupported platform!");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
|
||||
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
|
||||
.expect("Unsupported platform! 'apply_blur' is only supported on Windows");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let context = tauri::generate_context!();
|
||||
let system_tray = SystemTray::new();
|
||||
// 初始化状态
|
||||
let state = TrayState {
|
||||
id: Mutex::new(None)
|
||||
};
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
set_window_attribute(app);
|
||||
Ok(())
|
||||
})
|
||||
.manage(state) // 将状态注入到应用
|
||||
.menu(tauri::Menu::new())// 使用空菜单来替换默认的操作系统菜单
|
||||
.system_tray(common::tray::menu())// 将 `tauri.conf.json` 上配置的图标添加到系统托盘
|
||||
.system_tray(system_tray)// 将 `tauri.conf.json` 上配置的图标添加到系统托盘
|
||||
.on_system_tray_event(common::tray::handler) // 注册系统托盘事件处理程序
|
||||
.invoke_handler(tauri::generate_handler![reset_set_window])
|
||||
.invoke_handler(tauri::generate_handler![reset_set_window, exit, tray_blink]) // 使用定义的插件
|
||||
.on_window_event(|event| match event.event() {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
event.window().hide().unwrap();
|
||||
api.prevent_close();
|
||||
}
|
||||
_ => {}
|
||||
})// 阻止默认关闭行为
|
||||
.run(context)
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
@ -18,6 +18,9 @@
|
||||
},
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"notification": {
|
||||
"all": true
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
@ -35,6 +38,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"resources": ["tray"],
|
||||
"targets": "all",
|
||||
"identifier": "com.tauri.build",
|
||||
"windows": {
|
||||
@ -66,11 +70,23 @@
|
||||
"height": 448,
|
||||
"skipTaskbar": false,
|
||||
"decorations": false,
|
||||
"transparent":true
|
||||
"transparent": true
|
||||
},
|
||||
{
|
||||
"label": "tray",
|
||||
"url": "/tray",
|
||||
"resizable": false,
|
||||
"center": false,
|
||||
"visible": false,
|
||||
"width": 130,
|
||||
"height": 356,
|
||||
"alwaysOnTop": true,
|
||||
"skipTaskbar": true,
|
||||
"decorations": false
|
||||
}
|
||||
],
|
||||
"systemTray": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"iconPath": "icons/icon.ico",
|
||||
"iconAsTemplate": true
|
||||
}
|
||||
}
|
||||
|
BIN
src-tauri/tray/msg-sub.png
Normal file
After Width: | Height: | Size: 731 B |
BIN
src-tauri/tray/msg.png
Normal file
After Width: | Height: | Size: 601 B |
44
src/App.vue
@ -6,11 +6,14 @@
|
||||
</NaiveProvider>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { theme } from '@/stores/theme.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { StoresEnum, ThemeEnum } from '@/enums'
|
||||
import { onlineStatus } from '@/stores/onlineStatus.ts'
|
||||
|
||||
const themeStore = theme()
|
||||
const { THEME } = storeToRefs(themeStore)
|
||||
const settingStore = setting()
|
||||
const OLStatusStore = onlineStatus()
|
||||
const { themes } = storeToRefs(settingStore)
|
||||
|
||||
/* 禁止图片以及输入框的拖拽 */
|
||||
const preventDrag = (e: MouseEvent) => {
|
||||
@ -22,22 +25,31 @@ const preventDrag = (e: MouseEvent) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// /*! 使用msi或者其他安装包安装后才会显示应用的名字和图标 */
|
||||
// sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' })
|
||||
// 判断localStorage中是否有设置主题
|
||||
if (!localStorage.getItem('theme')) {
|
||||
themeStore.initTheme('light')
|
||||
if (!localStorage.getItem(StoresEnum.SETTING)) {
|
||||
settingStore.init(ThemeEnum.LIGHT)
|
||||
}
|
||||
document.documentElement.dataset.theme = THEME.value
|
||||
/* 第一次没有选状态的时候随机选中一个状态 */
|
||||
if (!localStorage.getItem(StoresEnum.ONLINE_STATUS)) {
|
||||
OLStatusStore.init()
|
||||
}
|
||||
document.documentElement.dataset.theme = themes.value.content
|
||||
window.addEventListener('dragstart', preventDrag)
|
||||
// /* 禁用浏览器默认的快捷键 */
|
||||
// window.addEventListener('keydown', (e) => {
|
||||
// // 排除ctrl+c ctrl+v
|
||||
// if (e.ctrlKey && (e.key === 'c' || e.key === 'v')) return
|
||||
// if (e.ctrlKey || e.metaKey || e.altKey) {
|
||||
// e.preventDefault()
|
||||
// }
|
||||
// })
|
||||
// /* 禁止右键菜单 */
|
||||
// window.addEventListener('contextmenu', (e) => e.preventDefault(), false)
|
||||
/* 开发环境不禁止 */
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
/* 禁用浏览器默认的快捷键 */
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// 排除ctrl+c ctrl+v
|
||||
if (e.ctrlKey && (e.key === 'c' || e.key === 'v')) return
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) {
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
/* 禁止右键菜单 */
|
||||
window.addEventListener('contextmenu', (e) => e.preventDefault(), false)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
BIN
src/assets/logo/hula.png
Normal file
After Width: | Height: | Size: 34 KiB |
@ -1,55 +0,0 @@
|
||||
import { appWindow, WebviewWindow } from '@tauri-apps/api/window'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
|
||||
/** 最小化 */
|
||||
export const minimizeWindow = async () => {
|
||||
await appWindow.minimize()
|
||||
}
|
||||
|
||||
/** 最大化 */
|
||||
export const maximizeWindow = async () => {
|
||||
await appWindow.maximize()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭窗口
|
||||
* @param label 窗口标签
|
||||
* @example
|
||||
* 传入窗口标签后开启窗口关闭的监听事件,使用appWindow.emit事件
|
||||
* @see EventEnum.WIN_CLOSE
|
||||
*/
|
||||
export const closeWindow = async (label: string) => {
|
||||
if (label !== void 0) {
|
||||
await emit(EventEnum.WIN_CLOSE, label)
|
||||
}
|
||||
await appWindow.close()
|
||||
}
|
||||
|
||||
/** 取消最大化 */
|
||||
export const unmaximize = async () => {
|
||||
await appWindow.unmaximize()
|
||||
}
|
||||
|
||||
/** 隐藏 */
|
||||
export const hideWindow = async () => {
|
||||
await appWindow.hide()
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据label隐藏窗口
|
||||
* @param label 窗口标签
|
||||
* */
|
||||
export const hideByLabel = async (label: string) => {
|
||||
const win = WebviewWindow.getByLabel(label)
|
||||
await win?.hide()
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动关闭窗口
|
||||
* @param label 窗口标签
|
||||
* */
|
||||
export const autoCloseWindow = async (label: string) => {
|
||||
const win = WebviewWindow.getByLabel(label)
|
||||
await win?.close()
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
left: `${pos.posX}px`,
|
||||
top: `${pos.posY}px`
|
||||
}">
|
||||
<div v-resize="handleSize" class="menu-list">
|
||||
<div v-resize="handleSize" v-if="menu.length > 0" class="menu-list">
|
||||
<div v-for="(item, index) in menu as any[]" :key="index">
|
||||
<!-- 禁止的菜单选项需要禁止点击事件 -->
|
||||
<div class="menu-item-disabled" v-if="item.disabled" @click.prevent="$event.preventDefault()">
|
||||
|
68
src/components/common/InfoPopover.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<!-- 个人信息框 -->
|
||||
<n-flex vertical :size="26" class="size-fit box-border rounded-8px relative">
|
||||
<n-flex vertical :size="20" class="wh-full p-10px box-border z-10">
|
||||
<n-flex vertical :size="20" align="center">
|
||||
<n-avatar
|
||||
:bordered="true"
|
||||
round
|
||||
:color="'#fff'"
|
||||
:size="80"
|
||||
:src="info.avatar"
|
||||
fallback-src="/logo.png"></n-avatar>
|
||||
|
||||
<n-flex :size="5" align="center" style="margin-left: -4px" class="item-hover">
|
||||
<img class="rounded-50% w-18px h-18px" src="/status/weather_3x.png" alt="" />
|
||||
<span>在线状态</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<!-- 地址 -->
|
||||
<n-flex :size="26" class="select-none">
|
||||
<span class="text-[--info-text-color]">所在地</span>
|
||||
<span>中国</span>
|
||||
</n-flex>
|
||||
<!-- 动态 -->
|
||||
<n-flex :size="40" class="select-none">
|
||||
<span class="text-[--info-text-color]">动态</span>
|
||||
<n-image-group>
|
||||
<n-flex :size="6" :wrap="false">
|
||||
<n-image
|
||||
v-for="n in 4"
|
||||
:key="n"
|
||||
preview-disabled
|
||||
class="rounded-8px"
|
||||
width="50"
|
||||
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" />
|
||||
</n-flex>
|
||||
</n-image-group>
|
||||
</n-flex>
|
||||
|
||||
<n-flex justify="center" align="center" :size="40">
|
||||
<n-button secondary> 发信息 </n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<!-- 背景 -->
|
||||
<img
|
||||
class="wh-full rounded-8px box-border p-22px absolute top-0 left-0"
|
||||
style="filter: blur(28px); opacity: 0.7"
|
||||
:src="info.avatar"
|
||||
alt="" />
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
|
||||
const { info } = defineProps<{
|
||||
info: MockItem
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item-hover {
|
||||
@apply select-none hover:bg-[--info-hover] cursor-pointer w-fit rounded-10px p-4px;
|
||||
transition: all 0.4s ease-in-out;
|
||||
}
|
||||
</style>
|
@ -14,48 +14,33 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { theme } from '@/stores/theme.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { dateZhCN, darkTheme, lightTheme, GlobalThemeOverrides, zhCN } from 'naive-ui'
|
||||
import { EventEnum, ThemeEnum } from '@/enums'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { ThemeEnum } from '@/enums'
|
||||
|
||||
const themeStore = theme()
|
||||
const { THEME, PATTERN } = storeToRefs(themeStore)
|
||||
const settingStore = setting()
|
||||
const { themes } = storeToRefs(settingStore)
|
||||
/*监听深色主题颜色变化*/
|
||||
const globalTheme = ref<any>(THEME.value)
|
||||
const globalTheme = ref<any>(themes.value.content)
|
||||
const prefers = matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
/* 跟随系统主题模式切换主题 */
|
||||
const followOS = () => {
|
||||
globalTheme.value = prefers.matches ? darkTheme : lightTheme
|
||||
document.documentElement.dataset.theme = prefers.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT
|
||||
THEME.value = prefers.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT
|
||||
themes.value.content = prefers.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT
|
||||
}
|
||||
|
||||
/* 监听其他标签页的变化 */
|
||||
listen(EventEnum.THEME, (e) => {
|
||||
if (e.payload === ThemeEnum.OS) {
|
||||
// 赋值给ui组件库的主题
|
||||
globalTheme.value = prefers.matches ? darkTheme : lightTheme
|
||||
// 给全局的dataset.theme赋值主题
|
||||
document.documentElement.dataset.theme = prefers.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT
|
||||
// 修改localStorage中的THEME和设置中选择(PATTERN)
|
||||
THEME.value = prefers.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT
|
||||
PATTERN.value = ThemeEnum.OS
|
||||
} else {
|
||||
globalTheme.value = e.payload === ThemeEnum.DARK ? darkTheme : lightTheme
|
||||
// 判断msgInfo.content是否是深色还是浅色
|
||||
document.documentElement.dataset.theme = (e.payload || ThemeEnum.LIGHT) as string
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (PATTERN.value === ThemeEnum.OS) {
|
||||
if (themes.value.pattern === ThemeEnum.OS) {
|
||||
followOS()
|
||||
themes.value.pattern = ThemeEnum.OS
|
||||
prefers.addEventListener('change', followOS)
|
||||
} else {
|
||||
globalTheme.value = THEME.value === ThemeEnum.DARK ? darkTheme : lightTheme
|
||||
// 判断content是否是深色还是浅色
|
||||
document.documentElement.dataset.theme = themes.value.content || ThemeEnum.LIGHT
|
||||
globalTheme.value = themes.value.content === ThemeEnum.DARK ? darkTheme : lightTheme
|
||||
prefers.removeEventListener('change', followOS)
|
||||
}
|
||||
})
|
||||
@ -89,7 +74,8 @@ const themeOverrides: GlobalThemeOverrides = {
|
||||
tabPaddingMediumSegment: '4px'
|
||||
},
|
||||
Popover: {
|
||||
padding: '5px'
|
||||
padding: '5px',
|
||||
borderRadius: '8px'
|
||||
},
|
||||
Dropdown: {
|
||||
borderRadius: '8px'
|
||||
|
@ -1,58 +1,79 @@
|
||||
<template>
|
||||
<!-- 好友详情 -->
|
||||
<div v-if="item.type === RoomTypeEnum.SINGLE" class="flex flex-col items-center mt-60px gap-30px select-none">
|
||||
<n-image width="146px" height="146px" class="rounded-50%" :src="item.avatar" alt="" />
|
||||
<n-flex v-if="item.type === RoomTypeEnum.SINGLE" vertical align="center" :size="30" class="mt-60px select-none">
|
||||
<n-image
|
||||
width="146px"
|
||||
height="146px"
|
||||
style="border: 2px solid #fff"
|
||||
class="rounded-50%"
|
||||
:src="item.avatar"
|
||||
alt="" />
|
||||
|
||||
<span class="text-20px">{{ item.accountName }}</span>
|
||||
<span class="text-20px text-[--text-color]">{{ item.accountName }}</span>
|
||||
|
||||
<span class="text-14px color-#909090">这个人很高冷,暂时没有留下什么</span>
|
||||
<span class="text-14px text-#909090">这个人很高冷,暂时没有留下什么</span>
|
||||
|
||||
<n-flex align="center" justify="space-between" :size="30" class="color-#606060">
|
||||
<n-flex align="center" justify="space-between" :size="30" class="text-#606060">
|
||||
<span>性别:男</span>
|
||||
<span>电话:13213213213</span>
|
||||
</n-flex>
|
||||
<!-- 选项按钮 -->
|
||||
<n-flex align="center" justify="space-between" :size="60">
|
||||
<n-icon-wrapper
|
||||
v-for="(item, index) in footerOptions"
|
||||
:key="index"
|
||||
@click="() => item.click()"
|
||||
class="cursor-pointer"
|
||||
:size="28"
|
||||
:border-radius="10"
|
||||
:color="'rgba(5, 150, 105, 0.8)'">
|
||||
<n-icon :size="20">
|
||||
<svg><use :href="item.url"></use></svg>
|
||||
</n-icon>
|
||||
</n-icon-wrapper>
|
||||
</n-flex>
|
||||
</div>
|
||||
<n-config-provider :theme="lightTheme">
|
||||
<n-flex align="center" justify="space-between" :size="60">
|
||||
<n-icon-wrapper
|
||||
v-for="(item, index) in footerOptions"
|
||||
:key="index"
|
||||
@click="item.click()"
|
||||
class="cursor-pointer"
|
||||
:size="28"
|
||||
:border-radius="10"
|
||||
:color="'rgba(5, 150, 105, 0.8)'">
|
||||
<n-icon :size="20">
|
||||
<svg><use :href="`#${item.url}`"></use></svg>
|
||||
</n-icon>
|
||||
</n-icon-wrapper>
|
||||
</n-flex>
|
||||
</n-config-provider>
|
||||
</n-flex>
|
||||
|
||||
<!-- 群聊详情 -->
|
||||
<div v-else class="flex flex-col flex-1 mt-60px gap-30px select-none p-[0_40px] box-border">
|
||||
<!-- 群聊头像以及简介 -->
|
||||
<n-flex align="center" justify="space-between">
|
||||
<n-flex align="center">
|
||||
<n-image width="120px" height="120px" class="rounded-50%" :src="item.avatar" alt="" />
|
||||
<n-image
|
||||
width="120px"
|
||||
height="120px"
|
||||
style="border: 2px solid #fff"
|
||||
class="rounded-50%"
|
||||
:src="item.avatar"
|
||||
alt="" />
|
||||
|
||||
<n-flex vertical :size="16" justify="space-between" class="text-14px color-#909090">
|
||||
<span class="text-16px color-[--text-color]">{{ item.accountName }}</span>
|
||||
<span class="text-16px text-[--text-color]">{{ item.accountName }}</span>
|
||||
<span>群号:1235873897182</span>
|
||||
<span>创建时间:2021-01-01</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<n-icon-wrapper class="cursor-pointer" :size="40" :border-radius="10" :color="'rgba(5, 150, 105, 0.8)'">
|
||||
<n-icon :size="22">
|
||||
<svg><use href="#message"></use></svg>
|
||||
</n-icon>
|
||||
</n-icon-wrapper>
|
||||
<n-config-provider :theme="lightTheme">
|
||||
<n-icon-wrapper
|
||||
@click="footerOptions[0].click()"
|
||||
class="cursor-pointer"
|
||||
:size="40"
|
||||
:border-radius="10"
|
||||
:color="'rgba(5, 150, 105, 0.8)'">
|
||||
<n-icon :size="22">
|
||||
<svg><use href="#message"></use></svg>
|
||||
</n-icon>
|
||||
</n-icon-wrapper>
|
||||
</n-config-provider>
|
||||
</n-flex>
|
||||
|
||||
<n-space vertical :size="20">
|
||||
<span>群成员:({{ options.length }}人)</span>
|
||||
<n-flex vertical :size="20">
|
||||
<span class="text-[--text-color]">群成员:({{ options.length }}人)</span>
|
||||
|
||||
<n-avatar-group :options="options" :size="40" :max="4">
|
||||
<n-avatar-group :options="options" :size="40" :max="4" expand-on-hover>
|
||||
<template #avatar="{ option: { name, src } }">
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
@ -67,13 +88,18 @@
|
||||
</n-dropdown>
|
||||
</template>
|
||||
</n-avatar-group>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { RoomTypeEnum } from '@/enums'
|
||||
import { MittEnum, RoomTypeEnum } from '@/enums'
|
||||
import { lightTheme } from 'naive-ui'
|
||||
import router from '@/router'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { useMessage } from '@/hooks/useMessage.ts'
|
||||
|
||||
const { handleMsgClick } = useMessage()
|
||||
const props = defineProps<{
|
||||
content: any[]
|
||||
}>()
|
||||
@ -82,25 +108,24 @@ const item = computed<MockItem>(() => {
|
||||
return content.value[0]
|
||||
})
|
||||
|
||||
type FooterOption = {
|
||||
url: string
|
||||
click: (...args: any[]) => void
|
||||
}
|
||||
const footerOptions = ref<FooterOption[]>([
|
||||
const footerOptions = ref<OPT.Details[]>([
|
||||
{
|
||||
url: '#message',
|
||||
url: 'message',
|
||||
click: () => {
|
||||
// TODO 需要增加独立窗口功能 (nyh -> 2024-03-25 16:01:23)
|
||||
router.push('/message')
|
||||
handleMsgClick(item.value)
|
||||
Mitt.emit(MittEnum.TO_SEND_MSG, { url: 'message' })
|
||||
}
|
||||
},
|
||||
{
|
||||
url: 'phone-telephone',
|
||||
click: () => {
|
||||
console.log(123)
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '#phone-telephone',
|
||||
click: () => {
|
||||
console.log(123)
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '#video-one',
|
||||
url: 'video-one',
|
||||
click: () => {
|
||||
console.log(123)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<!-- 输入框 -->
|
||||
<ContextMenu class="relative w-full h-100px" @select="$event.click()" :menu="menuList">
|
||||
<n-scrollbar style="max-height: 100px">
|
||||
<!-- 输入框 -->
|
||||
<div
|
||||
class="message-input"
|
||||
ref="messageInputDom"
|
||||
@ -14,6 +14,10 @@
|
||||
</n-scrollbar>
|
||||
</ContextMenu>
|
||||
|
||||
<!-- @提及框 -->
|
||||
<div v-if="ait" class="absolute w-180px h-160px bg-#fff top--130px left-20px rounded-8px">123</div>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<n-config-provider :theme="lightTheme">
|
||||
<n-button-group size="small" class="pr-20px">
|
||||
<n-button
|
||||
@ -33,11 +37,11 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { lightTheme } from 'naive-ui'
|
||||
import { MsgEnum } from '@/enums'
|
||||
import { MittEnum, MsgEnum } from '@/enums'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { createFileOrVideoDom } from '@/utils/CreateDom.ts'
|
||||
import { RegExp } from '@/utils/RegExp.ts'
|
||||
|
||||
const ait = ref(false)
|
||||
const menuList = ref([
|
||||
{ label: '剪切', icon: 'screenshot', disabled: true },
|
||||
{ label: '复制', icon: 'copy', disabled: true },
|
||||
@ -70,8 +74,6 @@ const menuList = ref([
|
||||
const msgInput = ref('')
|
||||
// 输入框dom元素
|
||||
const messageInputDom = ref()
|
||||
// 自定义输入框子节点元素列表
|
||||
const childNodes = ref<any>([])
|
||||
|
||||
/**
|
||||
* 将指定节点插入到光标位置
|
||||
@ -168,16 +170,11 @@ const getMessageContentType = () => {
|
||||
let hasText = false
|
||||
let hasImage = false
|
||||
let hasVideo = false
|
||||
let hasHyperlink = false
|
||||
|
||||
const elements = messageInputDom.value.childNodes
|
||||
for (let element of elements) {
|
||||
if (element.nodeType === Node.TEXT_NODE && element.nodeValue.trim() !== '') {
|
||||
if (RegExp.isHyperlink(element.nodeValue)) {
|
||||
hasHyperlink = true
|
||||
} else {
|
||||
hasText = true
|
||||
}
|
||||
hasText = true
|
||||
} else if (element.tagName === 'IMG') {
|
||||
hasImage = true
|
||||
} else if (element.tagName === 'VI DEO' || (element.tagName === 'A' && element.href.match(/\.(mp4|webm)$/i))) {
|
||||
@ -191,8 +188,6 @@ const getMessageContentType = () => {
|
||||
return MsgEnum.MIXED
|
||||
} else if (hasImage) {
|
||||
return MsgEnum.IMAGE
|
||||
} else if (hasHyperlink) {
|
||||
return MsgEnum.HYPERLINK
|
||||
} else {
|
||||
return MsgEnum.TEXT
|
||||
}
|
||||
@ -204,7 +199,17 @@ const send = () => {
|
||||
const contentType = getMessageContentType()
|
||||
const msg = {
|
||||
type: contentType,
|
||||
content: msgInput.value
|
||||
content: msgInput.value,
|
||||
hyperlinks: [] as any
|
||||
}
|
||||
const hyperlinkRegex = /(\bhttps?:\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi
|
||||
const foundHyperlinks = msg.content.match(hyperlinkRegex)
|
||||
|
||||
if (foundHyperlinks && foundHyperlinks.length > 0) {
|
||||
msg.hyperlinks = foundHyperlinks
|
||||
msg.content = msg.content.replace(hyperlinkRegex, (match) => {
|
||||
return `<a class="color-inherit" href="${match}" target="_blank" rel="noopener noreferrer">${match}</a>`
|
||||
})
|
||||
}
|
||||
// 判断文本信息是否超过限制
|
||||
if (msg.type === MsgEnum.TEXT && msg.content.length > 2000) {
|
||||
@ -216,15 +221,15 @@ const send = () => {
|
||||
window.$message.error('暂不支持混合类型消息发送')
|
||||
return
|
||||
}
|
||||
Mitt.emit('handleSendMessage', msg)
|
||||
Mitt.emit(MittEnum.SEND_MESSAGE, msg)
|
||||
msgInput.value = ''
|
||||
messageInputDom.value.innerHTML = ''
|
||||
}
|
||||
|
||||
/* 当输入框手动输入值的时候触发input事件 */
|
||||
const handleInput = (e: Event) => {
|
||||
childNodes.value = (e.target as HTMLInputElement).childNodes
|
||||
msgInput.value = (e.target as HTMLInputElement).innerHTML
|
||||
ait.value = msgInput.value.endsWith('@')
|
||||
}
|
||||
|
||||
/* input的keydown事件 */
|
||||
|
@ -2,7 +2,7 @@
|
||||
<!-- 顶部操作栏和显示用户名 -->
|
||||
<main
|
||||
style="box-shadow: 0 4px 4px var(--box-shadow-color)"
|
||||
class="relative z-20 flex-y-center justify-between p-[8px_20px_12px] select-none">
|
||||
class="relative z-30 flex-y-center justify-between p-[8px_20px_12px] select-none">
|
||||
<div class="color-[--text-color]">{{ activeItem.accountName }}</div>
|
||||
<!-- 顶部右边选项栏 -->
|
||||
<nav class="options flex-y-center gap-20px color-[--icon-color]">
|
||||
@ -162,69 +162,5 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.options {
|
||||
.options-box {
|
||||
cursor: pointer;
|
||||
svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
outline: none;
|
||||
}
|
||||
&:hover svg {
|
||||
color: #059669;
|
||||
animation: twinkle 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
color: var(--text-color);
|
||||
margin-top: 20px;
|
||||
background: var(--bg-setting-item);
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@apply flex flex-col absolute top-44px right-0 bg-[--bg-chat-drawer] p-22px box-border w-320px h-100vh shadow-[0_14px_14px_rgba(0,0,0,0.35)];
|
||||
}
|
||||
|
||||
/*! 使用vue内置transition做过渡效果 */
|
||||
// 进入动画的生效状态
|
||||
.sidebar-enter-active {
|
||||
animation: slideIn 0.3s ease-in-out;
|
||||
}
|
||||
// 离开动画的生效状态
|
||||
.sidebar-leave-active {
|
||||
animation: slideOut 0.3s ease-in-out;
|
||||
}
|
||||
/*!end */
|
||||
@import '@/styles/scss/chat-header';
|
||||
</style>
|
@ -1,100 +1,111 @@
|
||||
<template>
|
||||
<!-- 中间聊天内容(使用虚拟列表) -->
|
||||
<n-virtual-list
|
||||
<n-scrollbar
|
||||
id="image-chat-main"
|
||||
ref="virtualListInst"
|
||||
style="max-height: calc(100vh - 260px); position: relative"
|
||||
item-resizable
|
||||
padding-bottom="10px"
|
||||
:item-size="42"
|
||||
:items="items"
|
||||
@scroll="handleScroll($event)">
|
||||
<template #default="{ item }">
|
||||
<main
|
||||
:key="item.key"
|
||||
class="flex-y-center min-h-58px"
|
||||
:class="activeItem.type === RoomTypeEnum.GROUP ? 'p-[18px_20px]' : 'chat-single p-[2px_20px]'">
|
||||
<!-- 好友或者群聊的信息 -->
|
||||
<article class="flex flex-col w-full gap-18px" :class="item.accountId === userId ? 'items-end' : ''">
|
||||
<div
|
||||
class="flex items-start"
|
||||
:class="item.accountId === userId ? 'flex-row-reverse' : ''"
|
||||
style="max-width: calc(100% - 54px)">
|
||||
<!-- 头像 -->
|
||||
<img
|
||||
:class="item.accountId === userId ? '' : 'mr-10px'"
|
||||
class="w-34px rounded-50% select-none"
|
||||
:src="item.accountId === userId ? item.avatar : activeItem.avatar"
|
||||
alt="" />
|
||||
style="max-height: calc(100vh - 260px)"
|
||||
class="relative h-100vh">
|
||||
<n-virtual-list item-resizable padding-bottom="10px" :item-size="42" :items="items" @scroll="handleScroll($event)">
|
||||
<template #default="{ item }">
|
||||
<main
|
||||
:key="item.key"
|
||||
class="flex-y-center min-h-58px"
|
||||
:class="activeItem.type === RoomTypeEnum.GROUP ? 'p-[18px_20px]' : 'chat-single p-[2px_20px]'">
|
||||
<!-- 好友或者群聊的信息 -->
|
||||
<article class="flex flex-col w-full gap-18px" :class="item.accountId === userId ? 'items-end' : ''">
|
||||
<div
|
||||
class="flex flex-col gap-8px color-[--text-color]"
|
||||
:class="item.accountId === userId ? 'items-end mr-10px' : ''">
|
||||
<span class="text-12px select-none color-#909090" v-if="activeItem.type === RoomTypeEnum.GROUP">
|
||||
{{ item.accountId === userId ? item.value : activeItem.accountName }}
|
||||
</span>
|
||||
<!-- 右键菜单及其气泡样式 -->
|
||||
<ContextMenu
|
||||
:data-key="item.accountId === userId ? `U${item.key}` : `Q${item.key}`"
|
||||
@select="$event.click(item.key)"
|
||||
:menu="menuList"
|
||||
:special-menu="specialMenuList"
|
||||
@click="handleMsgClick(item)">
|
||||
<!-- <!– 渲染消息内容体 –>-->
|
||||
<!-- <RenderMessage :message="message" />-->
|
||||
<!-- 消息为文本类型 -->
|
||||
<div
|
||||
v-if="item.type === MsgEnum.TEXT"
|
||||
:class="[
|
||||
{ active: activeBubble === item.key },
|
||||
activeItem.type === RoomTypeEnum.GROUP ? '' : 'm-[10px_0]',
|
||||
item.accountId === userId ? 'bubble-oneself' : 'bubble'
|
||||
]"
|
||||
v-html="item.content"></div>
|
||||
class="flex items-start"
|
||||
:class="item.accountId === userId ? 'flex-row-reverse' : ''"
|
||||
style="max-width: calc(100% - 54px)">
|
||||
<!-- 头像 -->
|
||||
<n-popover
|
||||
@update:show="handlePopoverUpdate(item.key)"
|
||||
trigger="click"
|
||||
placement="right-start"
|
||||
:show-arrow="false"
|
||||
style="padding: 0; background: var(--bg-info); backdrop-filter: blur(10px)">
|
||||
<template #trigger>
|
||||
<ContextMenu
|
||||
@select="$event.click(item)"
|
||||
:menu="activeItem.type === RoomTypeEnum.GROUP ? optionsList : []"
|
||||
:special-menu="report">
|
||||
<n-avatar
|
||||
lazy
|
||||
round
|
||||
:color="'#fff'"
|
||||
:size="34"
|
||||
@click="selectKey = item.key"
|
||||
:src="item.accountId === userId ? item.avatar : activeItem.avatar"
|
||||
:class="item.accountId === userId ? '' : 'mr-10px'"
|
||||
fallback-src="/logo.png"
|
||||
:render-placeholder="() => null"
|
||||
:intersection-observer-options="{
|
||||
root: '#image-chat-main'
|
||||
}"></n-avatar>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<!-- 用户个人信息框 -->
|
||||
<InfoPopover :info="activeItemRef" />
|
||||
</n-popover>
|
||||
<div
|
||||
class="flex flex-col gap-8px color-[--text-color]"
|
||||
:class="item.accountId === userId ? 'items-end mr-10px' : ''">
|
||||
<ContextMenu
|
||||
@select="$event.click(item)"
|
||||
:menu="activeItem.type === RoomTypeEnum.GROUP ? optionsList : []"
|
||||
:special-menu="report">
|
||||
<span class="text-12px select-none color-#909090" v-if="activeItem.type === RoomTypeEnum.GROUP">
|
||||
{{ item.accountId === userId ? item.value : activeItem.accountName }}
|
||||
</span>
|
||||
</ContextMenu>
|
||||
<!-- 气泡样式 -->
|
||||
<ContextMenu
|
||||
:data-key="item.accountId === userId ? `U${item.key}` : `Q${item.key}`"
|
||||
@select="$event.click(item)"
|
||||
:menu="menuList"
|
||||
:special-menu="specialMenuList"
|
||||
@click="handleMsgClick(item)">
|
||||
<!-- <!– 渲染消息内容体 –>-->
|
||||
<!-- <RenderMessage :message="message" />-->
|
||||
<!-- 消息为文本类型 -->
|
||||
<div
|
||||
v-if="item.type === MsgEnum.TEXT"
|
||||
style="white-space: pre-wrap"
|
||||
:class="[
|
||||
{ active: activeBubble === item.key },
|
||||
item.accountId === userId ? 'bubble-oneself' : 'bubble'
|
||||
]">
|
||||
<span v-html="item.content"></span>
|
||||
</div>
|
||||
|
||||
<!-- 消息为超链接时 -->
|
||||
<div
|
||||
v-if="item.type === MsgEnum.HYPERLINK"
|
||||
:class="[
|
||||
{ active: activeBubble === item.key },
|
||||
activeItem.type === RoomTypeEnum.GROUP ? '' : 'm-[10px_0]',
|
||||
item.accountId === userId ? 'bubble-oneself' : 'bubble'
|
||||
]">
|
||||
<!-- rel="noopener noreferrer" 让网站安全跳转,防止钓鱼网站 -->
|
||||
<a class="color-inherit" target="_blank" rel="noopener noreferrer" :href="item.content">
|
||||
{{ item.content }}
|
||||
</a>
|
||||
</div>
|
||||
<!-- 消息为为图片类型(不固定宽度和高度), 多张图片时渲染 -->
|
||||
<n-image-group v-if="Array.isArray(item.content) && item.type === MsgEnum.IMAGE">
|
||||
<n-flex class="photo-wall" vertical>
|
||||
<n-image
|
||||
v-for="(src, index) in item.content"
|
||||
:key="index"
|
||||
:img-props="{ style: { maxWidth: '325px', maxHeight: '165px' } }"
|
||||
show-toolbar-tooltip
|
||||
style="border-radius: 8px"
|
||||
:src="src"></n-image>
|
||||
</n-flex>
|
||||
</n-image-group>
|
||||
|
||||
<!-- 消息为为图片类型(不固定宽度和高度), 多张图片时渲染 -->
|
||||
<n-image-group v-if="Array.isArray(item.content) && item.type === MsgEnum.IMAGE">
|
||||
<n-space
|
||||
class="photo-wall"
|
||||
vertical
|
||||
:class="activeItem.type === RoomTypeEnum.GROUP ? '' : 'm-[10px_0]'">
|
||||
<n-image
|
||||
v-for="(src, index) in item.content"
|
||||
:key="index"
|
||||
:img-props="{ style: { maxWidth: '325px', maxHeight: '165px' } }"
|
||||
show-toolbar-tooltip
|
||||
style="border-radius: 8px"
|
||||
:src="src"></n-image>
|
||||
</n-space>
|
||||
</n-image-group>
|
||||
|
||||
<!-- 单张图片时渲染 -->
|
||||
<n-image
|
||||
v-else-if="typeof item.content === 'string' && item.type === MsgEnum.IMAGE"
|
||||
:img-props="{ style: { maxWidth: '325px', maxHeight: '165px' } }"
|
||||
show-toolbar-tooltip
|
||||
style="border-radius: 8px"
|
||||
:class="activeItem.type === RoomTypeEnum.GROUP ? '' : 'm-[10px_0]'"
|
||||
:src="item.content"></n-image>
|
||||
</ContextMenu>
|
||||
<!-- 单张图片时渲染 -->
|
||||
<n-image
|
||||
v-else-if="typeof item.content === 'string' && item.type === MsgEnum.IMAGE"
|
||||
:img-props="{ style: { maxWidth: '325px', maxHeight: '165px' } }"
|
||||
show-toolbar-tooltip
|
||||
style="border-radius: 8px"
|
||||
:src="item.content"></n-image>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</article>
|
||||
</main>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</n-scrollbar>
|
||||
|
||||
<!-- 弹出框 -->
|
||||
<n-modal v-model:show="modalShow" class="w-350px border-rd-8px">
|
||||
@ -114,7 +125,7 @@
|
||||
</n-modal>
|
||||
|
||||
<!-- 悬浮按钮提示(头部悬浮) // TODO 要结合已读未读功能来判断之前的信息有多少没有读,当现在的距离没有到最底部并且又有新消息来未读的时候显示下标的更多信息 (nyh -> 2024-03-07 01:27:22)-->
|
||||
<header class="float-header">
|
||||
<header class="float-header" :class="activeItem.type === RoomTypeEnum.GROUP ? 'right-220px' : 'right-50px'">
|
||||
<div class="float-box">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-icon :color="'rgba(5,150,105,0.5)'">
|
||||
@ -126,11 +137,11 @@
|
||||
</header>
|
||||
|
||||
<!-- 悬浮按钮提示(底部悬浮) -->
|
||||
<footer class="float-footer" v-if="floatFooter && newMsgNum > 0">
|
||||
<div
|
||||
class="float-box"
|
||||
:class="{ max: newMsgNum > 99 }"
|
||||
@click="() => virtualListInst?.scrollTo({ position: 'bottom' })">
|
||||
<footer
|
||||
class="float-footer"
|
||||
v-if="floatFooter && newMsgNum > 0"
|
||||
:class="activeItem.type === RoomTypeEnum.GROUP ? 'right-220px' : 'right-50px'">
|
||||
<div class="float-box" :class="{ max: newMsgNum > 99 }" @click="scrollBottom">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-icon :color="newMsgNum > 99 ? '#ce304f' : 'rgba(5,150,105,0.5)'">
|
||||
<svg><use href="#double-down"></use></svg>
|
||||
@ -143,11 +154,16 @@
|
||||
</footer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { MsgEnum, RoomTypeEnum } from '@/enums'
|
||||
import { Menu, MockItem } from '@/services/types.ts'
|
||||
import { MittEnum, MsgEnum, RoomTypeEnum } from '@/enums'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { VirtualListInst } from 'naive-ui'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { optionsList, report } from './config.ts'
|
||||
import { usePopover } from '@/hooks/usePopover.ts'
|
||||
|
||||
/* 当前点击的用户的key */
|
||||
const selectKey = ref()
|
||||
const activeBubble = ref(-1)
|
||||
const userId = ref(10086)
|
||||
const copyright = ref('-HuLa©-版权所有')
|
||||
@ -174,11 +190,14 @@ const newMsgNum = ref(0)
|
||||
const itemComputed = computed(() => {
|
||||
return items.value.filter((item) => item.accountId !== userId.value).length
|
||||
})
|
||||
/* 虚拟列表的距离 */
|
||||
/* 虚拟列表 */
|
||||
const virtualListInst = ref<VirtualListInst>()
|
||||
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-main')
|
||||
|
||||
const { activeItem } = defineProps<{
|
||||
activeItem: MockItem
|
||||
}>()
|
||||
const activeItemRef = ref({ ...activeItem })
|
||||
// // 创建一个符合 TextBody 类型的对象
|
||||
// const textBody = {
|
||||
// content: '123',
|
||||
@ -202,15 +221,14 @@ const { activeItem } = defineProps<{
|
||||
// }
|
||||
// })
|
||||
// const message = computed(() => msg.value)
|
||||
|
||||
/* 右键菜单列表 */
|
||||
const menuList = ref<Menu>([
|
||||
/* 右键消息菜单列表 */
|
||||
const menuList = ref<OPT.RightMenu[]>([
|
||||
{
|
||||
label: '复制',
|
||||
icon: 'copy',
|
||||
click: (index: number) => {
|
||||
click: (item: any) => {
|
||||
// 复制内容到剪贴板
|
||||
copyrightComputed.value.copy(index)
|
||||
copyrightComputed.value.copy(item.key)
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -222,7 +240,7 @@ const menuList = ref<Menu>([
|
||||
{ label: '回复', icon: 'reply' }
|
||||
])
|
||||
/* 右键菜单下划线后的列表 */
|
||||
const specialMenuList = ref<Menu>([
|
||||
const specialMenuList = ref<OPT.RightMenu[]>([
|
||||
{
|
||||
label: '删除',
|
||||
icon: 'delete',
|
||||
@ -248,6 +266,7 @@ const items = ref(
|
||||
|
||||
watchEffect(() => {
|
||||
newMsgNum.value = itemComputed.value - historyIndex.value
|
||||
activeItemRef.value = { ...activeItem }
|
||||
})
|
||||
|
||||
/* 处理滚动事件(用于页脚显示功能) */
|
||||
@ -349,6 +368,13 @@ const addToDomUpdateQueue = (index: number, id: number) => {
|
||||
})
|
||||
}
|
||||
|
||||
/* 点击后滚动到底部 */
|
||||
const scrollBottom = () => {
|
||||
nextTick(() => {
|
||||
virtualListInst.value?.scrollTo({ position: 'bottom' })
|
||||
})
|
||||
}
|
||||
|
||||
const closeMenu = (event: any) => {
|
||||
if (!event.target.matches('.bubble', 'bubble-oneself')) {
|
||||
activeBubble.value = -1
|
||||
@ -356,9 +382,21 @@ const closeMenu = (event: any) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Mitt.on('handleSendMessage', (event) => {
|
||||
/*! 启动图标闪烁 需要设置"resources": ["sec-tauri/图标放置的文件夹"]*/
|
||||
invoke('tray_blink', {
|
||||
isRun: true,
|
||||
ms: 500,
|
||||
iconPath1: 'tray/msg.png',
|
||||
iconPath2: 'tray/msg-sub.png'
|
||||
}).catch((error) => {
|
||||
console.error('设置图标失败:', error)
|
||||
})
|
||||
Mitt.on(MittEnum.SEND_MESSAGE, (event) => {
|
||||
handleSendMessage(event)
|
||||
})
|
||||
Mitt.on(MittEnum.MSG_BOX_SHOW, (event: any) => {
|
||||
activeItemRef.value = event.item
|
||||
})
|
||||
window.addEventListener('click', closeMenu, true)
|
||||
// let index = items.value.length > 0 ? items.value[items.value.length - 1].key : 0
|
||||
//
|
||||
@ -383,58 +421,5 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 气泡样式 */
|
||||
@mixin bubble {
|
||||
@apply w-fit max-w-55vw min-h-1em p-[8px_12px] text-15px line-height-22px bg-[--bg-bubble] rounded-[2px_18px_18px];
|
||||
word-break: break-all; /* 强制连续文本换行 */
|
||||
&.active {
|
||||
background-color: var(--bg-bubble-active);
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
@mixin float($position: top) {
|
||||
@if $position == bottom {
|
||||
@apply fixed z-10 bottom-210px right-50px w-fit select-none text-#059669 cursor-pointer;
|
||||
} @else {
|
||||
@apply fixed z-10 top-80px right-50px w-fit select-none text-#059669 cursor-pointer;
|
||||
}
|
||||
}
|
||||
.chat-single:first-child {
|
||||
padding-top: 16px;
|
||||
}
|
||||
.bubble {
|
||||
@include bubble;
|
||||
}
|
||||
.bubble-oneself {
|
||||
@include bubble;
|
||||
@apply rounded-[18px_2px_18px_18px] color-#fff;
|
||||
background-color: rgba(5, 150, 105, 0.8);
|
||||
}
|
||||
/*! 气泡动画 */
|
||||
.bubble-animation {
|
||||
animation: bubble-twinkle 0.4s ease-out forwards;
|
||||
}
|
||||
.photo-wall {
|
||||
@extend .bubble-oneself;
|
||||
@apply flex flex-col items-start gap-6px max-w-380px;
|
||||
}
|
||||
.float-header {
|
||||
@include float;
|
||||
}
|
||||
.float-footer {
|
||||
@include float(bottom);
|
||||
}
|
||||
.float-box {
|
||||
background-color: #fff;
|
||||
border-radius: 50px;
|
||||
padding: 6px 8px;
|
||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.16);
|
||||
transition: background-color 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
&:hover {
|
||||
background-color: rgb(215, 229, 219);
|
||||
}
|
||||
}
|
||||
.max:hover {
|
||||
background-color: #f5dce1;
|
||||
}
|
||||
@import '@/styles/scss/chat-main';
|
||||
</style>
|
93
src/components/rightBox/chatBox/ChatSidebar.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<!--! 这里最好不要使用n-flex,滚动高度会有问题 -->
|
||||
<main v-if="activeItem.type === RoomTypeEnum.GROUP" style="border-left: 1px solid var(--line-color)" class="item-box">
|
||||
<n-flex v-if="!isSearch" align="center" justify="space-between" class="pr-8px pl-8px h-42px">
|
||||
<span class="text-14px">群聊成员</span>
|
||||
<svg @click="handleSearch" class="size-14px"><use href="#search"></use></svg>
|
||||
</n-flex>
|
||||
<!-- 搜索框 -->
|
||||
<n-flex v-else align="center" class="pr-8px h-42px">
|
||||
<n-input
|
||||
@blur="isSearch = false"
|
||||
ref="inputInstRef"
|
||||
v-model:value="searchRef"
|
||||
clearable
|
||||
placeholder="搜索"
|
||||
type="text"
|
||||
size="tiny"
|
||||
class="h-26px w-95% lh-26px rounded-6px">
|
||||
<template #prefix>
|
||||
<svg class="w-12px h-12px"><use href="#search"></use></svg>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-flex>
|
||||
|
||||
<!--! 使用滚动条组件来包裹虚拟列表不然会有其他问题 -->
|
||||
<!-- // TODO popover显示的时候去改变窗口的大小、当点击了半个选项的时候也会出现原生滚动条 (nyh -> 2024-03-25 05:04:37) -->
|
||||
<!-- // TODO 如果popover显示就先暂时不让滚动,因为在n-scrollbar和n-virtual-list中使用当我点击最后一个选项时候n-popover位置不够导致出现原生滚动条 (nyh -> 2024-03-24 22:46:38) -->
|
||||
<!-- // TODO 如果直接使用n-virtual-list的滚动配上n-popover似乎也没有这个bug,但是当点击倒数第二个的时候还是会出现滚动条 (nyh -> 2024-03-25 00:30:53) -->
|
||||
<n-scrollbar id="image-chat-sidebar" ref="virtualListInst" style="max-height: calc(100vh - 130px)">
|
||||
<n-virtual-list item-resizable :item-size="42" :items="MockList">
|
||||
<template #default="{ item }">
|
||||
<n-popover
|
||||
@update:show="handlePopoverUpdate(item.key)"
|
||||
trigger="click"
|
||||
placement="left-start"
|
||||
:show-arrow="false"
|
||||
style="padding: 0; background: var(--bg-info); backdrop-filter: blur(10px)">
|
||||
<template #trigger>
|
||||
<ContextMenu @select="$event.click(item)" :menu="optionsList" :special-menu="report">
|
||||
<n-flex @click="selectKey = item.key" :key="item.key" :size="10" align="center" class="item">
|
||||
<n-avatar
|
||||
lazy
|
||||
round
|
||||
:color="'#fff'"
|
||||
:size="24"
|
||||
:src="item.avatar"
|
||||
fallback-src="/logo.png"
|
||||
:render-placeholder="() => null"
|
||||
:intersection-observer-options="{
|
||||
root: '#image-chat-sidebar'
|
||||
}"></n-avatar>
|
||||
<span class="text-12px">{{ item.accountName }}</span>
|
||||
</n-flex>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
<!-- 用户个人信息框 -->
|
||||
<InfoPopover :info="item" />
|
||||
</n-popover>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</n-scrollbar>
|
||||
</main>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { RoomTypeEnum } from '@/enums'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { MockList } from '@/mock'
|
||||
import { InputInst } from 'naive-ui'
|
||||
import { optionsList, report } from './config.ts'
|
||||
import { usePopover } from '@/hooks/usePopover.ts'
|
||||
|
||||
/* 当前点击的用户的key */
|
||||
const selectKey = ref()
|
||||
const isSearch = ref(false)
|
||||
const searchRef = ref('')
|
||||
const inputInstRef = ref<InputInst | null>(null)
|
||||
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-sidebar')
|
||||
|
||||
const { activeItem } = defineProps<{
|
||||
activeItem: MockItem
|
||||
}>()
|
||||
|
||||
const handleSearch = () => {
|
||||
isSearch.value = !isSearch.value
|
||||
nextTick(() => {
|
||||
inputInstRef.value?.select()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/scss/chat-sidebar';
|
||||
</style>
|
36
src/components/rightBox/chatBox/config.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// TODO config文件做简单的操作配置,如果需求复杂就封装成hooks (nyh -> 2024-03-23 03:35:05)
|
||||
/* 右键用户信息菜单(单聊的时候显示) */
|
||||
const optionsList = ref([
|
||||
{
|
||||
label: '发送信息',
|
||||
icon: 'message-action',
|
||||
click: (item: any) => {
|
||||
console.log(item)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'TA',
|
||||
icon: 'aite',
|
||||
click: () => {}
|
||||
},
|
||||
{
|
||||
label: '查看资料',
|
||||
icon: 'notes',
|
||||
click: () => {}
|
||||
},
|
||||
{
|
||||
label: '添加好友',
|
||||
icon: 'people-plus',
|
||||
click: () => {}
|
||||
}
|
||||
])
|
||||
/* 举报选项 */
|
||||
const report = ref([
|
||||
{
|
||||
label: '举报',
|
||||
icon: 'caution',
|
||||
click: () => {}
|
||||
}
|
||||
])
|
||||
|
||||
export { optionsList, report }
|
@ -1,20 +1,40 @@
|
||||
<template>
|
||||
<!-- 头部 -->
|
||||
<ChatHeader :active-item="activeItem" />
|
||||
<!-- 中间聊天框内容 -->
|
||||
<ChatMain :active-item="activeItem" />
|
||||
<!-- 输入框和操作列表 -->
|
||||
<ChatFooter />
|
||||
<ChatHeader :active-item="activeItemRef" />
|
||||
<n-flex :size="0" class="h-full">
|
||||
<n-flex vertical :size="0" class="flex-1">
|
||||
<!-- 中间聊天框内容 -->
|
||||
<ChatMain :active-item="activeItemRef" />
|
||||
<!-- 输入框和操作列表 -->
|
||||
<ChatFooter class="flex-1" />
|
||||
</n-flex>
|
||||
<ChatSidebar :active-item="activeItemRef" />
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import ChatFooter from './chatFooter.vue'
|
||||
import ChatHeader from './chatHeader.vue'
|
||||
import ChatMain from './chatMain.vue'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
|
||||
const { activeItem } = defineProps<{
|
||||
activeItem: MockItem
|
||||
activeItem?: MockItem
|
||||
}>()
|
||||
</script>
|
||||
const activeItemRef = ref({ ...activeItem! })
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
watchEffect(() => {
|
||||
activeItemRef.value = { ...activeItem! }
|
||||
})
|
||||
|
||||
listen(appWindow.label, (e) => {
|
||||
activeItemRef.value = e.payload as any
|
||||
})
|
||||
</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>
|
||||
|
@ -4,8 +4,8 @@
|
||||
<script setup lang="ts">
|
||||
import { MsgEnum } from '@/enums'
|
||||
import type { MsgType } from '@/services/types'
|
||||
import Text from './text.vue'
|
||||
import Image from './image.vue'
|
||||
import Text from './Text.vue'
|
||||
import Image from './Image.vue'
|
||||
|
||||
const componentMap = {
|
||||
[MsgEnum.TEXT]: Text,
|
||||
|
@ -23,7 +23,7 @@
|
||||
<svg class="w-16px h-16px color-[--action-bar-icon-color] cursor-pointer"><use href="#left-bar"></use></svg>
|
||||
</div>
|
||||
<!-- 最小化 -->
|
||||
<div v-if="minW" @click="minimizeWindow" class="hover-box">
|
||||
<div v-if="minW" @click="appWindow.minimize()" class="hover-box">
|
||||
<svg class="w-24px h-24px color-[--action-bar-icon-color] opacity-66 cursor-pointer">
|
||||
<use href="#maximize"></use>
|
||||
</svg>
|
||||
@ -38,20 +38,54 @@
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 关闭窗口 -->
|
||||
<div v-if="closeW" @click="closeWindow(currentLabel as string)" class="action-close">
|
||||
<div v-if="closeW" @click="handleCloseWin" class="action-close">
|
||||
<svg class="w-14px h-14px color-[--action-bar-icon-color] cursor-pointer">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 是否退到托盘提示框 -->
|
||||
<n-modal v-if="!tips.notTips" v-model:show="tipsRef.show" class="border-rd-8px">
|
||||
<div class="bg-[--bg-popover] w-290px h-full p-6px box-border flex flex-col">
|
||||
<svg @click="tipsRef.show = false" class="w-12px h-12px ml-a cursor-pointer select-none">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
<n-flex vertical :size="20" class="p-[22px_10px_10px_22px] select-none">
|
||||
<span class="text-16px">最小化还是直接退出程序?</span>
|
||||
<label class="text-14px text-#707070 flex gap-6px lh-16px items-center">
|
||||
<n-radio :checked="tipsRef.type === CloseBxEnum.HIDE" @change="tipsRef.type = CloseBxEnum.HIDE" />
|
||||
<span>最小化到系统托盘</span>
|
||||
</label>
|
||||
<label class="text-14px text-#707070 flex gap-6px lh-16px items-center">
|
||||
<n-radio :checked="tipsRef.type === CloseBxEnum.CLOSE" @change="tipsRef.type = CloseBxEnum.CLOSE" />
|
||||
<span>直接退出程序</span>
|
||||
</label>
|
||||
<label class="text-12px text-#909090 flex gap-6px justify-end items-center">
|
||||
<n-checkbox size="small" v-model:checked="tipsRef.notTips" />
|
||||
<span>下次不出现此提示</span>
|
||||
</label>
|
||||
|
||||
<n-flex justify="end">
|
||||
<n-button @click="handleConfirm" class="w-78px" color="#059669">确定</n-button>
|
||||
<n-button @click="tipsRef.show = false" class="w-78px" secondary>取消</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { closeWindow, maximizeWindow, minimizeWindow, unmaximize } from '@/common/WindowEvent.ts'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import Mitt from '@/utils/Bus'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { alwaysOnTop } from '@/stores/alwaysOnTop.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
import { CloseBxEnum, EventEnum, MittEnum } from '@/enums'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { PersistedStateOptions } from 'pinia-plugin-persistedstate'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
/**
|
||||
* 新版defineProps可以直接结构 { minW, maxW, closeW } 如果需要使用默认值withDefaults的时候使用新版解构方式会报错
|
||||
@ -77,7 +111,14 @@ const props = withDefaults(
|
||||
)
|
||||
const { minW, maxW, closeW, topWinLabel, shrinkStatus } = toRefs(props)
|
||||
const alwaysOnTopStore = alwaysOnTop()
|
||||
const settingStore = setting()
|
||||
const { tips, escClose } = storeToRefs(settingStore)
|
||||
const { resizeWindow } = useWindow()
|
||||
const tipsRef = reactive({
|
||||
type: tips.value.type,
|
||||
notTips: tips.value.notTips,
|
||||
show: false
|
||||
})
|
||||
// 窗口是否最大化状态
|
||||
const windowMaximized = ref(false)
|
||||
// 窗口是否置顶状态
|
||||
@ -90,41 +131,38 @@ watchEffect(() => {
|
||||
if (alwaysOnTopStatus.value) {
|
||||
appWindow.setAlwaysOnTop(alwaysOnTopStatus.value as boolean)
|
||||
}
|
||||
listen(EventEnum.LOGOUT, async () => {
|
||||
/* 退出账号前把窗口全部关闭 */
|
||||
if (appWindow.label !== 'login') {
|
||||
await appWindow.close()
|
||||
}
|
||||
})
|
||||
listen(EventEnum.EXIT, async () => {
|
||||
await invoke('exit').catch((error) => {
|
||||
console.error('退出失败:', error)
|
||||
})
|
||||
})
|
||||
|
||||
if (escClose.value) {
|
||||
window.addEventListener('keydown', (e) => isEsc(e))
|
||||
} else {
|
||||
window.removeEventListener('keydown', (e) => isEsc(e))
|
||||
}
|
||||
})
|
||||
|
||||
// todo 放大的时候图个拖动了窗口,窗口会变回原来的大小,但是图标的状态没有改变
|
||||
// // 定义一个可能保存unlisten函数的变量
|
||||
// let unlistenMoveEvent = null as any
|
||||
//
|
||||
// watchEffect(async () => {
|
||||
// if (windowMaximized.value) {
|
||||
// unlistenMoveEvent = await appWindow.listen('tauri://move', () => {
|
||||
// windowMaximized.value = false
|
||||
// unlistenMoveEvent()
|
||||
// unlistenMoveEvent = null
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
/* 判断当前是否是最大化窗口 */
|
||||
const checkMaximizedStatus = async () => {
|
||||
windowMaximized.value = await appWindow.isMaximized()
|
||||
}
|
||||
|
||||
/* 恢复窗口大小 */
|
||||
const restoreWindow = async () => {
|
||||
if (windowMaximized.value) {
|
||||
await unmaximize()
|
||||
await appWindow.unmaximize()
|
||||
} else {
|
||||
await maximizeWindow()
|
||||
await appWindow.maximize()
|
||||
}
|
||||
await checkMaximizedStatus()
|
||||
}
|
||||
|
||||
/* 收缩窗口 */
|
||||
const shrinkWindow = async () => {
|
||||
/*使用mitt给兄弟组件更新*/
|
||||
Mitt.emit('shrinkWindow', shrinkStatus.value)
|
||||
Mitt.emit(MittEnum.SHRINK_WINDOW, shrinkStatus.value)
|
||||
if (shrinkStatus.value) {
|
||||
await resizeWindow('home', 310, 700)
|
||||
} else {
|
||||
@ -140,6 +178,64 @@ const handleAlwaysOnTop = async () => {
|
||||
await appWindow.setAlwaysOnTop(isTop)
|
||||
}
|
||||
}
|
||||
|
||||
/* 点击确定时 */
|
||||
const handleConfirm = async () => {
|
||||
tips.value.type = tipsRef.type
|
||||
tips.value.notTips = tipsRef.notTips
|
||||
tipsRef.show = false
|
||||
if (tips.value.type === CloseBxEnum.CLOSE) {
|
||||
await emit(EventEnum.EXIT)
|
||||
} else {
|
||||
await nextTick(() => {
|
||||
appWindow.hide()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* 监听是否按下esc */
|
||||
const isEsc = (e: PersistedStateOptions) => {
|
||||
// 判断按下的是否是esc
|
||||
if (e.key === 'Escape' && escClose.value) {
|
||||
handleCloseWin()
|
||||
}
|
||||
}
|
||||
|
||||
// 判断当前是否是最大化
|
||||
const handleResize = () => {
|
||||
appWindow.isMaximized().then((res) => {
|
||||
windowMaximized.value = res
|
||||
})
|
||||
}
|
||||
|
||||
/* 处理关闭窗口事件 */
|
||||
const handleCloseWin = async () => {
|
||||
if (appWindow.label === 'home') {
|
||||
if (!tips.value.notTips) {
|
||||
tipsRef.show = true
|
||||
} else {
|
||||
if (tips.value.type === CloseBxEnum.CLOSE) {
|
||||
await emit(EventEnum.EXIT)
|
||||
} else {
|
||||
await nextTick(() => {
|
||||
appWindow.hide()
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await appWindow.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 添加和移除resize事件监听器
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('keydown', (e) => isEsc(e))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -149,4 +245,8 @@ const handleAlwaysOnTop = async () => {
|
||||
.action-close {
|
||||
@apply w-28px h24px flex-center cursor-pointer hover:bg-#c22b1c svg:hover:color-[#fff];
|
||||
}
|
||||
.n-modal {
|
||||
align-self: start;
|
||||
margin: 60px auto;
|
||||
}
|
||||
</style>
|
||||
|
@ -17,6 +17,7 @@ export enum RCodeEnum {
|
||||
/**业务出现问题*/
|
||||
BUSINESS_EXCEPTION = '600'
|
||||
}
|
||||
|
||||
/**URL*/
|
||||
export enum URLEnum {
|
||||
/**用户*/
|
||||
@ -27,12 +28,32 @@ export enum URLEnum {
|
||||
|
||||
/** tauri原生跨窗口通信时传输的类型 */
|
||||
export enum EventEnum {
|
||||
/** 主题 */
|
||||
THEME = 'theme',
|
||||
/** 窗口关闭 */
|
||||
WIN_CLOSE = 'winClose',
|
||||
/** 窗口显示 */
|
||||
WIN_SHOW = 'winShow'
|
||||
WIN_SHOW = 'winShow',
|
||||
/** 退出程序 */
|
||||
EXIT = 'exit',
|
||||
/** 退出账号 */
|
||||
LOGOUT = 'logout',
|
||||
/** 独立窗口 */
|
||||
ALONE = 'alone'
|
||||
}
|
||||
|
||||
/** Mitt兄弟组件通信 */
|
||||
export enum MittEnum {
|
||||
/** 更新消息数量 */
|
||||
UPDATE_MSG_TOTAL = 'updateMsgTotal',
|
||||
/** 显示消息框 */
|
||||
MSG_BOX_SHOW = 'msgBoxShow',
|
||||
/** 发送消息 */
|
||||
SEND_MESSAGE = 'sendMessage',
|
||||
/** 跳到发送信息 */
|
||||
TO_SEND_MSG = 'toSendMsg',
|
||||
/** 缩小窗口 */
|
||||
SHRINK_WINDOW = 'shrinkWindow',
|
||||
/** 详情页面显示 */
|
||||
DETAILS_SHOW = 'detailsShow'
|
||||
}
|
||||
|
||||
/** 主题类型 */
|
||||
@ -45,6 +66,16 @@ export enum ThemeEnum {
|
||||
OS = 'os'
|
||||
}
|
||||
|
||||
/* pinia存储的名称 */
|
||||
export enum StoresEnum {
|
||||
/** 置顶 */
|
||||
ALWAYS_ON_TOP = 'alwaysOnTop',
|
||||
/** 设置 */
|
||||
SETTING = 'setting',
|
||||
/** 在线状态 */
|
||||
ONLINE_STATUS = 'onlineStatus'
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
@ -72,5 +103,13 @@ export enum RoomTypeEnum {
|
||||
/** 1群聊 */
|
||||
GROUP = 1,
|
||||
/** 2单聊 */
|
||||
SINGLE
|
||||
SINGLE = 2
|
||||
}
|
||||
|
||||
/** 关闭窗口的行为 */
|
||||
export enum CloseBxEnum {
|
||||
/** 隐藏 */
|
||||
HIDE = 'hide',
|
||||
/** 关闭 */
|
||||
CLOSE = 'close'
|
||||
}
|
||||
|
139
src/hooks/useMessage.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
import { EventEnum, MittEnum } from '@/enums'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { delay } from 'lodash-es'
|
||||
import { MockList } from '@/mock'
|
||||
import { WebviewWindow } from '@tauri-apps/api/window'
|
||||
|
||||
const { createWebviewWindow, checkWinExist } = useWindow()
|
||||
/* 建议把此状态存入localStorage中 */
|
||||
const activeItem = ref(-1)
|
||||
const msgBoxShow = ref(false)
|
||||
/* 独立窗口的集合 */
|
||||
const aloneWin = ref(new Set())
|
||||
const shrinkStatus = ref(false)
|
||||
const itemRef = ref()
|
||||
export const useMessage = () => {
|
||||
/* 监听独立窗口关闭事件 */
|
||||
watchEffect(() => {
|
||||
Mitt.on(MittEnum.SHRINK_WINDOW, async (event) => {
|
||||
shrinkStatus.value = event as boolean
|
||||
})
|
||||
})
|
||||
|
||||
/* 处理点击选中消息 */
|
||||
const handleMsgClick = (item: MockItem) => {
|
||||
msgBoxShow.value = true
|
||||
activeItem.value = item.key
|
||||
const data = { msgBoxShow, item }
|
||||
Mitt.emit(MittEnum.MSG_BOX_SHOW, data)
|
||||
// 判断是否打开了独立的窗口
|
||||
if (aloneWin.value.has(EventEnum.ALONE + item.key)) {
|
||||
checkWinExist(EventEnum.ALONE + item.key).then()
|
||||
activeItem.value = -1
|
||||
Mitt.emit(MittEnum.MSG_BOX_SHOW, { item: -1 })
|
||||
}
|
||||
// 如果是收缩页面状态点击消息框就直接变成独立窗口
|
||||
if (shrinkStatus.value) {
|
||||
openAloneWin(item).then()
|
||||
}
|
||||
}
|
||||
|
||||
/* 处理双击事件 */
|
||||
const handleMsgDblclick = (item: MockItem) => {
|
||||
delay(async () => {
|
||||
await openAloneWin(item)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/* 打开独立窗口 */
|
||||
const openAloneWin = async (item: MockItem) => {
|
||||
itemRef.value = { ...item }
|
||||
if (activeItem.value === item.key) {
|
||||
activeItem.value = -1
|
||||
Mitt.emit(MittEnum.MSG_BOX_SHOW, { item: -1 })
|
||||
}
|
||||
// TODO 传递用户信息(这里的label最好使用用户唯一的id来代替) (nyh -> 2024-03-18 12:18:10)
|
||||
await createWebviewWindow(item.accountName, EventEnum.ALONE + item.key, 720, 800, '', true, true, 580)
|
||||
}
|
||||
|
||||
const menuList = ref<OPT.RightMenu[]>([
|
||||
{
|
||||
label: '置顶',
|
||||
icon: 'topping',
|
||||
click: (item: MockItem) => {
|
||||
const index = MockList.value.findIndex((e) => e.key === item.key)
|
||||
// 实现置顶功能
|
||||
if (index !== 0) {
|
||||
// 交换元素位置
|
||||
const temp = MockList.value[index]
|
||||
MockList.value[index] = MockList.value[0]
|
||||
MockList.value[0] = temp
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '复制账号',
|
||||
icon: 'copy',
|
||||
click: (item: MockItem) => {
|
||||
window.$message.success(`复制成功${item.key}`)
|
||||
}
|
||||
},
|
||||
{ label: '标记未读', icon: 'message-unread' },
|
||||
{
|
||||
label: '打开独立聊天窗口',
|
||||
icon: 'freezing-line-column',
|
||||
click: async (item: MockItem) => {
|
||||
await openAloneWin(item)
|
||||
}
|
||||
},
|
||||
{ label: '设置免打扰', icon: 'close-remind' }
|
||||
])
|
||||
|
||||
const specialMenuList = ref<OPT.RightMenu[]>([
|
||||
{
|
||||
label: '从消息列表中移除',
|
||||
icon: 'delete',
|
||||
click: (item: MockItem) => {
|
||||
// 根据key找到items中对应的下标
|
||||
const index = MockList.value.findIndex((e) => e.key === item.key)
|
||||
// 删除消息的时候判断是否已经打开了独立窗口
|
||||
if (aloneWin.value.has(`alone${index}`)) {
|
||||
const win = WebviewWindow.getByLabel(`alone${index}`)
|
||||
win?.close()
|
||||
}
|
||||
// 如果找到了对应的元素,则移除
|
||||
if (index !== -1) {
|
||||
const removeItem = MockList.value.splice(index, 1)[0]
|
||||
if (activeItem.value === removeItem.key) {
|
||||
if (index < MockList.value.length) {
|
||||
// 需要使用新的索引位置找到key更新activeItem.value
|
||||
activeItem.value = MockList.value[index].key
|
||||
handleMsgClick(MockList.value[index])
|
||||
} else {
|
||||
// 如果我们删除的是最后一个元素,则需要选中前一个元素
|
||||
activeItem.value = MockList.value[MockList.value.length - 1].key
|
||||
handleMsgClick(MockList.value[MockList.value.length - 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ label: '屏蔽此人消息', icon: 'forbid' }
|
||||
])
|
||||
|
||||
onMounted(async () => {
|
||||
await listen(EventEnum.ALONE, () => {
|
||||
emit(EventEnum.ALONE + itemRef.value.key, itemRef.value)
|
||||
if (aloneWin.value.has(EventEnum.ALONE + itemRef.value.key)) return
|
||||
aloneWin.value.add(EventEnum.ALONE + itemRef.value.key)
|
||||
})
|
||||
await listen(EventEnum.WIN_CLOSE, (e) => {
|
||||
aloneWin.value.delete(e.payload)
|
||||
})
|
||||
})
|
||||
|
||||
return { activeItem, msgBoxShow, handleMsgClick, handleMsgDblclick, menuList, specialMenuList }
|
||||
}
|
40
src/hooks/usePopover.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Ref } from 'vue'
|
||||
|
||||
/*! 这个是暂时用来解决在n-scrollbar中使用n-virtual-list使用n-popover时候滚动出现原生滚动条的方法 */
|
||||
export const usePopover = (selectKey: Ref<number>, id: string) => {
|
||||
/*! 暂时使用这些方法来阻止popover显示时候的滚动行为 */
|
||||
// 禁止滚动的默认行为
|
||||
const preventDefault = (e: Event) => e.preventDefault()
|
||||
const close = (event: any) => {
|
||||
if (!event.target.matches('.n-popover, .n-popover *')) {
|
||||
const scrollbar = document.querySelector(`#${id}`) as HTMLElement
|
||||
if (!scrollbar) return
|
||||
scrollbar.style.pointerEvents = ''
|
||||
window.removeEventListener('wheel', preventDefault) // 移除禁止滚轮滚动
|
||||
}
|
||||
}
|
||||
|
||||
const handlePopoverUpdate = (key: number) => {
|
||||
const scrollbar = document.querySelector(`#${id}`) as HTMLElement
|
||||
if (!scrollbar) return
|
||||
if (selectKey.value === key) {
|
||||
// 禁止 n-scrollbar 滚动
|
||||
scrollbar.style.pointerEvents = 'none'
|
||||
window.addEventListener('wheel', preventDefault, { passive: false }) // 禁止使用滚轮滚动页面
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', close, true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', close, true)
|
||||
})
|
||||
/*! end */
|
||||
|
||||
return {
|
||||
handlePopoverUpdate
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { WebviewWindow, LogicalSize } from '@tauri-apps/api/window'
|
||||
import { autoCloseWindow } from '@/common/WindowEvent.ts'
|
||||
import { LogicalSize, WebviewWindow } from '@tauri-apps/api/window'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
export const useWindow = () => {
|
||||
@ -26,9 +25,17 @@ export const useWindow = () => {
|
||||
minW = 310,
|
||||
minH = 540
|
||||
) => {
|
||||
const checkLabel = computed(() => {
|
||||
/* 如果是打开独立窗口就截取label中的固定label名称 */
|
||||
if (label.includes('alone')) {
|
||||
return label.replace(/\d/g, '')
|
||||
} else {
|
||||
return label
|
||||
}
|
||||
})
|
||||
const webview = new WebviewWindow(label, {
|
||||
title: title,
|
||||
url: `/${label}`,
|
||||
url: `/${checkLabel.value}`,
|
||||
fullscreen: false,
|
||||
resizable: resizable,
|
||||
center: true,
|
||||
@ -47,23 +54,14 @@ export const useWindow = () => {
|
||||
console.error('设置窗口阴影失败:', error)
|
||||
})
|
||||
if (wantCloseWindow) {
|
||||
await autoCloseWindow(wantCloseWindow)
|
||||
const win = WebviewWindow.getByLabel(wantCloseWindow)
|
||||
win?.close()
|
||||
}
|
||||
})
|
||||
|
||||
await webview.once('tauri://error', async () => {
|
||||
// TODO 这里利用错误处理的方式来查询是否是已经创建了窗口,如果一开始就使用WebviewWindow.getByLabel来查询在刷新的时候就会出现问题 (nyh -> 2024-03-06 23:54:17)
|
||||
const isExistsWinds = WebviewWindow.getByLabel(label)
|
||||
if (isExistsWinds) {
|
||||
// 如果窗口已存在,首先检查是否最小化了
|
||||
const minimized = await webview.isMinimized()
|
||||
if (minimized) {
|
||||
// 如果已最小化,恢复窗口
|
||||
await webview.unminimize()
|
||||
}
|
||||
// 如果窗口已存在,则给它焦点,使其在最前面显示
|
||||
await webview.setFocus()
|
||||
}
|
||||
await checkWinExist(label)
|
||||
})
|
||||
|
||||
return webview
|
||||
@ -86,8 +84,32 @@ export const useWindow = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查窗口是否存在
|
||||
* @param L 窗口标签
|
||||
*/
|
||||
const checkWinExist = async (L: string) => {
|
||||
const isExistsWinds = WebviewWindow.getByLabel(L)
|
||||
if (isExistsWinds) {
|
||||
// 如果窗口已存在,首先检查是否最小化了
|
||||
const minimized = await isExistsWinds.isMinimized()
|
||||
// 检查是否是隐藏
|
||||
const hidden = await isExistsWinds.isVisible()
|
||||
if (!hidden) {
|
||||
await isExistsWinds.show()
|
||||
}
|
||||
if (minimized) {
|
||||
// 如果已最小化,恢复窗口
|
||||
await isExistsWinds.unminimize()
|
||||
}
|
||||
// 如果窗口已存在,则给它焦点,使其在最前面显示
|
||||
await isExistsWinds.setFocus()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createWebviewWindow,
|
||||
resizeWindow
|
||||
resizeWindow,
|
||||
checkWinExist
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<main data-tauri-drag-region class="resizable select-none" :style="{ width: width + 'px' }">
|
||||
<ActionBar class="absolute right-0" v-if="shrinkStatus" :shrink-status="!shrinkStatus" :max-w="false" />
|
||||
<ActionBar
|
||||
class="absolute right-0"
|
||||
v-if="shrinkStatus"
|
||||
:shrink-status="!shrinkStatus"
|
||||
:max-w="false"
|
||||
:current-label="appWindow.label" />
|
||||
|
||||
<!-- <div class="resize-handle" @mousedown="initDrag"></div>-->
|
||||
|
||||
@ -10,10 +15,12 @@
|
||||
class="mt-30px w-full h-38px flex flex-col items-center">
|
||||
<div class="flex-center gap-5px w-full pr-16px pl-16px box-border">
|
||||
<n-input
|
||||
:on-focus="() => router.push('/searchDetails')"
|
||||
id="search"
|
||||
@focus="() => router.push('/searchDetails')"
|
||||
class="rounded-4px w-full"
|
||||
style="background: var(--search-bg-color)"
|
||||
:maxlength="20"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="搜索">
|
||||
<template #prefix>
|
||||
@ -29,7 +36,7 @@
|
||||
</header>
|
||||
|
||||
<!-- 列表 -->
|
||||
<n-scrollbar style="max-height: calc(100vh - 70px)">
|
||||
<n-scrollbar style="max-height: calc(100vh - 70px)" id="scrollbar">
|
||||
<div class="h-full flex-1 p-[4px_10px_0px_8px]">
|
||||
<router-view />
|
||||
</div>
|
||||
@ -40,6 +47,8 @@
|
||||
<script setup lang="ts">
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import router from '@/router'
|
||||
import { MittEnum } from '@/enums'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
|
||||
// const minWidth = 160 // 设置最小宽度
|
||||
// const maxWidth = 320 // 设置最大宽度
|
||||
@ -50,11 +59,27 @@ const width = ref(250) // 初始化宽度
|
||||
const shrinkStatus = ref(false)
|
||||
|
||||
// todo 1.了解这里是怎么实现的 2.修改拖拽放大缩小的事件
|
||||
Mitt.on('shrinkWindow', (event) => {
|
||||
Mitt.on(MittEnum.SHRINK_WINDOW, (event) => {
|
||||
shrinkStatus.value = event as boolean
|
||||
width.value = 250
|
||||
})
|
||||
|
||||
const closeMenu = (event: Event) => {
|
||||
const e = event.target as HTMLInputElement
|
||||
const route = router.currentRoute.value.path
|
||||
/* 判断如果点击的搜索框,就关闭消息列表 */
|
||||
if (!e.matches('#scrollbar, #scrollbar *, #search *, #search') && route === '/searchDetails') {
|
||||
router.go(-1)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', closeMenu, true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', closeMenu, true)
|
||||
})
|
||||
// watchEffect(() => {
|
||||
// if (width.value === maxWidth) {
|
||||
// Mitt.emit('shrinkWindow', false)
|
||||
@ -86,21 +111,6 @@ Mitt.on('shrinkWindow', (event) => {
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.resizable {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--center-bg-color);
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
cursor: ew-resize;
|
||||
background-color: #ccc; /* 可以根据需要更改颜色 */
|
||||
}
|
||||
<style scoped lang="scss">
|
||||
@import 'style';
|
||||
</style>
|
||||
|
16
src/layout/center/style.scss
Normal file
@ -0,0 +1,16 @@
|
||||
.resizable {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--center-bg-color);
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
cursor: ew-resize;
|
||||
background-color: #ccc; /* 可以根据需要更改颜色 */
|
||||
}
|
@ -11,6 +11,7 @@ import Center from './center/index.vue'
|
||||
import Left from './left/index.vue'
|
||||
import Right from './right/index.vue'
|
||||
import Mitt from '@/utils/Bus'
|
||||
import { MittEnum } from '@/enums'
|
||||
|
||||
/* todo home窗口创建的时候已经设置了resizable: true,可以调整大小了,但是还是不可以调整大小 */
|
||||
const shrinkStatus = ref(false)
|
||||
@ -18,7 +19,7 @@ const shrinkStatus = ref(false)
|
||||
* event默认如果没有传递值就为true,所以shrinkStatus的值为false就会发生值的变化
|
||||
* 因为shrinkStatus的值为false,所以v-if="!shrinkStatus" 否则right组件刚开始渲染的时候不会显示
|
||||
* */
|
||||
Mitt.on('shrinkWindow', async (event) => {
|
||||
Mitt.on(MittEnum.SHRINK_WINDOW, (event) => {
|
||||
shrinkStatus.value = event as boolean
|
||||
})
|
||||
</script>
|
||||
|
@ -1,27 +1,10 @@
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { delay } from 'lodash-es'
|
||||
|
||||
type TopActive = {
|
||||
url: string
|
||||
icon: string
|
||||
iconAction?: string
|
||||
badge?: number
|
||||
}[]
|
||||
|
||||
type BottomActive = {
|
||||
title: string
|
||||
url: string
|
||||
label: string
|
||||
icon: string
|
||||
iconAction?: string
|
||||
}[]
|
||||
|
||||
type MenuList = {
|
||||
label: string
|
||||
icon: string
|
||||
click: () => void
|
||||
}[]
|
||||
const { createWebviewWindow } = useWindow()
|
||||
const itemsTop = ref<TopActive>([
|
||||
const itemsTop = ref<OPT.L.Top[]>([
|
||||
{
|
||||
url: 'message',
|
||||
icon: 'message',
|
||||
@ -38,7 +21,7 @@ const itemsTop = ref<TopActive>([
|
||||
iconAction: 'fire-action'
|
||||
}
|
||||
])
|
||||
const itemsBottom: BottomActive = [
|
||||
const itemsBottom: OPT.L.Bottom[] = [
|
||||
{
|
||||
title: '邮件',
|
||||
url: '/mail',
|
||||
@ -62,7 +45,7 @@ const itemsBottom: BottomActive = [
|
||||
}
|
||||
]
|
||||
/* 设置列表菜单项 */
|
||||
const menuList = ref<MenuList>([
|
||||
const moreList = ref<OPT.L.MoreList[]>([
|
||||
{
|
||||
label: '检查更新',
|
||||
icon: 'arrow-circle-up',
|
||||
@ -92,10 +75,15 @@ const menuList = ref<MenuList>([
|
||||
icon: 'power',
|
||||
click: async () => {
|
||||
// todo 退出账号 需要关闭其他的全部窗口
|
||||
// 1.需要退出账号
|
||||
await createWebviewWindow('登录', 'login', 320, 448, 'home', true, false, 320, 448)
|
||||
await createWebviewWindow('登录', 'login', 320, 448, 'home', true, false, 320, 448).then(() => {
|
||||
/* 给一点延迟,不然创建登录窗口后还没有来得及设置阴影和圆角效果 */
|
||||
delay(async () => {
|
||||
/* 通知全部打开的窗口然后关闭 */
|
||||
await emit(EventEnum.LOGOUT)
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
export { itemsTop, itemsBottom, menuList }
|
||||
export { itemsTop, itemsBottom, moreList }
|
@ -1,15 +1,82 @@
|
||||
<template>
|
||||
<main class="left w-60px h-full p-[30px_6px_15px] box-border flex-col-center select-none">
|
||||
<!-- 头像 -->
|
||||
<div class="relative w-36px h-36px rounded-50%">
|
||||
<img class="rounded-50% wh-full bg-#fff cursor-pointer" :src="'https://picsum.photos/140'" alt="" />
|
||||
<main data-tauri-drag-region class="left w-60px h-full p-[30px_6px_15px] box-border flex-col-center select-none">
|
||||
<!-- 点击时头像内容框 -->
|
||||
<n-popover
|
||||
v-model:show="infoShow"
|
||||
trigger="click"
|
||||
:show-arrow="false"
|
||||
:placement="shrinkStatus ? 'bottom-start' : 'right-start'"
|
||||
style="padding: 0; background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(10px)">
|
||||
<template #trigger>
|
||||
<!-- 头像 -->
|
||||
<div class="relative w-36px h-36px rounded-50% cursor-pointer">
|
||||
<n-avatar round :color="'#fff'" :size="36" :src="'https://picsum.photos/140'" fallback-src="/logo.png" />
|
||||
|
||||
<div
|
||||
class="bg-[--bg-avatar] text-10px rounded-50% w-10px h-10px absolute bottom-0 right-0"
|
||||
style="border: 2px solid var(--bg-avatar)">
|
||||
<span>⛅</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@click.stop="openContent('在线状态', 'onlineStatus', 320, 480)"
|
||||
class="bg-[--bg-avatar] text-10px rounded-50% w-12px h-12px absolute bottom--2px right--2px"
|
||||
style="border: 2px solid var(--bg-avatar)">
|
||||
<img class="rounded-50% wh-full" :src="url" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 用户个人信息框 -->
|
||||
<n-flex
|
||||
vertical
|
||||
:size="26"
|
||||
class="wh-full p-15px box-border rounded-8px"
|
||||
:style="`background: linear-gradient(to bottom, ${bgColor} 0%, ${themeColor} 100%)`">
|
||||
<!-- 头像以及信息区域 -->
|
||||
<n-flex justify="space-between" align="center" :size="25">
|
||||
<n-flex>
|
||||
<img class="w-68px h-68px rounded-50% select-none" :src="'https://picsum.photos/140'" alt="" />
|
||||
|
||||
<n-flex vertical justify="center" :size="10" class="text-[--text-color]">
|
||||
<span class="text-18px">用户名</span>
|
||||
<span class="text-12px text-[--info-text-color]">账号 763868126381</span>
|
||||
<n-flex
|
||||
@click="openContent('在线状态', 'onlineStatus', 320, 480)"
|
||||
:size="5"
|
||||
align="center"
|
||||
style="margin-left: -4px"
|
||||
class="item-hover">
|
||||
<img class="rounded-50% w-18px h-18px" :src="url" alt="" />
|
||||
<span>{{ title }}</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<n-flex vertical align="center" :size="5" class="item-hover">
|
||||
<svg class="w-20px h-20px"><use href="#thumbs-up"></use></svg>
|
||||
<span class="text-12px">9999+</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
<!-- 地址 -->
|
||||
<n-flex :size="26" class="select-none">
|
||||
<span class="text-[--info-text-color]">所在地</span>
|
||||
<span>中国</span>
|
||||
</n-flex>
|
||||
<!-- 动态 -->
|
||||
<n-flex :size="40" class="select-none">
|
||||
<span class="text-[--info-text-color]">动态</span>
|
||||
<n-image-group>
|
||||
<n-flex :size="6" :class="shrinkStatus ? 'overflow-hidden w-180px' : ''" :wrap="false">
|
||||
<n-image
|
||||
v-for="n in 4"
|
||||
:key="n"
|
||||
preview-disabled
|
||||
class="rounded-8px"
|
||||
width="50"
|
||||
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" />
|
||||
</n-flex>
|
||||
</n-image-group>
|
||||
</n-flex>
|
||||
|
||||
<n-flex justify="center" align="center" :size="40">
|
||||
<n-button secondary> 编辑资料 </n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-popover>
|
||||
|
||||
<div data-tauri-drag-region class="flex-1 mt-20px flex-col-x-center justify-between">
|
||||
<!-- 上部分操作栏 -->
|
||||
@ -53,7 +120,7 @@
|
||||
<!-- 更多选项面板 -->
|
||||
<div v-if="settingShow" class="setting-item">
|
||||
<div class="menu-list">
|
||||
<div v-for="(item, index) in menuList" :key="index">
|
||||
<div v-for="(item, index) in moreList" :key="index">
|
||||
<div class="menu-item" @click="() => item.click()">
|
||||
<svg><use :href="`#${item.icon}`"></use></svg>
|
||||
{{ item.label }}
|
||||
@ -70,33 +137,51 @@ import { delay } from 'lodash-es'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import router from '@/router'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { EventEnum, MittEnum, ThemeEnum } from '@/enums'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { itemsTop, itemsBottom, menuList } from './configure.ts'
|
||||
import { itemsTop, itemsBottom, moreList } from './config.ts'
|
||||
import { onlineStatus } from '@/stores/onlineStatus.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
|
||||
const prefers = matchMedia('(prefers-color-scheme: dark)')
|
||||
const { createWebviewWindow } = useWindow()
|
||||
const settingStore = setting()
|
||||
const { themes } = storeToRefs(settingStore)
|
||||
const OLStatusStore = onlineStatus()
|
||||
const { url, title, bgColor } = storeToRefs(OLStatusStore)
|
||||
/*当前选中的元素 默认选中itemsTop的第一项*/
|
||||
const activeItem = ref<string>(itemsTop.value[0].url)
|
||||
const settingShow = ref(false)
|
||||
const shrinkStatus = ref(false)
|
||||
const infoShow = ref(false)
|
||||
const themeColor = ref(themes.value.content === ThemeEnum.DARK ? 'rgba(63,63,63, 0.2)' : 'rgba(241,241,241, 0.2)')
|
||||
/* 已打开窗口的列表 */
|
||||
const openWindowsList = ref(new Set())
|
||||
const { createWebviewWindow } = useWindow()
|
||||
|
||||
watchEffect(async () => {
|
||||
Mitt.on('updateMsgTotal', (event) => {
|
||||
/* 跟随系统主题模式切换主题 */
|
||||
const followOS = () => {
|
||||
themeColor.value = prefers.matches ? 'rgba(63,63,63, 0.2)' : 'rgba(241,241,241, 0.2)'
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
Mitt.on(MittEnum.UPDATE_MSG_TOTAL, (event) => {
|
||||
itemsTop.value.find((item) => {
|
||||
if (item.url === 'message') {
|
||||
item.badge = event as number
|
||||
}
|
||||
})
|
||||
})
|
||||
await listen(EventEnum.WIN_SHOW, (e) => {
|
||||
// 如果已经存在就不添加
|
||||
if (openWindowsList.value.has(e.payload)) return
|
||||
openWindowsList.value.add(e.payload)
|
||||
})
|
||||
await listen(EventEnum.WIN_CLOSE, (e) => {
|
||||
openWindowsList.value.delete(e.payload)
|
||||
Mitt.on(MittEnum.TO_SEND_MSG, (event: any) => {
|
||||
activeItem.value = event.url
|
||||
})
|
||||
/* 判断是否是跟随系统主题 */
|
||||
if (themes.value.pattern === ThemeEnum.OS) {
|
||||
followOS()
|
||||
prefers.addEventListener('change', followOS)
|
||||
} else {
|
||||
prefers.removeEventListener('change', followOS)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
@ -119,11 +204,14 @@ const pageJumps = (url: string) => {
|
||||
* 打开内容对应窗口
|
||||
* @param title 窗口的标题
|
||||
* @param label 窗口的标识
|
||||
* @param w 窗口的宽度
|
||||
* @param h 窗口的高度
|
||||
* */
|
||||
const openContent = (title: string, label: string) => {
|
||||
const openContent = (title: string, label: string, w = 840, h = 600) => {
|
||||
delay(async () => {
|
||||
await createWebviewWindow(title, label, 840, 600)
|
||||
await createWebviewWindow(title, label, w, h)
|
||||
}, 300)
|
||||
infoShow.value = false
|
||||
}
|
||||
|
||||
const closeMenu = (event: any) => {
|
||||
@ -132,10 +220,22 @@ const closeMenu = (event: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
/* 页面加载的时候默认显示消息列表 */
|
||||
pageJumps(activeItem.value)
|
||||
window.addEventListener('click', closeMenu, true)
|
||||
|
||||
Mitt.on(MittEnum.SHRINK_WINDOW, (event) => {
|
||||
shrinkStatus.value = event as boolean
|
||||
})
|
||||
await listen(EventEnum.WIN_SHOW, (e) => {
|
||||
// 如果已经存在就不添加
|
||||
if (openWindowsList.value.has(e.payload)) return
|
||||
openWindowsList.value.add(e.payload)
|
||||
})
|
||||
await listen(EventEnum.WIN_CLOSE, (e) => {
|
||||
openWindowsList.value.delete(e.payload)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -144,46 +244,5 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@mixin action() {
|
||||
padding: 6px 8px 6px 8px;
|
||||
&:not(.active):hover {
|
||||
background: rgba(193, 193, 193, 0.4);
|
||||
border-radius: 8px;
|
||||
color: #059669;
|
||||
cursor: pointer;
|
||||
animation: linearAnimation 3s linear forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
background: var(--left-bg-color);
|
||||
}
|
||||
|
||||
.top-action,
|
||||
.bottom-action,
|
||||
.more {
|
||||
@include action;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: var(--left-active-color);
|
||||
border-radius: 8px;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
@include menu-item-style(absolute);
|
||||
left: 58px;
|
||||
bottom: 10px;
|
||||
@include menu-list();
|
||||
}
|
||||
|
||||
:deep(.n-badge .n-badge-sup) {
|
||||
font-weight: bold;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
:deep(.n-badge) {
|
||||
color: inherit;
|
||||
}
|
||||
@import 'style';
|
||||
</style>
|
||||
|
47
src/layout/left/style.scss
Normal file
@ -0,0 +1,47 @@
|
||||
@mixin action() {
|
||||
padding: 6px 8px 6px 8px;
|
||||
&:not(.active):hover {
|
||||
background: rgba(193, 193, 193, 0.4);
|
||||
border-radius: 8px;
|
||||
color: #059669;
|
||||
cursor: pointer;
|
||||
animation: linearAnimation 3s linear forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
background: var(--left-bg-color);
|
||||
}
|
||||
|
||||
.top-action,
|
||||
.bottom-action,
|
||||
.more {
|
||||
@include action;
|
||||
}
|
||||
|
||||
.active {
|
||||
background: var(--left-active-color);
|
||||
border-radius: 8px;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
@include menu-item-style(absolute);
|
||||
left: 58px;
|
||||
bottom: 10px;
|
||||
@include menu-list();
|
||||
}
|
||||
|
||||
.item-hover {
|
||||
@apply select-none hover:bg-[--info-hover] cursor-pointer w-fit rounded-10px p-4px;
|
||||
transition: all 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
:deep(.n-badge .n-badge-sup) {
|
||||
font-weight: bold;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
:deep(.n-badge) {
|
||||
color: inherit;
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<main class="flex-1 bg-[--right-bg-color] h-full w-100vw">
|
||||
<ActionBar />
|
||||
<ActionBar :current-label="appWindow.label" />
|
||||
<!-- 需要判断当前路由是否是信息详情界面 -->
|
||||
<ChatBox :active-item="activeItem" v-if="msgBoxShow && isChat" />
|
||||
<ChatBox :active-item="activeItem" v-if="msgBoxShow && isChat && activeItem !== -1" />
|
||||
|
||||
<Details :content="DetailsContent" v-else-if="detailsShow && isDetails" />
|
||||
|
||||
@ -16,18 +16,18 @@
|
||||
<script setup lang="ts">
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import router from '@/router'
|
||||
import { theme } from '@/stores/theme.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { EventEnum, ThemeEnum } from '@/enums'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { MittEnum, ThemeEnum } from '@/enums'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
|
||||
const themeStore = theme()
|
||||
const { THEME, PATTERN } = storeToRefs(themeStore)
|
||||
const settingStore = setting()
|
||||
const { themes } = storeToRefs(settingStore)
|
||||
const msgBoxShow = ref(false)
|
||||
const detailsShow = ref(false)
|
||||
const activeItem = ref()
|
||||
const DetailsContent = ref()
|
||||
const imgTheme = ref(THEME.value)
|
||||
const imgTheme = ref(themes.value.content)
|
||||
const prefers = matchMedia('(prefers-color-scheme: dark)')
|
||||
// 判断当前路由是否是聊天界面
|
||||
const isChat = computed(() => {
|
||||
@ -43,37 +43,26 @@ const followOS = () => {
|
||||
imgTheme.value = prefers.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT
|
||||
}
|
||||
|
||||
/* 监听其他标签页的变化 */
|
||||
listen(EventEnum.THEME, (e) => {
|
||||
if (e.payload === ThemeEnum.OS) {
|
||||
followOS()
|
||||
prefers.addEventListener('change', followOS)
|
||||
} else {
|
||||
imgTheme.value = (e.payload || ThemeEnum.LIGHT) as string
|
||||
prefers.removeEventListener('change', followOS)
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (PATTERN.value === ThemeEnum.OS) {
|
||||
if (themes.value.pattern === ThemeEnum.OS) {
|
||||
followOS()
|
||||
prefers.addEventListener('change', followOS)
|
||||
} else {
|
||||
imgTheme.value = THEME.value || ThemeEnum.LIGHT
|
||||
imgTheme.value = themes.value.content || ThemeEnum.LIGHT
|
||||
prefers.removeEventListener('change', followOS)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (isChat) {
|
||||
Mitt.on('msgBoxShow', (event: any) => {
|
||||
Mitt.on(MittEnum.MSG_BOX_SHOW, (event: any) => {
|
||||
msgBoxShow.value = event.msgBoxShow
|
||||
activeItem.value = event.item
|
||||
})
|
||||
}
|
||||
|
||||
if (isDetails) {
|
||||
Mitt.on('detailsShow', (event: any) => {
|
||||
Mitt.on(MittEnum.DETAILS_SHOW, (event: any) => {
|
||||
DetailsContent.value = event.data
|
||||
detailsShow.value = event.detailsShow as boolean
|
||||
})
|
||||
|