perf(component): 优化一些功能的操作体验和样式

This commit is contained in:
Dawn
2025-03-01 22:48:15 +08:00
parent 801ec4ee00
commit 31c1709f63
14 changed files with 170 additions and 57 deletions

View File

@@ -1,7 +1,7 @@
# 后端服务地址
# VITE_SERVICE_URL="https://hulaspark.com/api"
VITE_SERVICE_URL="https://hulaspark.com/api"
# # websocket服务地址
# VITE_WEBSOCKET_URL="wss://hulaspark.com/websocket"
VITE_WEBSOCKET_URL="wss://hulaspark.com/websocket"
# 项目标题
VITE_APP_TITLE="HuLa—IM"
# 项目名称
@@ -10,5 +10,5 @@ VITE_APP_NAME="HuLa"
VITE_GITEE_TOKEN="a9029798336825cea39ac9e4413b8579"
# 启用本地的服务地址,先要注释掉上面的服务地址
VITE_SERVICE_URL="http://127.0.0.1:9190"
VITE_WEBSOCKET_URL="ws://127.0.0.1:8090/websocket"
# VITE_SERVICE_URL="http://127.0.0.1:9190"
# VITE_WEBSOCKET_URL="ws://127.0.0.1:8090/websocket"

View File

@@ -160,7 +160,11 @@ const finishLoading = () => {
loadingText.value = '确定'
}
defineExpose({
// 定义组件实例类型
export interface AvatarCropperInstance {
finishLoading: () => void
}
defineExpose<AvatarCropperInstance>({
finishLoading
})

View File

@@ -319,7 +319,7 @@ defineExpose<VirtualListExpose>({
} else if (options.position === 'top') {
// 滚动到顶部
containerRef.value.scrollTop = 0
} else if (typeof options.index === 'string') {
} else if (typeof options.index === 'number') {
// 滚动到指定索引位置
const offset = getOffsetForIndex(options.index)
containerRef.value.scrollTo({

View File

@@ -96,7 +96,7 @@
</div>
<!-- 消息为系统消息时 -->
<div v-if="item.message.type === MsgEnum.SYSTEM">
<div v-else-if="item.message.type === MsgEnum.SYSTEM">
<p class="text-(12px #909090) select-none cursor-default">
{{ item.message.body }}
</p>
@@ -318,7 +318,7 @@
:size="6"
v-if="item.message.body.reply"
@click="jumpToReplyMsg(item.message.body.reply.id)"
class="reply-bubble relative w-fit custom-shadow">
class="reply-bubble relative max-w-86% w-fit custom-shadow">
<svg class="size-14px">
<use href="#to-top"></use>
</svg>

View File

@@ -287,7 +287,9 @@ export enum ModalEnum {
/** 锁屏弹窗 */
LOCK_SCREEN,
/** 检查更新弹窗 */
CHECK_UPDATE
CHECK_UPDATE,
/** 异地登录弹窗 */
REMOTE_LOGIN
}
/** MacOS键盘映射 */

View File

@@ -27,8 +27,8 @@
<script setup lang="ts">
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
import { useMitt } from '@/hooks/useMitt.ts'
import { ChangeTypeEnum, MittEnum, OnlineEnum, RoomTypeEnum } from '@/enums'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { ChangeTypeEnum, MittEnum, ModalEnum, OnlineEnum, RoomTypeEnum } from '@/enums'
import { getCurrentWebviewWindow, WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useGlobalStore } from '@/stores/global.ts'
import { useContactStore } from '@/stores/contacts.ts'
import { useGroupStore } from '@/stores/group'
@@ -36,17 +36,13 @@ import { useUserStore } from '@/stores/user'
import { useChatStore } from '@/stores/chat'
import { LoginSuccessResType, OnStatusChangeType, WsResponseMessageType, WsTokenExpire } from '@/services/wsType.ts'
import type { MarkItemType, MessageType, RevokedMsgType } from '@/services/types.ts'
import { useLogin } from '@/hooks/useLogin.ts'
import { computedToken } from '@/services/request'
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification'
import { useUserInfo } from '@/hooks/useCached.ts'
import { emitTo } from '@tauri-apps/api/event'
import { useThrottleFn } from '@vueuse/core'
import apis from '@/services/apis.ts'
import { confirm } from '@tauri-apps/plugin-dialog'
import { useCachedStore } from '@/stores/cached'
import { clearListener, initListener, readCountQueue } from '@/utils/ReadCountQueue'
import { useSettingStore } from '@/stores/setting'
const loadingPercentage = ref(10)
const loadingText = ref('正在加载应用...')
@@ -93,9 +89,6 @@ const groupStore = useGroupStore()
const userStore = useUserStore()
const chatStore = useChatStore()
const cachedStore = useCachedStore()
const { logout, resetLoginState } = useLogin()
const settingStore = useSettingStore()
const { login } = storeToRefs(settingStore)
// 清空未读消息
// globalStore.unReadMark.newMsgUnreadCount = 0
const shrinkStatus = ref(false)
@@ -121,13 +114,13 @@ useMitt.on(MittEnum.SHRINK_WINDOW, (event: boolean) => {
shrinkStatus.value = event
})
useMitt.on(WsResponseMessageType.LOGIN_SUCCESS, (data: LoginSuccessResType) => {
useMitt.on(WsResponseMessageType.LOGIN_SUCCESS, async (data: LoginSuccessResType) => {
const { ...rest } = data
// 更新一下请求里面的 token.
computedToken.clear()
computedToken.get()
// 自己更新自己上线
groupStore.batchUpdateUserStatus([
await groupStore.batchUpdateUserStatus([
{
activeStatus: OnlineEnum.ONLINE,
avatar: rest.avatar,
@@ -148,18 +141,24 @@ useMitt.on(WsResponseMessageType.ONLINE, async (onStatusChangeType: OnStatusChan
console.log('收到用户上线通知')
groupStore.countInfo.onlineNum = onStatusChangeType.onlineNum
// groupStore.countInfo.totalNum = onStatusChangeType.totalNum
groupStore.batchUpdateUserStatus(onStatusChangeType.changeList)
await groupStore.batchUpdateUserStatus(onStatusChangeType.changeList)
await groupStore.refreshGroupMembers()
})
useMitt.on(WsResponseMessageType.TOKEN_EXPIRED, async (wsTokenExpire: WsTokenExpire) => {
console.log('账号在其他设备登录', wsTokenExpire)
if (userStore.userInfo.uid === wsTokenExpire.uid && userStore.userInfo.client === wsTokenExpire.client) {
// TODO: 换成web的弹出框
await confirm('账号在其他设备' + (wsTokenExpire.ip ? wsTokenExpire.ip : '未知IP') + '登录')
// token已在后端清空只需要返回登录页
await apis.logout(login.value.autoLogin)
await resetLoginState()
await logout()
if (
Number(userStore.userInfo.uid) === Number(wsTokenExpire.uid) &&
userStore.userInfo.client === wsTokenExpire.client
) {
// 聚焦主窗口
const home = await WebviewWindow.getByLabel('home')
await home?.setFocus()
console.log('账号在其他设备登录', wsTokenExpire)
useMitt.emit(MittEnum.LEFT_MODAL_SHOW, {
type: ModalEnum.REMOTE_LOGIN,
props: {
ip: wsTokenExpire.ip
}
})
}
})
useMitt.on(WsResponseMessageType.INVALID_USER, (param: { uid: string }) => {

View File

@@ -149,6 +149,7 @@ import { formatTimestamp, isDiffNow } from '@/utils/ComputedTime.ts'
import dayjs from 'dayjs'
import { useTauriListener } from '@/hooks/useTauriListener'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import type { AvatarCropperInstance } from '@/components/common/AvatarCropper.vue'
const appWindow = WebviewWindow.getCurrent()
let localUserInfo = ref<Partial<UserInfoType>>({})
@@ -161,7 +162,7 @@ const { countGraphemes } = useCommon()
const showCropper = ref(false)
const fileInput = ref<HTMLInputElement>()
const localImageUrl = ref('')
const cropperRef = useTemplateRef('cropperRef')
const cropperRef = useTemplateRef<AvatarCropperInstance>('cropperRef')
// 监听裁剪窗口的关闭
watch(

View File

@@ -64,14 +64,18 @@ const moreList = ref<OPT.L.MoreList[]>([
label: '检查更新',
icon: 'arrow-circle-up',
click: () => {
useMitt.emit(MittEnum.LEFT_MODAL_SHOW, ModalEnum.CHECK_UPDATE)
useMitt.emit(MittEnum.LEFT_MODAL_SHOW, {
type: ModalEnum.CHECK_UPDATE
})
}
},
{
label: '锁定屏幕',
icon: 'lock',
click: () => {
useMitt.emit(MittEnum.LEFT_MODAL_SHOW, ModalEnum.LOCK_SCREEN)
useMitt.emit(MittEnum.LEFT_MODAL_SHOW, {
type: ModalEnum.LOCK_SCREEN
})
}
},
{

View File

@@ -11,7 +11,7 @@
<InfoEdit />
<!-- 弹出框 -->
<component :is="componentMap" />
<component :is="componentMap" v-bind="componentProps" />
</main>
</div>
</template>
@@ -20,22 +20,27 @@ import LeftAvatar from './components/LeftAvatar.vue'
import ActionList from './components/ActionList.vue'
import InfoEdit from './components/InfoEdit.vue'
import { useMitt } from '@/hooks/useMitt.ts'
import { lock, LockScreen, CheckUpdate } from './model.tsx'
import { modalShow, LockScreen, CheckUpdate, RemoteLogin } from './model.tsx'
import type { Component } from 'vue'
import { MittEnum, ModalEnum } from '@/enums'
const componentMap = shallowRef<Component>()
// 存储要传递给组件的props
const componentProps = shallowRef<Record<string, any>>({})
/** 弹窗组件内容映射 */
const componentMapping: Record<number, Component> = {
[ModalEnum.LOCK_SCREEN]: LockScreen,
[ModalEnum.CHECK_UPDATE]: CheckUpdate
[ModalEnum.CHECK_UPDATE]: CheckUpdate,
[ModalEnum.REMOTE_LOGIN]: RemoteLogin
}
onMounted(() => {
useMitt.on(MittEnum.LEFT_MODAL_SHOW, (event) => {
componentMap.value = componentMapping[event as ModalEnum]
useMitt.on(MittEnum.LEFT_MODAL_SHOW, (event: { type: ModalEnum; props?: Record<string, any> }) => {
componentMap.value = componentMapping[event.type]
// 保存传入的props
componentProps.value = event.props || {}
nextTick(() => {
lock.value.modalShow = true
modalShow.value = true
})
})
})

View File

@@ -26,13 +26,30 @@ import { useUserStore } from '@/stores/user.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { AvatarUtils } from '@/utils/AvatarUtils'
import { confirm } from '@tauri-apps/plugin-dialog'
import apis from '@/services/apis'
import { useLogin } from '@/hooks/useLogin'
const { logout, resetLoginState } = useLogin()
const formRef = ref<FormInst | null>()
const formValue = ref({
lockPassword: ''
})
export const modalShow = ref(false)
export const remotelogin = ref({
loading: false,
async logout() {
remotelogin.value.loading = true
const settingStore = useSettingStore()
const { login } = storeToRefs(settingStore)
// token已在后端清空只需要返回登录页
await apis.logout(login.value.autoLogin)
await resetLoginState()
await logout()
modalShow.value = false
remotelogin.value.loading = false
}
})
export const lock = ref({
modalShow: false,
loading: false,
rules: {
lockPassword: {
@@ -53,7 +70,7 @@ export const lock = ref({
/** 发送锁屏事件,当打开的窗口接受到后会自动锁屏 */
await emit(EventEnum.LOCK_SCREEN)
lock.value.loading = false
lock.value.modalShow = false
modalShow.value = false
formValue.value.lockPassword = ''
}, 1000)
})
@@ -66,18 +83,18 @@ export const lock = ref({
export const LockScreen = defineComponent(() => {
const userStore = useUserStore()
return () => (
<NModal v-model:show={lock.value.modalShow} maskClosable={false} class="w-350px border-rd-8px">
<NModal v-model:show={modalShow.value} maskClosable={false} class="w-350px border-rd-8px">
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
{type() === 'macos' ? (
<div
onClick={() => (lock.value.modalShow = false)}
onClick={() => (modalShow.value = false)}
class="mac-close relative size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
) : (
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<svg onClick={() => (modalShow.value = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
)}
@@ -285,18 +302,18 @@ export const CheckUpdate = defineComponent(() => {
await checkUpdate()
})
return () => (
<NModal v-model:show={lock.value.modalShow} maskClosable={false} class="w-350px border-rd-8px">
<NModal v-model:show={modalShow.value} maskClosable={false} class="w-350px border-rd-8px">
<div class="bg-[--bg-popover] w-500px h-full p-6px box-border flex flex-col">
{type() === 'macos' ? (
<div
onClick={() => (lock.value.modalShow = false)}
onClick={() => (modalShow.value = false)}
class="mac-close relative size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
) : (
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<svg onClick={() => (modalShow.value = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
)}
@@ -421,3 +438,68 @@ export const CheckUpdate = defineComponent(() => {
</NModal>
)
})
/**
* 异地登录弹窗
*/
export const RemoteLogin = defineComponent({
props: {
ip: {
type: String,
default: '未知IP'
}
},
setup(props) {
const userStore = useUserStore()
return () => (
<NModal
v-model:show={modalShow.value}
maskClosable={false}
class="w-350px border-rd-8px select-none cursor-default">
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
{type() === 'macos' ? (
<div
onClick={remotelogin.value.logout}
class="mac-close relative size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
) : (
<svg onClick={remotelogin.value.logout} class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
)}
<div class="flex flex-col gap-10px p-10px select-none">
<NFlex vertical align="center" size={30}>
<span class="text-(14px [--text-color])">线</span>
<div class="relative">
<img class="rounded-full size-72px" src={AvatarUtils.getAvatarUrl(userStore.userInfo.avatar!)} />
<div class="absolute inset-0 bg-[--avatar-hover-bg] backdrop-blur-[2px] rounded-full flex items-center justify-center">
<svg class="size-34px text-white animate-pulse">
<use href="#cloudError"></use>
</svg>
</div>
</div>
<div class="text-(13px centent [--text-color]) px-12px leading-loose mb-20px">
<span class="text-#13987f">{props.ip}</span>{' '}
</div>
</NFlex>
<NButton
disabled={remotelogin.value.loading}
loading={remotelogin.value.loading}
onClick={remotelogin.value.logout}
style={{ color: '#fff' }}
class="w-full"
color="#13987f">
</NButton>
</div>
</div>
</NModal>
)
}
})

View File

@@ -4,21 +4,21 @@ import type { UserInfoType, UserItem } from '@/services/types.ts'
export enum WsResponseMessageType {
/** 无网络连接 */
NO_INTERNET = 'noInternet',
/** 1.登录返回二维码 */
/** 登录返回二维码 */
LOGIN_QR_CODE = 'loginQrCode',
/** 2.用户扫描成功等待授权 */
/** 用户扫描成功等待授权 */
WAITING_AUTHORIZE = 'waitingAuthorize',
/** 3.用户登录成功返回用户信息 */
/** 用户登录成功返回用户信息 */
LOGIN_SUCCESS = 'loginSuccess',
/** 4.收到消息 */
/** 收到消息 */
RECEIVE_MESSAGE = 'receiveMessage',
/** 5.上线推送 */
/** 上线推送 */
ONLINE = 'online',
/** 6.前端token失效 */
/** 前端token失效 */
TOKEN_EXPIRED = 'tokenExpired',
/** 7.禁用的用户 */
/** 禁用的用户 */
INVALID_USER = 'invalidUser',
/** 8.点赞、倒赞更新通知 */
/** 点赞、倒赞更新通知 */
MSG_MARK_ITEM = 'msgMarkItem',
/** 消息撤回 */
MSG_RECALL = 'msgRecall',
@@ -31,7 +31,19 @@ export enum WsResponseMessageType {
/** 同意好友请求 */
REQUEST_APPROVAL_FRIEND = 'requestApprovalFriend',
/** 用户状态改变 */
USER_STATE_CHANGE = 'userStateChange'
USER_STATE_CHANGE = 'userStateChange',
/** 管理员修改群聊信息 */
ROOM_INFO_CHANGE = 'roomInfoChange',
/** 自己修改我在群里的信息 */
MY_ROOM_INFO_CHANGE = 'myRoomInfoChange',
/** 群通知消息 */
ROOM_GROUP_MSG = 'roomGroupMsg',
/** 群公告消息 */
ROOM_GROUP_NOTICE_MSG = 'roomGroupNoticeMsg',
/** 群公告已读 */
ROOM_GROUP_NOTICE_READ_MSG = 'roomGroupNoticeReadMsg',
/** 群解散 */
ROOM_DISSOLUTION = 'roomDissolution'
}
/**

View File

@@ -149,7 +149,7 @@ export const useGroupStore = defineStore('group', () => {
* 批量更新用户在线状态
* @param items 需要更新状态的用户列表
*/
const batchUpdateUserStatus = (items: UserItem[]) => {
const batchUpdateUserStatus = async (items: UserItem[]) => {
for (const curUser of items) {
const findIndex = userList.value.findIndex((item) => item.uid === curUser.uid)
userList.value[findIndex] = {

View File

@@ -103,6 +103,8 @@ svg {
--danger-bg: #f6dfe3;
// 头像边框颜色
--avatar-border-color: #fff;
// hover头像样式
--avatar-hover-bg: rgba(255, 255, 255, 0.1);
// 插件背景颜色
--plugin-bg-color: #eee;
// 列表悬浮的颜色
@@ -193,6 +195,8 @@ html[data-theme='dark'] {
--danger-bg: #37292c;
// 头像边框颜色
--avatar-border-color: #1b1b1b;
// hover头像样式
--avatar-hover-bg: rgba(30, 30, 30, 0.6);
// 插件背景颜色
--plugin-bg-color: #3b3b3b;
// 列表悬浮的颜色

View File

@@ -160,7 +160,7 @@ onMounted(() => {
// 获取用户详情
userStore.getUserDetailAction()
// 自己更新自己上线
groupStore.batchUpdateUserStatus([
await groupStore.batchUpdateUserStatus([
{
activeStatus: OnlineEnum.ONLINE,
avatar: rest.avatar,