!4 更新到master分支

Merge pull request !4 from nongyehong/dev
This commit is contained in:
nongyehong 2024-03-26 21:48:01 +00:00 committed by Gitee
commit d5bcf543ae
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
138 changed files with 4174 additions and 1401 deletions

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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_ 交互,根据提示完成信息的输入和选择

View File

@ -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>

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -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": {

File diff suppressed because it is too large Load Diff

BIN
public/msg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

BIN
public/status/IonBan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/status/aiziji@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/status/banzhuan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
public/status/boring@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
public/status/busy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/status/chigua@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
public/status/cloaking.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/status/crush.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/status/emonew@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
public/status/fish@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/status/game_3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
public/status/ganzuoye.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
public/status/imfine_3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
public/status/jinli@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
public/status/leave.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/status/luck@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
public/status/music@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
public/status/nandehutu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
public/status/offline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/status/online.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
public/status/signal_3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
public/status/stayup_3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
public/status/study_3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
public/status/timi_3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/status/tkong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
public/status/tv_3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
public/status/wang_3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
public/status/xiadaxue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/status/ximao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

829
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -1,2 +1,3 @@
pub mod tray;
pub mod window;
pub mod window;
pub mod plugins;

View 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();
}
}));
}

View File

@ -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();
}
});
}
}

View File

@ -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")]

View File

@ -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");
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

BIN
src-tauri/tray/msg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -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()
}

View File

@ -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()">

View 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>

View File

@ -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
// localStorageTHEME(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'

View File

@ -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)
}

View File

@ -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事件 */

View File

@ -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>

View File

@ -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)">
<!-- &lt;!&ndash; 渲染消息内容体 &ndash;&gt;-->
<!-- <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)">
<!-- &lt;!&ndash; 渲染消息内容体 &ndash;&gt;-->
<!-- <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>

View 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 popovern-scrollbarn-virtual-list使n-popover (nyh -> 2024-03-24 22:46:38) -->
<!-- // TODO 使n-virtual-listn-popoverbug (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>

View 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 }

View File

@ -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>

View File

@ -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,

View File

@ -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>

View File

@ -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
View 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
View 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
}
}

View File

@ -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
}
}

View File

@ -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>

View 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; /* 可以根据需要更改颜色 */
}

View File

@ -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>

View File

@ -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 }

View File

@ -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>

View 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;
}

View File

@ -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
})

Some files were not shown because too many files have changed in this diff Show More