mirror of
https://gitee.com/HuLaSpark/HuLa.git
synced 2024-11-29 18:28:30 +08:00
⚡ perf(system): 优化系统托盘图标闪烁
This commit is contained in:
parent
4f81f50e13
commit
5614750ead
18
src-tauri/Cargo.lock
generated
18
src-tauri/Cargo.lock
generated
@ -1262,6 +1262,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tokio",
|
||||
"window-shadows",
|
||||
"window-vibrancy",
|
||||
]
|
||||
@ -3088,14 +3089,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.35.0"
|
||||
version = "1.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c"
|
||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.41",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -25,6 +25,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
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,3 +1,4 @@
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
@ -31,4 +32,58 @@ pub fn set_tray_icon(app: AppHandle) {
|
||||
#[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();
|
||||
}
|
||||
}));
|
||||
}
|
@ -29,6 +29,9 @@ fn red_icon(app: &AppHandle) {
|
||||
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);
|
||||
},
|
||||
|
@ -1,25 +1,32 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::sync::Mutex;
|
||||
use tauri::{SystemTray};
|
||||
use crate::common::window::set_window_attribute;
|
||||
use crate::common::plugins::{ exit, reset_set_window, set_tray_icon };
|
||||
use crate::common::plugins::{ exit, reset_set_window, set_tray_icon, tray_blink, TrayState };
|
||||
|
||||
mod common;
|
||||
|
||||
|
||||
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(system_tray)// 将 `tauri.conf.json` 上配置的图标添加到系统托盘
|
||||
.on_system_tray_event(common::tray::handler) // 注册系统托盘事件处理程序
|
||||
.invoke_handler(tauri::generate_handler![reset_set_window, set_tray_icon, exit]) // 使用定义的插件
|
||||
.invoke_handler(tauri::generate_handler![reset_set_window, set_tray_icon, exit, tray_blink]) // 使用定义的插件
|
||||
.on_window_event(|event| match event.event() {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
event.window().hide().unwrap();
|
||||
|
BIN
src-tauri/tray/msg-sub.png
Normal file
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
BIN
src-tauri/tray/msg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 601 B |
@ -73,7 +73,7 @@
|
||||
<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>
|
||||
|
@ -21,7 +21,10 @@
|
||||
:class="item.accountId === userId ? 'flex-row-reverse' : ''"
|
||||
style="max-width: calc(100% - 54px)">
|
||||
<!-- 头像 -->
|
||||
<ContextMenu :menu="optionsList" :special-menu="report">
|
||||
<ContextMenu
|
||||
@select="$event.click(item)"
|
||||
:menu="activeItem.type === RoomTypeEnum.GROUP ? optionsList : []"
|
||||
:special-menu="report">
|
||||
<img
|
||||
:class="item.accountId === userId ? '' : 'mr-10px'"
|
||||
class="w-34px rounded-50% select-none"
|
||||
@ -31,7 +34,10 @@
|
||||
<div
|
||||
class="flex flex-col gap-8px color-[--text-color]"
|
||||
:class="item.accountId === userId ? 'items-end mr-10px' : ''">
|
||||
<ContextMenu :menu="optionsList" :special-menu="report">
|
||||
<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>
|
||||
@ -39,7 +45,7 @@
|
||||
<!-- 气泡样式 -->
|
||||
<ContextMenu
|
||||
:data-key="item.accountId === userId ? `U${item.key}` : `Q${item.key}`"
|
||||
@select="$event.click(item.key)"
|
||||
@select="$event.click(item)"
|
||||
:menu="menuList"
|
||||
:special-menu="specialMenuList"
|
||||
@click="handleMsgClick(item)">
|
||||
@ -136,6 +142,7 @@ 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'
|
||||
|
||||
const activeBubble = ref(-1)
|
||||
const userId = ref(10086)
|
||||
@ -191,54 +198,14 @@ const { activeItem } = defineProps<{
|
||||
// }
|
||||
// })
|
||||
// const message = computed(() => msg.value)
|
||||
|
||||
/* 右键用户信息菜单(单聊的时候显示) */
|
||||
const optionsList = computed(() => {
|
||||
if (activeItem.type === RoomTypeEnum.GROUP) {
|
||||
return [
|
||||
{
|
||||
label: '发送信息',
|
||||
icon: 'message-action',
|
||||
click: () => {}
|
||||
},
|
||||
{
|
||||
label: 'TA',
|
||||
icon: 'aite',
|
||||
click: () => {}
|
||||
},
|
||||
{
|
||||
label: '查看资料',
|
||||
icon: 'notes',
|
||||
click: () => {}
|
||||
},
|
||||
{
|
||||
label: '添加好友',
|
||||
icon: 'people-plus',
|
||||
click: () => {}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
/* 举报选项 */
|
||||
const report = computed(() => {
|
||||
if (activeItem.type === RoomTypeEnum.GROUP) {
|
||||
return [
|
||||
{
|
||||
label: '举报',
|
||||
icon: 'caution',
|
||||
click: () => {}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
/* 右键消息菜单列表 */
|
||||
const menuList = ref<OPT.RightMenu[]>([
|
||||
{
|
||||
label: '复制',
|
||||
icon: 'copy',
|
||||
click: (index: number) => {
|
||||
click: (item: any) => {
|
||||
// 复制内容到剪贴板
|
||||
copyrightComputed.value.copy(index)
|
||||
copyrightComputed.value.copy(item.key)
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -391,7 +358,13 @@ const closeMenu = (event: any) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
invoke('set_tray_icon').catch((error) => {
|
||||
// 启动图标闪烁
|
||||
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) => {
|
||||
|
@ -1,20 +1,17 @@
|
||||
<template>
|
||||
<!--! 这里最好不要使用n-flex,滚动高度会有问题 -->
|
||||
<main
|
||||
v-if="activeItem?.type === RoomTypeEnum.GROUP"
|
||||
style="border-left: 1px solid var(--line-color)"
|
||||
class="item-box">
|
||||
<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"
|
||||
autofocus
|
||||
clearable
|
||||
placeholder="搜索"
|
||||
type="text"
|
||||
size="tiny"
|
||||
@ -26,16 +23,29 @@
|
||||
</n-flex>
|
||||
|
||||
<n-virtual-list
|
||||
id="image-scroll-container"
|
||||
ref="virtualListInst"
|
||||
style="max-height: calc(100vh - 130px)"
|
||||
item-resizable
|
||||
:item-size="42"
|
||||
:items="MockList">
|
||||
<template #default="{ item }">
|
||||
<n-flex :key="item.key" :size="10" align="center" class="item">
|
||||
<n-avatar lazy round :size="24" :src="item.avatar"></n-avatar>
|
||||
<span class="text-12px">{{ item.accountName }}</span>
|
||||
</n-flex>
|
||||
<ContextMenu @select="$event.click(item)" :menu="optionsList" :special-menu="report">
|
||||
<n-flex :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-scroll-container'
|
||||
}"></n-avatar>
|
||||
<span class="text-12px">{{ item.accountName }}</span>
|
||||
</n-flex>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</main>
|
||||
@ -45,12 +55,13 @@ 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'
|
||||
|
||||
const isSearch = ref(false)
|
||||
const searchRef = ref('')
|
||||
const inputInstRef = ref<InputInst | null>(null)
|
||||
const { activeItem } = defineProps<{
|
||||
activeItem?: MockItem
|
||||
activeItem: MockItem
|
||||
}>()
|
||||
|
||||
const handleSearch = () => {
|
||||
@ -62,25 +73,5 @@ const handleSearch = () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item-box {
|
||||
@apply flex flex-col w-180px h-100vh z-20 p-[12px_0_12px_6px] box-border select-none text-[--text-color];
|
||||
.item {
|
||||
height: 42px;
|
||||
padding-left: 4px;
|
||||
width: 95%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--bg-group-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*! 修改naive-ui虚拟列表滚动条的宽度 */
|
||||
:deep(
|
||||
.n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--vertical,
|
||||
.n-scrollbar + .n-scrollbar-rail.n-scrollbar-rail--vertical
|
||||
) {
|
||||
width: 6px;
|
||||
}
|
||||
@import '@/styles/scss/chat-sidebar';
|
||||
</style>
|
||||
|
36
src/components/rightBox/chatBox/config.ts
Normal file
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 }
|
@ -45,8 +45,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 是否退到托盘提示框 -->
|
||||
<n-modal v-if="!tray.notTips" v-model:show="tray.tips" class="w-350px border-rd-8px">
|
||||
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
|
||||
<n-modal v-if="!tray.notTips" v-model:show="tray.tips" class="border-rd-8px">
|
||||
<div class="bg-[--bg-popover] w-290px h-full p-6px box-border flex flex-col">
|
||||
<svg @click="tray.tips = false" class="w-12px h-12px ml-a cursor-pointer select-none">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
|
@ -18,6 +18,14 @@ export enum RCodeEnum {
|
||||
BUSINESS_EXCEPTION = '600'
|
||||
}
|
||||
|
||||
/**URL*/
|
||||
export enum URLEnum {
|
||||
/**用户*/
|
||||
USER = '/user',
|
||||
/**文章*/
|
||||
ARTICLE = '/article'
|
||||
}
|
||||
|
||||
/** tauri原生跨窗口通信时传输的类型 */
|
||||
export enum EventEnum {
|
||||
/** 主题 */
|
||||
|
139
src/hooks/useMessage.ts
Normal file
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 }
|
||||
}
|
@ -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>
|
||||
@ -41,6 +48,7 @@
|
||||
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 // 设置最大宽度
|
||||
@ -56,6 +64,21 @@ Mitt.on(MittEnum.SHRINK_WINDOW, (event) => {
|
||||
width.value = 250
|
||||
})
|
||||
|
||||
const closeMenu = (event: Event) => {
|
||||
const e = event.target as HTMLInputElement
|
||||
/* 判断如果点击的不是滚动条和搜索框,就关闭消息列表 */
|
||||
if (!e.matches('#scrollbar, #scrollbar *, #search *, #search')) {
|
||||
router.push('/message')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', closeMenu, true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('click', closeMenu, true)
|
||||
})
|
||||
// watchEffect(() => {
|
||||
// if (width.value === maxWidth) {
|
||||
// Mitt.emit('shrinkWindow', false)
|
||||
|
21
src/styles/scss/chat-sidebar.scss
Normal file
21
src/styles/scss/chat-sidebar.scss
Normal file
@ -0,0 +1,21 @@
|
||||
.item-box {
|
||||
@apply flex flex-col w-180px h-100vh z-20 p-[12px_0_12px_6px] box-border select-none text-[--text-color];
|
||||
.item {
|
||||
height: 42px;
|
||||
padding-left: 4px;
|
||||
width: 95%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--bg-group-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*! 修改naive-ui虚拟列表滚动条的宽度 */
|
||||
:deep(
|
||||
.n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--vertical,
|
||||
.n-scrollbar + .n-scrollbar-rail.n-scrollbar-rail--vertical
|
||||
) {
|
||||
width: 6px;
|
||||
}
|
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@ -46,7 +46,6 @@ declare module 'vue' {
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
@ -56,6 +55,5 @@ declare module 'vue' {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Text: typeof import('./../components/rightBox/renderMessage/Text.vue')['default']
|
||||
UserSidebar: typeof import('./../components/rightBox/chatBox/UserSidebar.vue')['default']
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { statusItem } from './home-window/onlineStatus/config.ts'
|
||||
import { onlineStatus } from '@/stores/onlineStatus.ts'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
const { checkWinExist, createWebviewWindow } = useWindow()
|
||||
const OLStatusStore = onlineStatus()
|
||||
@ -65,6 +66,15 @@ const exit = async () => {
|
||||
console.error('退出失败:', error)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 暂停图标闪烁
|
||||
listen('stop', async () => {
|
||||
await invoke('tray_blink', { isRun: false }).catch((error) => {
|
||||
console.error('暂停闪烁失败:', error)
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -15,6 +15,7 @@ appWindow.onCloseRequested(async (e) => {
|
||||
await emit(EventEnum.WIN_CLOSE, e.windowLabel)
|
||||
})
|
||||
|
||||
/*! 创建新窗口然后需要通信传递数据时候需要进行提交一次页面创建成功的事件,否则会接收不到数据 */
|
||||
onMounted(async () => {
|
||||
await emit(EventEnum.ALONE)
|
||||
})
|
||||
|
@ -1,132 +0,0 @@
|
||||
import { MockList } from '@/mock'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { MockItem } from '@/services/types.ts'
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
import { EventEnum, MittEnum } from '@/enums'
|
||||
import { WebviewWindow } from '@tauri-apps/api/window'
|
||||
import { delay } from 'lodash-es'
|
||||
|
||||
const { createWebviewWindow, checkWinExist } = useWindow()
|
||||
/* 建议把此状态存入localStorage中 */
|
||||
const activeItem = ref(-1)
|
||||
const msgBoxShow = ref(false)
|
||||
/* 独立窗口的集合 */
|
||||
const aloneWin = ref(new Set())
|
||||
const shrinkStatus = ref(false)
|
||||
|
||||
/* 监听独立窗口关闭事件 */
|
||||
watchEffect(async () => {
|
||||
await listen(EventEnum.WIN_CLOSE, (e) => {
|
||||
aloneWin.value.delete(e.payload)
|
||||
})
|
||||
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) => {
|
||||
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)
|
||||
await listen(EventEnum.ALONE, () => {
|
||||
emit(EventEnum.ALONE + item.key, item)
|
||||
aloneWin.value.add(EventEnum.ALONE + item.key)
|
||||
})
|
||||
}
|
||||
|
||||
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' }
|
||||
])
|
||||
|
||||
export { activeItem, msgBoxShow, handleMsgClick, handleMsgDblclick, menuList, specialMenuList }
|
@ -45,10 +45,11 @@
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { MockList } from '@/mock'
|
||||
import { activeItem, handleMsgClick, menuList, specialMenuList, handleMsgDblclick } from './config.ts'
|
||||
import { useMessage } from '@/hooks/useMessage.ts'
|
||||
import { MittEnum } from '@/enums'
|
||||
|
||||
const msgTotal = ref(0)
|
||||
const { activeItem, handleMsgClick, menuList, specialMenuList, handleMsgDblclick } = useMessage()
|
||||
|
||||
watchEffect(() => {
|
||||
Mitt.emit(MittEnum.UPDATE_MSG_TOTAL, msgTotal.value)
|
||||
|
Loading…
Reference in New Issue
Block a user