fix(component): 🐛修复一些已知问题 (#47)

This commit is contained in:
Dawn 2024-11-20 00:36:44 +08:00 committed by GitHub
commit 0e0955f3ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 3867 additions and 2168 deletions

View File

@ -1,5 +1,5 @@
# 后端服务地址
VITE_SERVICE_URL="https://hulaspark.com"
VITE_SERVICE_URL="https://hulaspark.com/api"
# websocket服务地址
VITE_WEBSOCKET_URL="wss://hulaspark.com/websocket"
# 项目标题

View File

@ -1,5 +1,5 @@
# 后端服务地址
VITE_SERVICE_URL="https://hulaspark.com"
VITE_SERVICE_URL="https://hulaspark.com/api/"
# websocket服务地址
VITE_WEBSOCKET_URL="wss://hulaspark.com/websocket"
# 项目标题

2
.gitignore vendored
View File

@ -13,8 +13,6 @@ dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.DS_Store
*.suo
*.ntvs*

View File

@ -7,5 +7,6 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="ts-external-references" level="project" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{ts-external-references}" />
</component>
</project>

View File

@ -0,0 +1,14 @@
<component name="libraryTable">
<library name="ts-external-references" type="javaScript">
<properties>
<sourceFilesUrls>
<item url="file://$PROJECT_DIR$/node_modules/.pnpm/@rspack+core@1.1.0_@swc+helpers@0.5.15/node_modules/@rspack/core/module.d.ts" />
</sourceFilesUrls>
</properties>
<CLASSES>
<root url="file://$PROJECT_DIR$/node_modules/.pnpm/@rspack+core@1.1.0_@swc+helpers@0.5.15/node_modules/@rspack/core/module.d.ts" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

18
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"configurations": [
{
"type": "node-terminal",
"name": "tauri:dev",
"request": "launch",
"command": "pnpm run tauri:dev",
"cwd": "${workspaceFolder}"
},
{
"type": "node-terminal",
"name": "tauri:build",
"request": "launch",
"command": "pnpm run tauri:build",
"cwd": "${workspaceFolder}"
}
]
}

13
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "tauri:build:debug",
"group": "build",
"problemMatcher": [],
"label": "npm: tauri:build:debug",
"detail": "tauri build --debug"
}
]
}

View File

@ -1,19 +1,15 @@
## [2.5.3](https://github.com/HuLaSpark/HuLa/compare/v2.5.2...v2.5.3) (2024-11-06)
### 🐛 Bug Fixes | Bug 修复
* **component:** :bug: 修复输入框换行不兼容webkit的问题 ([345d830](https://github.com/HuLaSpark/HuLa/commit/345d83068711df087dd0ba403446c739151a11dd))
* **layout:** :bug: 修复聊天框改变宽度的时候可以选中文本的问题 ([56d79cc](https://github.com/HuLaSpark/HuLa/commit/56d79ccc8ba015a313eabcd938757f35d1d840a4))
* **layout:** :bug: 修复选择了图片不显示在输入框中的bug ([c7cdac6](https://github.com/HuLaSpark/HuLa/commit/c7cdac69ce6fa185489dcb480991e3a268fec99d))
* **service:** :bug: 修复请求接口bug ([f3723d4](https://github.com/HuLaSpark/HuLa/commit/f3723d4e5a2342314ce6e85931a49f1ddfecab0b))
- **component:** :bug: 修复输入框换行不兼容webkit的问题 ([345d830](https://github.com/HuLaSpark/HuLa/commit/345d83068711df087dd0ba403446c739151a11dd))
- **layout:** :bug: 修复聊天框改变宽度的时候可以选中文本的问题 ([56d79cc](https://github.com/HuLaSpark/HuLa/commit/56d79ccc8ba015a313eabcd938757f35d1d840a4))
- **layout:** :bug: 修复选择了图片不显示在输入框中的bug ([c7cdac6](https://github.com/HuLaSpark/HuLa/commit/c7cdac69ce6fa185489dcb480991e3a268fec99d))
- **service:** :bug: 修复请求接口bug ([f3723d4](https://github.com/HuLaSpark/HuLa/commit/f3723d4e5a2342314ce6e85931a49f1ddfecab0b))
### ⚡️ Performance Improvements | 性能优化
* **component:** :zap: 优化右键菜单功能 ([7b53029](https://github.com/HuLaSpark/HuLa/commit/7b530297ac37122ead00a15864e16a73a5547d04))
- **component:** :zap: 优化右键菜单功能 ([7b53029](https://github.com/HuLaSpark/HuLa/commit/7b530297ac37122ead00a15864e16a73a5547d04))
## [2.5.2](https://github.com/HuLaSpark/HuLa/compare/v2.5.1...v2.5.2) (2024-10-31)

View File

@ -5,6 +5,7 @@
<p align="center">An Instant Messaging System Built with Tauri, Vite 5, Vue 3, and TypeScript</p>
<div align="center">
<a href="https://www.bestpractices.dev/zh-CN/projects/9692"><img src="https://bestpractices.coreinfrastructure.org/projects/9692/badge" alt="CI"></a>
<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">

View File

@ -5,6 +5,7 @@
<p align="center">一个基于Tauri、Vite 5、Vue 3 和 TypeScript 构建的即时通讯系统</p>
<div align="center">
<a href="https://www.bestpractices.dev/zh-CN/projects/9692"><img src="https://bestpractices.coreinfrastructure.org/projects/9692/badge" alt="CI"></a>
<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">

View File

@ -69,11 +69,11 @@
"@types/node": "^20.14.14",
"@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "^7.15.0",
"@unocss/preset-uno": "^0.62.3",
"@unocss/reset": "^0.62.3",
"@unocss/transformer-directives": "^0.62.3",
"@unocss/transformer-variant-group": "^0.62.3",
"@unocss/vite": "^0.62.3",
"@unocss/preset-uno": "^0.64.0",
"@unocss/reset": "^0.64.0",
"@unocss/transformer-directives": "^0.64.0",
"@unocss/transformer-variant-group": "^0.64.0",
"@unocss/vite": "^0.64.0",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"@vueuse/core": "^10.11.0",
@ -89,7 +89,7 @@
"oxlint": "^0.2.18",
"prettier": "^3.3.2",
"release-it": "^17.10.0",
"sass": "1.77.6",
"sass": "1.80.6",
"typescript": "^5.6.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4",

File diff suppressed because it is too large Load Diff

View File

@ -57,11 +57,11 @@
import { useGlobalStore } from '@/stores/global.ts'
import { type } from '@tauri-apps/plugin-os'
import { useUserInfo } from '@/hooks/useCached.ts'
import { leftHook } from '@/layout/left/hook.ts'
import apis from '@/services/apis.ts'
import { useCommon } from '@/hooks/useCommon.ts'
const globalStore = useGlobalStore()
const { countGraphemes } = leftHook()
const { countGraphemes } = useCommon()
const userInfo = ref(useUserInfo(globalStore.addFriendModalInfo.uid).value)
const requestMsg = ref()

View File

@ -167,6 +167,7 @@ const handleAfterEnter = (el: any) => {
</script>
<style scoped lang="scss">
@use '@/styles/scss/global/variable.scss' as *;
@mixin menu-item {
padding: 2px 8px;
border-radius: 4px;

View File

@ -4,13 +4,24 @@
<n-flex vertical :size="20" class="size-full p-10px box-border z-10">
<n-flex vertical :size="20" align="center">
<n-avatar
v-if="isCurrentUser.avatar"
:bordered="true"
round
:color="'#fff'"
:size="80"
:src="isCurrentUser.avatar"
fallback-src="/logo.png"></n-avatar>
<n-avatar
v-else
:bordered="true"
round
:color="'#909090'"
:size="80"
:src="isCurrentUser.avatar"
fallback-src="/logo.png">
{{ isCurrentUser.name }}
</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>

View File

@ -1,13 +1,15 @@
<template>
<!-- 好友详情 -->
<n-flex v-if="content.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="" />
<n-avatar v-if="item.avatar" class="rounded-50% size-146px border-(2px solid #fff)" :src="item.avatar" />
<n-avatar
v-else
:color="'#909090'"
class="rounded-50% size-146px text-28px border-(2px solid #fff)"
:src="item.avatar">
{{ item.name!.slice(0, 1) }}
</n-avatar>
<span class="text-(20px [--text-color])">{{ item.name }}</span>

View File

@ -56,7 +56,7 @@
</n-virtual-list>
</div>
<!-- 发送按钮 TODO 建议不要放在外面会影响视觉效果可以放在发送按钮里面做提示发送按钮需要修改一下大小 (nyh -> 2024-09-01 23:41:34) -->
<!-- 发送按钮 -->
<n-flex align="center" justify="space-between" :size="12">
<n-config-provider :theme="lightTheme">
<n-button-group size="small" class="pr-20px">
@ -119,7 +119,7 @@ import Mitt from '@/utils/Bus.ts'
import { CacheUserItem, MockItem } from '@/services/types.ts'
import { emit, listen } from '@tauri-apps/api/event'
import { useSettingStore } from '@/stores/setting.ts'
import { sendOptions } from '@/views/homeWindow/more/settings/config.ts'
import { sendOptions } from '@/views/moreWindow/settings/config.ts'
import { useMsgInput } from '@/hooks/useMsgInput.ts'
import { useCommon } from '@/hooks/useCommon.ts'
import { onKeyStroke } from '@vueuse/core'

View File

@ -85,16 +85,6 @@
<div class="pl-20px flex flex-col items-end gap-6px">
<MsgInput ref="MsgInputRef" />
</div>
<div v-if="isGuest" class="fuzzy">
<n-flex align="center" :size="0" class="pb-60px text-(14px [--text-color])">
<p>当前为</p>
<p class="color-#c14053 px-2px">游客模式</p>
<p></p>
<p @click="logout(true)" class="color-#13987f px-4px cursor-pointer">扫码登录</p>
<p>后使用</p>
</n-flex>
</div>
</main>
</template>
@ -104,20 +94,12 @@ import { LimitEnum, MsgEnum } from '@/enums'
import { useCommon } from '@/hooks/useCommon.ts'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { emit } from '@tauri-apps/api/event'
import { useLogin } from '@/hooks/useLogin.ts'
import { useSettingStore } from '@/stores/setting.ts'
const { logout } = useLogin()
const { open, onChange, reset } = useFileDialog()
const { login } = useSettingStore()
const MsgInputRef = ref()
const msgInputDom = ref()
const emojiShow = ref()
const { insertNode, triggerInputEvent, getEditorRange, imgPaste, FileOrVideoPaste } = useCommon()
/**
* 是否为游客模式
*/
const isGuest = computed(() => login.accountInfo.token === 'test')
/**
* 选择表情并把表情插入输入框
@ -185,13 +167,6 @@ onMounted(() => {
}
}
.fuzzy {
@apply bg-transparent select-none cursor-default size-full absolute-flex-center;
overflow: hidden;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
:deep(.n-input .n-input-wrapper) {
padding: 0;
}

View File

@ -94,18 +94,28 @@
:content="item"
:menu="chatStore.isGroup ? optionsList : void 0"
:special-menu="report">
<!-- 没有头像时候显示 -->
<n-avatar
round
:color="'#fff'"
v-if="avatarExists(item.fromUser.uid)"
:size="34"
:color="'#909090'"
@click="selectKey = item.message.id"
class="select-none"
:src="getAvatarSrc(item.fromUser.uid)"
:class="item.fromUser.uid === userUid ? '' : 'mr-10px'">
{{ avatarExists(item.fromUser.uid) }}
</n-avatar>
<!-- 存在头像时候显示 -->
<n-avatar
round
v-else
:size="34"
@click="selectKey = item.message.id"
class="select-none"
:src="
item.fromUser.uid === userUid
? login.accountInfo.avatar
: useUserInfo(item.fromUser.uid).value.avatar
"
:class="item.fromUser.uid === userUid ? '' : 'mr-10px'"></n-avatar>
:src="getAvatarSrc(item.fromUser.uid)"
:class="item.fromUser.uid === userUid ? '' : 'mr-10px'">
</n-avatar>
</ContextMenu>
</template>
<!-- 用户个人信息框 -->
@ -430,6 +440,16 @@ watch(chatMessageList, (value, oldValue) => {
}
})
/** 获取用户头像 */
const getAvatarSrc = (uid: number) => {
return uid === userUid.value ? login.value.accountInfo.avatar : useUserInfo(uid).value.avatar
}
/** 头像是否存在 */
const avatarExists = (uid: number) => {
return getAvatarSrc(uid) ? void 0 : useUserInfo(uid).value.name?.slice(0, 1)
}
//
const handleMouseEnter = (key: any) => {
// 1600key

View File

@ -19,7 +19,7 @@
</div>
<n-flex v-if="!isSearch" align="center" justify="space-between" class="pr-8px pl-8px h-42px">
<span class="text-14px">群聊成员&nbsp;{{ userList.length }}</span>
<span class="text-14px">在线群聊成员&nbsp;{{ groupStore.countInfo.onlineNum }}</span>
<svg @click="handleSelect" class="size-14px"><use href="#search"></use></svg>
</n-flex>
<!-- 搜索框 -->
@ -47,6 +47,7 @@
id="image-chat-sidebar"
style="max-height: calc(100vh - 130px)"
item-resizable
@scroll="handleScroll($event)"
:item-size="42"
:items="filteredUserList">
<template #default="{ item }">
@ -65,6 +66,7 @@
:special-menu="report">
<n-flex @click="selectKey = item.uid" :key="item.uid" :size="10" align="center" class="item">
<n-avatar
v-if="item.avatar"
lazy
round
class="grayscale"
@ -77,6 +79,23 @@
:intersection-observer-options="{
root: '#image-chat-sidebar'
}"></n-avatar>
<n-avatar
v-else
lazy
round
class="grayscale text-10px"
:class="{ 'grayscale-0': item.activeStatus === OnlineEnum.ONLINE }"
:color="'#909090'"
:size="24"
:src="item.avatar"
fallback-src="/logo.png"
:render-placeholder="() => null"
:intersection-observer-options="{
root: '#image-chat-sidebar'
}">
{{ item.name?.slice(0, 1) }}
</n-avatar>
<span class="text-12px truncate flex-1">{{ item.name }}</span>
<div v-if="item.uid === 1" class="flex p-4px rounded-4px bg-#f5dadf size-fit select-none">
<span class="text-(10px #d5304f)">群主</span>
@ -131,6 +150,15 @@ const isCollapsed = ref(true)
const { optionsList, report, selectKey } = useChatMain()
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-sidebar')
watch(userList, (newVal) => {
//
if (searchRef.value) {
filteredUserList.value = newVal.filter((user) => user.name.toLowerCase().includes(searchRef.value.toLowerCase()))
} else {
filteredUserList.value = newVal
}
})
const handleSelect = () => {
isSearch.value = !isSearch.value
nextTick(() => {
@ -142,6 +170,8 @@ const handleSelect = () => {
* 重置搜索状态
*/
const handleBlur = () => {
//
if (searchRef.value) return
isSearch.value = false
searchRef.value = ''
filteredUserList.value = userList.value
@ -155,6 +185,19 @@ const handleSearch = useDebounceFn((value: string) => {
filteredUserList.value = userList.value.filter((user) => user.name.toLowerCase().includes(value.toLowerCase()))
}, 10)
/**
* 处理滚动事件
* @param event 滚动事件
*/
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement
const isBottom = target.scrollHeight - target.scrollTop === target.clientHeight
if (isBottom && !groupStore.userListOptions.loading) {
groupStore.loadMoreGroupMembers()
}
}
onMounted(() => {
Mitt.on(`${MittEnum.INFO_POPOVER}-Sidebar`, (event: any) => {
selectKey.value = event.uid

View File

@ -4,6 +4,10 @@
data-tauri-drag-region
:class="osType === 'windows' ? 'flex justify-end select-none' : 'h-24px select-none w-full'">
<template v-if="osType === 'windows'">
<!-- 登录窗口的代理按钮 -->
<div v-if="proxy" @click="router.push('/proxy')" class="w-30px h-24px flex-center">
<svg class="size-16px color-[--action-bar-icon-color] cursor-pointer"><use href="#settings"></use></svg>
</div>
<!-- 固定在最顶层 -->
<div v-if="topWinLabel !== void 0" @click="handleAlwaysOnTop" class="hover-box">
<n-popover trigger="hover">
@ -86,10 +90,12 @@ import { emit, listen } from '@tauri-apps/api/event'
import { CloseBxEnum, EventEnum, MittEnum } from '@/enums'
import { exit } from '@tauri-apps/plugin-process'
import { type } from '@tauri-apps/plugin-os'
import router from '@/router'
const appWindow = WebviewWindow.getCurrent()
const {
topWinLabel,
proxy = false,
minW = true,
maxW = true,
closeW = true,
@ -103,6 +109,7 @@ const {
topWinLabel?: string
currentLabel?: string
shrinkStatus?: boolean
proxy?: boolean
}>()
const { getWindowTop, setWindowTop } = useAlwaysOnTopStore()
const settingStore = useSettingStore()
@ -239,10 +246,10 @@ onUnmounted(() => {
<style scoped lang="scss">
.hover-box {
@apply w-28px h24px flex-center hover:bg-[--icon-hover-color];
@apply w-28px h-24px flex-center hover:bg-[--icon-hover-color];
}
.action-close {
@apply w-28px h24px flex-center cursor-pointer hover:bg-#c22b1c svg:hover:color-[#fff];
@apply w-28px h-24px flex-center cursor-pointer hover:bg-#c22b1c svg:hover:color-[#fff];
}
.n-modal {
align-self: start;

View File

@ -21,13 +21,15 @@ export enum RCodeEnum {
/**URL*/
export enum URLEnum {
/**用户*/
USER = '/api/user',
USER = '/user',
/**Token*/
TOKEN = '/token',
/**聊天*/
CHAT = '/api/chat',
CHAT = '/chat',
/**房间*/
ROOM = '/api/room',
ROOM = '/room',
/**oss*/
OSS = '/api/oss'
OSS = '/oss'
}
/** tauri原生跨窗口通信时传输的类型 */
@ -171,7 +173,7 @@ export enum PowerEnum {
ADMIN
}
export enum IsYetEnum {
export enum IsYesEnum {
NO,
YES
}

View File

@ -13,7 +13,7 @@ export const useChatMain = (activeItem?: SessionItem) => {
const { removeTag, userUid } = useCommon()
const globalStore = useGlobalStore()
const chatStore = useChatStore()
const userInfo = useUserStore()?.userInfo
const userStore = useUserStore()?.userInfo
// const userInfo = useUserStore()?.userInfo
// const chatMessageList = computed(() => chatStore.chatMessageList)
const messageOptions = computed(() => chatStore.currentMessageOptions)
@ -92,7 +92,7 @@ export const useChatMain = (activeItem?: SessionItem) => {
if (isDiffNow({ time: item.message.sendTime, unit: 'minute', diff: 2 })) return
// 判断自己是否是发送者或者是否是管理员
const isCurrentUser = item.fromUser.uid === userUid.value
const isAdmin = userInfo?.power === PowerEnum.ADMIN
const isAdmin = userStore?.power === PowerEnum.ADMIN
return isCurrentUser || isAdmin
}
}
@ -178,7 +178,8 @@ export const useChatMain = (activeItem?: SessionItem) => {
{
label: 'TA',
icon: 'aite',
click: () => {}
click: () => {},
visible: (item: any) => (item.uid ? item.uid !== userUid.value : item.fromUser.uid !== userUid.value)
},
{
label: '查看资料',
@ -189,6 +190,12 @@ export const useChatMain = (activeItem?: SessionItem) => {
Mitt.emit(`${MittEnum.INFO_POPOVER}-${type}`, { uid: uid, type: type })
}
},
{
label: '修改群昵称',
icon: 'edit',
click: () => {},
visible: (item: any) => (item.uid ? item.uid === userUid.value : item.fromUser.uid === userUid.value)
},
{
label: '添加好友',
icon: 'people-plus',

View File

@ -3,6 +3,7 @@ import { Ref } from 'vue'
import { createFileOrVideoDom } from '@/utils/CreateDom.ts'
import { RegExp } from '@/utils/RegExp.ts'
import { useSettingStore } from '@/stores/setting.ts'
import GraphemeSplitter from 'grapheme-splitter'
/** 常用工具类 */
export const useCommon = () => {
@ -345,6 +346,12 @@ export const useCommon = () => {
}
}
/** 计算字符长度 */
const countGraphemes = (value: string) => {
const splitter = new GraphemeSplitter()
return splitter.countGraphemes(value)
}
/** 去除字符串中的元素标记 */
const removeTag = (fragment: any) => new DOMParser().parseFromString(fragment, 'text/html').body.textContent || ''
@ -357,6 +364,7 @@ export const useCommon = () => {
handlePaste,
removeTag,
FileOrVideoPaste,
countGraphemes,
reply,
userUid
}

View File

@ -16,20 +16,13 @@ export const useLogin = () => {
/**
*
* @param isToQrcode
*/
const logout = async (isToQrcode = false) => {
const logout = async () => {
const { createWebviewWindow } = useWindow()
localStorage.removeItem('USER_INFO')
localStorage.removeItem('TOKEN')
// todo 退出账号 需要关闭其他的全部窗口
await createWebviewWindow('登录', 'login', 320, 448, 'home', false, 320, 448).then(() => {
emit(EventEnum.LOGOUT)
emit('logout_success')
// 用于跳转到二维码页面
if (isToQrcode) {
localStorage.setItem('isToQrcode', '1')
}
})
}

View File

@ -203,10 +203,10 @@ export const useMsgInput = (messageInputDom: Ref) => {
msgType: msg.type,
body: { content: msg.content, replyMsgId: msg.reply !== 0 ? msg.reply : undefined }
})
.then(async (res) => {
if (res.message.type === MsgEnum.TEXT) {
await chatStore.pushMsg(res)
}
.then(async () => {
// if (res.message.type === MsgEnum.TEXT) {
// await chatStore.pushMsg(res)
// }
// 发完消息就要刷新会话列表,
// FIXME 如果当前会话已经置顶了,可以不用刷新
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)

View File

@ -221,5 +221,5 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
@import 'style';
@use 'style';
</style>

View File

@ -1,3 +1,5 @@
@use '@/styles/scss/global/variable.scss' as *;
.resizable {
height: 100%;
position: relative;

View File

@ -355,7 +355,7 @@ onMounted(() => {
})
</script>
<style lang="scss" scoped>
@import '../style';
@use '../style';
.setting-item {
left: 24px;

View File

@ -24,7 +24,23 @@
<n-flex :size="20" class="p-22px select-none" vertical>
<!-- 头像 -->
<n-flex justify="center">
<n-avatar :size="80" :src="editInfo.content.avatar" round style="border: 3px solid #fff" />
<n-avatar
v-if="editInfo.content.avatar"
:size="80"
:src="editInfo.content.avatar"
round
style="border: 3px solid #fff" />
<n-avatar
v-else
:size="80"
:color="'#909090'"
:src="editInfo.content.avatar"
round
class="text-22px"
style="border: 3px solid #fff">
{{ editInfo.content.name?.slice(0, 1) }}
</n-avatar>
</n-flex>
<n-flex v-if="currentBadge" align="center" justify="center">
<span class="text-(14px #707070)">当前佩戴的徽章:</span>
@ -47,6 +63,7 @@
:passively-activated="true"
class="rounded-6px"
clearable
:allow-input="noSideSpace"
placeholder="请输入你的昵称"
show-count
type="text">
@ -63,7 +80,7 @@
<template v-for="item in editInfo.badgeList" :key="item.id">
<div class="badge-item">
<n-image
:class="{ 'grayscale-0': item.obtain === IsYetEnum.YES }"
:class="{ 'grayscale-0': item.obtain === IsYesEnum.YES }"
:src="item.img"
alt="badge"
class="flex-center grayscale"
@ -72,8 +89,8 @@
preview-disabled
round />
<div class="tip">
<template v-if="item.obtain === IsYetEnum.YES">
<n-button v-if="item.wearing === IsYetEnum.NO" color="#13987f" @click="toggleWarningBadge(item)">
<template v-if="item.obtain === IsYesEnum.YES">
<n-button v-if="item.wearing === IsYesEnum.NO" color="#13987f" @click="toggleWarningBadge(item)">
佩戴
</n-button>
</template>
@ -97,26 +114,30 @@
</n-modal>
</template>
<script setup lang="ts">
import { IsYetEnum, MittEnum } from '@/enums'
import { IsYesEnum, MittEnum } from '@/enums'
import { leftHook } from '@/layout/left/hook.ts'
import Mitt from '@/utils/Bus.ts'
import apis from '@/services/apis.ts'
import { type } from '@tauri-apps/plugin-os'
import { useCommon } from '@/hooks/useCommon.ts'
import { useUserStore } from '@/stores/user.ts'
const { login, editInfo, currentBadge, saveEditInfo, toggleWarningBadge, countGraphemes } = leftHook()
const userStore = useUserStore()
const { login, editInfo, currentBadge, saveEditInfo, toggleWarningBadge } = leftHook()
const { countGraphemes } = useCommon()
/** 不允许输入空格 */
const noSideSpace = (value: string) => !value.startsWith(' ') && !value.endsWith(' ')
onMounted(() => {
Mitt.on(MittEnum.OPEN_EDIT_INFO, () => {
Mitt.emit(MittEnum.CLOSE_INFO_SHOW)
editInfo.value.show = true
/** 获取用户的徽章列表 */
editInfo.value.content = userStore.userInfo
/** 获取徽章列表 */
apis.getBadgeList().then((res) => {
editInfo.value.badgeList = res as any
})
/** 获取用户信息 */
apis.getUserInfo().then((res) => {
editInfo.value.content = res as any
})
})
})
</script>
@ -124,7 +145,7 @@ onMounted(() => {
.badge-item {
.tip {
transition: opacity 0.4s ease-in-out;
@apply absolute top-0 left-0 w-full h-full flex-center z-999 opacity-0;
@apply absolute top-0 left-0 w-full h-full flex-center gap-4px z-999 opacity-0;
}
@apply bg-#ccc relative rounded-50% size-fit p-4px cursor-pointer;
&:hover .tip {

View File

@ -8,7 +8,17 @@
<template #trigger>
<!-- 头像 -->
<div class="relative size-34px rounded-50% cursor-pointer">
<n-avatar :color="'#fff'" :size="34" :src="login.accountInfo.avatar" fallback-src="/logo.png" round />
<n-avatar
v-if="avatarExists"
:color="'#909090'"
:size="34"
:src="login.accountInfo.avatar"
fallback-src="/logo.png"
round>
{{ avatarExists }}
</n-avatar>
<n-avatar v-else :size="34" :src="login.accountInfo.avatar" fallback-src="/logo.png" round />
<div
class="bg-[--left-bg-color] text-10px rounded-50% size-12px absolute bottom--2px right--2px border-(2px solid [--left-bg-color])"
@ -24,9 +34,26 @@
class="size-full p-15px box-border rounded-8px"
vertical>
<!-- 头像以及信息区域 -->
<n-flex :size="25" align="center" justify="space-between">
<n-flex :size="25" align="center" justify="space-between" class="select-none cursor-default">
<n-flex>
<img :src="login.accountInfo.avatar" alt="" class="size-68px rounded-50% select-none" />
<n-avatar
v-if="avatarExists"
:color="'#909090'"
:src="login.accountInfo.avatar"
round
fallback-src="/logo.png"
class="size-68px text-20px select-none cursor-default">
{{ avatarExists }}
</n-avatar>
<n-avatar
v-else
:color="'#909090'"
:src="login.accountInfo.avatar"
round
fallback-src="/logo.png"
class="size-68px text-20px select-none cursor-default">
</n-avatar>
<n-flex :size="10" class="text-[--text-color]" justify="center" vertical>
<span class="text-18px">{{ login.accountInfo.name }}</span>
@ -77,8 +104,9 @@
<script setup lang="ts">
import { leftHook } from '../hook.ts'
const avatarExists = computed(() => (login.accountInfo.avatar ? void 0 : login.accountInfo.name.slice(0, 1)))
const { login, shrinkStatus, url, infoShow, bgColor, title, themeColor, openContent, handleEditing } = leftHook()
</script>
<style lang="scss" scoped>
@import '../style';
@use '../style';
</style>

View File

@ -230,6 +230,7 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
@use '@/styles/scss/global/variable.scss' as *;
.box {
@apply relative select-none custom-shadow cursor-pointer size-fit w-100px h-100px rounded-8px overflow-hidden;
transition: all 0.2s;

View File

@ -242,6 +242,7 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
@use '@/styles/scss/global/variable.scss' as *;
.float-block {
--y: 0;
--height: 70px;

View File

@ -2,9 +2,17 @@ import { useWindow } from '@/hooks/useWindow.ts'
import { MittEnum, ModalEnum, PluginEnum } from '@/enums'
import Mitt from '@/utils/Bus.ts'
import { useLogin } from '@/hooks/useLogin.ts'
import { useSettingStore } from '@/stores/setting.ts'
import apis from '@/services/apis.ts'
import { LoginStatus, useWsLoginStore } from '@/stores/ws.ts'
import { useUserStore } from '@/stores/user.ts'
const { createWebviewWindow } = useWindow()
const { logout } = useLogin()
const settingStore = useSettingStore()
const loginStore = useWsLoginStore()
const userStore = useUserStore()
const { login } = storeToRefs(settingStore)
/**
* 使pinia写入了localstorage中
*/
@ -89,7 +97,23 @@ const moreList = ref<OPT.L.MoreList[]>([
label: '退出账号',
icon: 'power',
click: async () => {
await logout()
await apis
.logout()
.then(async () => {
await logout()
// 如果没有设置自动登录,则清除用户信息
if (!login.value.autoLogin) {
login.value.accountInfo.token = ''
userStore.userInfo = {}
localStorage.removeItem('USER_INFO')
localStorage.removeItem('TOKEN')
}
userStore.isSign = false
loginStore.loginStatus = LoginStatus.Init
})
.catch(() => {
window.$message.error('退出账号失败')
})
}
}
])

View File

@ -3,7 +3,7 @@ import { useSettingStore } from '@/stores/setting.ts'
import { useUserStore } from '@/stores/user.ts'
import { useCachedStore } from '@/stores/cached.ts'
import { onlineStatus } from '@/stores/onlineStatus.ts'
import { EventEnum, IsYetEnum, MittEnum, MsgEnum, PluginEnum, ThemeEnum } from '@/enums'
import { EventEnum, IsYesEnum, MittEnum, MsgEnum, PluginEnum, ThemeEnum } from '@/enums'
import { BadgeType, UserInfoType } from '@/services/types.ts'
import { useChatStore } from '@/stores/chat.ts'
import { useUserInfo } from '@/hooks/useCached.ts'
@ -11,7 +11,6 @@ import { renderReplyContent } from '@/utils/RenderReplyContent.ts'
import { formatTimestamp } from '@/utils/ComputedTime.ts'
import Mitt from '@/utils/Bus.ts'
import apis from '@/services/apis.ts'
import GraphemeSplitter from 'grapheme-splitter'
import { delay } from 'lodash-es'
import router from '@/router'
import { listen } from '@tauri-apps/api/event'
@ -38,19 +37,18 @@ export const leftHook = () => {
/** 已打开窗口的列表 */
const openWindowsList = ref(new Set())
/** 编辑资料弹窗 */
// TODO 这里考虑是否查接口查实时的用户信息还是直接查本地存储的用户信息 (nyh -> 2024-05-05 01:12:36)
const editInfo = ref<{
show: boolean
content: UserInfoType
content: Partial<UserInfoType>
badgeList: BadgeType[]
}>({
show: false,
content: {} as UserInfoType,
content: {},
badgeList: []
})
/** 当前用户佩戴的徽章 */
const currentBadge = computed(() =>
editInfo.value.badgeList.find((item) => item.obtain === IsYetEnum.YES && item.wearing === IsYetEnum.YES)
editInfo.value.badgeList.find((item) => item.obtain === IsYesEnum.YES && item.wearing === IsYesEnum.YES)
)
const chatStore = useChatStore()
const sessionList = computed(() =>
@ -133,20 +131,8 @@ export const leftHook = () => {
/** 佩戴徽章 */
const toggleWarningBadge = async (badge: BadgeType) => {
if (!badge?.id) return
const res: any = await apis.setUserBadge(badge.id)
if (res) {
window.$message.success('佩戴成功')
/** 获取用户信息 */
apis.getUserInfo().then((res) => {
editInfo.value.content = res as any
})
}
}
/** 计算字符长度 */
const countGraphemes = (value: string) => {
const splitter = new GraphemeSplitter()
return splitter.countGraphemes(value)
await apis.setUserBadge(badge.id)
window.$message.success('佩戴成功')
}
/* 打开并且创建modal */
@ -262,7 +248,6 @@ export const leftHook = () => {
openContent,
saveEditInfo,
toggleWarningBadge,
countGraphemes,
updateCurrentUserCache,
followOS
}

View File

@ -42,5 +42,5 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import 'style';
@use 'style';
</style>

View File

@ -1,3 +1,5 @@
@use '@/styles/scss/global/variable.scss' as *;
@mixin action() {
&:not(.active):hover {
background: var(--left-bg-hover);

16
src/model/User.ts Normal file
View File

@ -0,0 +1,16 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface User {
id: number
name: string
account: string
password: string
avatar: string
sex: number
openId: string
activeStatus: number
lastOptTime: string
itemId: number
status: number
createTime: string
updateTime: string
}

View File

@ -1,7 +1,7 @@
<template>
<n-flex data-tauri-drag-region vertical :size="10" align="center" justify="center" class="flex flex-1">
<!-- logo -->
<img data-tauri-drag-region class="w-275px h-125px drop-shadow-2xl" src="@/assets/logo/hula.png" alt="" />
<img data-tauri-drag-region class="w-275px h-125px drop-shadow-2xl" src="../../../assets/logo/hula.png" alt="" />
<n-flex data-tauri-drag-region vertical justify="center" :size="16" class="p-[30px_20px]">
<p class="text-(14px [--chat-text-color])">你可以尝试使用以下功能</p>

View File

@ -8,11 +8,21 @@ const routes: Array<RouteRecordRaw> = [
name: 'login',
component: () => import('@/views/loginWindow/Login.vue')
},
{
path: '/register',
name: 'register',
component: () => import('@/views/registerWindow/index.vue')
},
{
path: '/qrCode',
name: 'qrCode',
component: () => import('@/views/loginWindow/QRCode.vue')
},
{
path: '/proxy',
name: 'proxy',
component: () => import('@/views/loginWindow/Proxy.vue')
},
{
path: '/tray',
name: 'tray',
@ -48,44 +58,44 @@ const routes: Array<RouteRecordRaw> = [
{
path: '/robot',
name: 'robot',
component: () => import('@/views/homeWindow/robot/index.vue'),
component: () => import('@/plugins/robot/index.vue'),
children: [
{
path: '/welcome',
name: 'welcome',
component: () => import('@/views/homeWindow/robot/views/Welcome.vue')
component: () => import('@/plugins/robot/views/Welcome.vue')
},
{
path: '/chat',
name: 'chat',
component: () => import('@/views/homeWindow/robot/views/Chat.vue')
component: () => import('@/plugins/robot/views/Chat.vue')
},
{
path: '/chatSettings',
name: 'chatSettings',
component: () => import('@/views/homeWindow/robot/views/chatSettings/index.vue')
component: () => import('@/plugins/robot/views/chatSettings/index.vue')
}
]
},
{
path: '/mail',
name: 'mail',
component: () => import('@/views/homeWindow/Mail.vue')
component: () => import('@/views/mailWindow/index.vue')
},
{
path: '/dynamic',
name: 'dynamic',
component: () => import('@/views/homeWindow/Dynamic.vue')
component: () => import('@/plugins/dynamic/index.vue')
},
{
path: '/onlineStatus',
name: 'onlineStatus',
component: () => import('@/views/homeWindow/onlineStatus/index.vue')
component: () => import('@/views/onlineStatusWindow/index.vue')
},
{
path: '/about',
name: 'about',
component: () => import('@/views/homeWindow/more/About.vue')
component: () => import('@/views/aboutWindow/index.vue')
},
{
path: '/alone',
@ -100,22 +110,22 @@ const routes: Array<RouteRecordRaw> = [
{
path: '/settings',
name: 'settings',
component: () => import('@/views/homeWindow/more/settings/index.vue'),
component: () => import('@/views/moreWindow/settings/index.vue'),
children: [
{
path: '/general',
name: 'general',
component: () => import('@/views/homeWindow/more/settings/General.vue')
component: () => import('@/views/moreWindow/settings/General.vue')
},
{
path: '/loginSetting',
name: 'loginSetting',
component: () => import('@/views/homeWindow/more/settings/LoginSetting.vue')
component: () => import('@/views/moreWindow/settings/LoginSetting.vue')
},
{
path: '/versatile',
name: 'versatile',
component: () => import('@/views/homeWindow/more/settings/Versatile.vue')
component: () => import('@/views/moreWindow/settings/Versatile.vue')
}
]
}

View File

@ -22,14 +22,12 @@ import type {
import request from '@/services/request'
const GET = <T>(url: string, params?: any) => request.get<T>(url, params)
const POST = <T>(url: string, params?: any) => request.post<T>(url, params)
const PUT = <T>(url: string, params?: any) => request.put<T>(url, params)
const DELETE = <T>(url: string, params?: any) => request.delete<T>(url, params)
const GET = <T>(url: string, params?: any, abort?: AbortController) => request.get<T>(url, params, abort)
const POST = <T>(url: string, params?: any, abort?: AbortController) => request.post<T>(url, params, abort)
const PUT = <T>(url: string, params?: any, abort?: AbortController) => request.put<T>(url, params, abort)
const DELETE = <T>(url: string, params?: any, abort?: AbortController) => request.delete<T>(url, params, abort)
export default {
/** 获取用户信息 */
getUserInfo: (): Promise<UserItem> => GET(urls.getUserInfo),
/** 获取群成员列表 */
getGroupList: (params?: any) => GET<ListResponse<UserItem>>(urls.getGroupUserList, params),
/** 获取群成员统计 */
@ -47,7 +45,7 @@ export default {
/** 标记消息,点赞等 */
markMsg: (data?: MarkMsgReq) => PUT<void>(urls.markMsg, data),
/** 获取用户详细信息 */
getUserDetail: () => GET<UserInfoType>(urls.getUserInfoDetail, {}),
getUserDetail: () => GET<UserInfoType>(urls.getUserInfoDetail),
/** 获取徽章列表 */
getBadgeList: (): Promise<BadgeType> => GET(urls.getBadgeList),
/** 设置用户勋章 */
@ -67,9 +65,9 @@ export default {
/** 删除id */
deleteEmoji: (params: { id: number }) => DELETE<EmojiItem[]>(urls.deleteEmoji, params),
/** 获取联系人列表 */
getContactList: (params?: any) => GET<ListResponse<ContactItem>>(urls.getContactList, { params }),
getContactList: (params?: any) => GET<ListResponse<ContactItem>>(urls.getContactList, params),
/** 获取好友申请列表 */
requestFriendList: (params?: any) => GET<ListResponse<RequestFriendItem>>(urls.requestFriendList, { params }),
requestFriendList: (params?: any) => GET<ListResponse<RequestFriendItem>>(urls.requestFriendList, params),
/** 发送添加好友请求 */
sendAddFriendRequest: (params: { targetUid: number; msg: string }) =>
POST<EmojiItem[]>(urls.sendAddFriendRequest, params),
@ -96,7 +94,7 @@ export default {
/** 群组详情 */
groupDetail: (params: { id: number }) => GET<GroupDetailReq>(urls.groupDetail, params),
/** 会话详情 */
sessionDetail: (params: { id: number }) => GET<SessionItem>(urls.sessionDetail, { params }),
sessionDetail: (params: { id: number }) => GET<SessionItem>(urls.sessionDetail, params),
/** 会话详情(联系人列表发消息用) */
sessionDetailWithFriends: (params: { uid: number }) => GET<SessionItem>(urls.sessionDetailWithFriends, params),
/** 添加群管理 */
@ -115,5 +113,13 @@ export default {
exitGroup: ({ roomId }: { roomId: number }) =>
DELETE<boolean>(urls.exitGroup, {
roomId
})
}),
/** 账号密码登录 */
login: (user: User, abort?: AbortController) => POST<string>(urls.login, user, abort),
/** 退出登录 */
logout: (abort?: AbortController) => POST<string>(urls.logout, abort),
/** 注册 */
register: (user: User) => POST<string>(urls.register, user),
/** 检查token是否有效 */
checkToken: () => POST<string>(urls.checkToken)
}

View File

@ -24,20 +24,36 @@ export type HttpParams = {
* @param {string} url
* @param {HttpParams} options
* @param {boolean} [fullResponse=false]
* @param {AbortController} abort
* @returns {Promise<T | { data: Promise<T>; resp: Response }>}
*/
async function Http<T>(
url: string,
options: HttpParams,
fullResponse?: true
fullResponse?: true,
abort?: AbortController
): Promise<{ data: Promise<T>; resp: Response }> {
// 获取token
const token = localStorage.getItem('TOKEN')
// 构建请求头
const httpHeaders = new Headers(options.headers || {})
// 设置Content-Type
if (!httpHeaders.has('Content-Type') && !(options.body instanceof FormData)) {
httpHeaders.set('Content-Type', 'application/json')
}
// 设置Authorization
if (token) {
httpHeaders.set('Authorization', `Bearer ${token}`)
}
// 构建 fetch 请求选项
const fetchOptions: RequestInit = {
method: options.method,
headers: httpHeaders
headers: httpHeaders,
signal: abort?.signal
}
// 判断是否需要添加请求体

View File

@ -1,4 +1,3 @@
import { useSettingStore } from '@/stores/setting.ts'
import Http, { HttpParams } from './http.ts'
import { ServiceResponse } from '@/services/types.ts'
@ -26,18 +25,11 @@ const responseInterceptor = async <T>(
url: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
query: any,
body: any
body: any,
abort?: AbortController
): Promise<T> => {
const token = useSettingStore().login.accountInfo.token
const headers = {
'Content-Type': 'application/json;charset=utf-8',
Authorization: `Bearer ${token}`
}
let httpParams: HttpParams = {
method,
headers
method
}
if (method === 'GET') {
@ -54,11 +46,11 @@ const responseInterceptor = async <T>(
}
try {
const data = await Http(url, httpParams, true)
const data = await Http(url, httpParams, true, abort)
const resp = data.resp
const serviceData = (await data.data) as ServiceResponse
console.log(data)
console.log(url, data)
//检查发送请求是否成功
if (resp.status > 400) {
let message = ''
@ -116,20 +108,20 @@ const responseInterceptor = async <T>(
}
}
const get = async <T>(url: string, query: T): Promise<T> => {
return responseInterceptor(url, 'GET', query, {})
const get = async <T>(url: string, query: T, abort?: AbortController): Promise<T> => {
return responseInterceptor(url, 'GET', query, {}, abort)
}
const post = async <T>(url: string, params: any): Promise<T> => {
return responseInterceptor(url, 'POST', {}, params)
const post = async <T>(url: string, params: any, abort?: AbortController): Promise<T> => {
return responseInterceptor(url, 'POST', {}, params, abort)
}
const put = async <T>(url: string, params: any): Promise<T> => {
return responseInterceptor(url, 'PUT', {}, params)
const put = async <T>(url: string, params: any, abort?: AbortController): Promise<T> => {
return responseInterceptor(url, 'PUT', {}, params, abort)
}
const del = async <T>(url: string, params: any): Promise<T> => {
return responseInterceptor(url, 'DELETE', {}, params)
const del = async <T>(url: string, params: any, abort?: AbortController): Promise<T> => {
return responseInterceptor(url, 'DELETE', {}, params, abort)
}
export default {

View File

@ -3,7 +3,7 @@
* 使TSDoc规范进行注释便使
* @see TSDoc规范https://tsdoc.org/
**/
import { ActEnum, IsYetEnum, MarkEnum, MsgEnum, OnlineEnum, RoomTypeEnum, SexEnum } from '@/enums'
import { ActEnum, IsYesEnum, MarkEnum, MsgEnum, OnlineEnum, RoomTypeEnum, SexEnum } from '@/enums'
/**响应请求体*/
export type ServiceResponse = {
@ -135,6 +135,10 @@ export type UserInfoType = {
/** 用户唯一标识 */
uid: number
/** 用户头像 */
account: string
/** 用户头像 */
password: string
/** 用户头像 */
avatar: string
/** 用户名 */
name: string
@ -156,9 +160,9 @@ export type BadgeType = {
// 徽章图标
img: string
// 是否拥有 0否 1是
obtain: IsYetEnum
obtain: IsYesEnum
// 是否佩戴 0否 1是
wearing: IsYetEnum
wearing: IsYesEnum
}
export type MarkItemType = {

View File

@ -5,7 +5,7 @@ const { VITE_SERVICE_URL } = import.meta.env
const prefix = VITE_SERVICE_URL
export default {
getUserInfo: `${prefix + URLEnum.USER}/userInfo`, // 获取用户信息
// 用户相关
getBadgeList: `${prefix + URLEnum.USER}/badges`, // 获取徽章列表
getMemberStatistic: `${prefix + URLEnum.CHAT}/public/member/statistic`,
getUserInfoBatch: `${prefix + URLEnum.USER}/public/summary/userInfo/batch`,
@ -43,5 +43,15 @@ export default {
revokeAdmin: `${prefix + URLEnum.ROOM}/group/admin`, // 添加管理员
groupDetail: `${prefix + URLEnum.ROOM}/public/group`, // 群组详情
sessionDetail: `${prefix + URLEnum.CHAT}/public/contact/detail`, // 会话详情
sessionDetailWithFriends: `${prefix + URLEnum.CHAT}/public/contact/detail/friend` // 会话详情(联系人列表发消息用)
sessionDetailWithFriends: `${prefix + URLEnum.CHAT}/public/contact/detail/friend`, // 会话详情(联系人列表发消息用)
// token相关
// 注册
register: `${prefix + URLEnum.TOKEN}/register`,
// 登录
login: `${prefix + URLEnum.TOKEN}/login`,
// 退出登录
logout: `${prefix + URLEnum.TOKEN}/logout`,
// 检查token是否有效
checkToken: `${prefix + URLEnum.TOKEN}/check`
}

View File

@ -3,7 +3,6 @@ import { useUserStore } from '@/stores/user'
import { useChatStore } from '@/stores/chat'
import { useGroupStore } from '@/stores/group'
import { useGlobalStore } from '@/stores/global'
import { useEmojiStore } from '@/stores/emoji'
import { WsResponseMessageType } from '@/utils/wsType'
import type { LoginSuccessResType, LoginInitResType, WsReqMsgContentType, OnStatusChangeType } from '@/utils/wsType'
import type { MessageType, MarkItemType, RevokedMsgType } from '@/services/types'
@ -40,11 +39,11 @@ class WS {
worker.postMessage(`{"type":"initWS","value":${token ? `"${token}"` : null}}`)
}
onWorkerMsg = (e: MessageEvent<any>) => {
onWorkerMsg = async (e: MessageEvent<any>) => {
const params: { type: string; value: unknown } = JSON.parse(e.data)
switch (params.type) {
case 'message': {
this.onMessage(params.value as string)
await this.onMessage(params.value as string)
break
}
case 'open': {
@ -100,7 +99,7 @@ class WS {
}
// 收到消息回调
onMessage = (value: string) => {
onMessage = async (value: string) => {
// FIXME 可能需要 try catch,
const params: { type: WsResponseMessageType; data: unknown } = JSON.parse(value)
const loginStore = useWsLoginStore()
@ -108,7 +107,6 @@ class WS {
const chatStore = useChatStore()
const groupStore = useGroupStore()
const globalStore = useGlobalStore()
const emojiStore = useEmojiStore()
switch (params.type) {
// 获取登录二维码
case WsResponseMessageType.LoginQrCode: {
@ -126,7 +124,6 @@ class WS {
case WsResponseMessageType.LoginSuccess: {
userStore.isSign = true
const { token, ...rest } = params.data as LoginSuccessResType
Mitt.emit(WsResEnum.LOGIN_SUCCESS, params.data)
// FIXME 可以不需要赋值了,单独请求了接口。
userStore.userInfo = { ...userStore.userInfo, ...rest }
localStorage.setItem('USER_INFO', JSON.stringify(rest))
@ -137,6 +134,8 @@ class WS {
computedToken.get()
// 获取用户详情
userStore.getUserDetailAction()
// 获取用户详情
await chatStore.getSessionList(true)
// 自己更新自己上线
groupStore.batchUpdateUserStatus([
{
@ -147,15 +146,14 @@ class WS {
uid: rest.uid
}
])
// 获取用户详情
chatStore.getSessionList(true)
// 自定义表情列表
emojiStore.getEmojiList()
// TODO 先不获取 emoji 列表,当我点击 emoji 按钮的时候再获取
// await emojiStore.getEmojiList()
Mitt.emit(WsResEnum.LOGIN_SUCCESS, params.data)
break
}
// 收到消息
case WsResponseMessageType.ReceiveMessage: {
chatStore.pushMsg(params.data as MessageType)
await chatStore.pushMsg(params.data as MessageType)
Mitt.emit(MittEnum.SEND_MESSAGE, params.data)
break
}
@ -164,7 +162,9 @@ class WS {
const data = params.data as OnStatusChangeType
groupStore.countInfo.onlineNum = data.onlineNum
// groupStore.countInfo.totalNum = data.totalNum
groupStore.batchUpdateUserStatus(data.changeList)
//groupStore.batchUpdateUserStatus(data.changeList)
groupStore.getGroupUserList(true)
console.log('收到用户下线通知', data)
break
}
// 用户 token 过期

View File

@ -70,6 +70,7 @@ export const useCachedStore = defineStore('cached', () => {
})
.filter((item) => !item.lastModifyTime || isDiffNow10Min(item.lastModifyTime))
if (!result.length) return
// TODO 批量请求徽章详情当翻历史记录的时候会导致发送很多请求,需要优化,可以直接存储到本地
const data = await apis.getBadgesBatch(result)
data?.forEach(
(item: CacheBadgeItem) =>
@ -93,7 +94,7 @@ export const useCachedStore = defineStore('cached', () => {
const getGroupAtUserBaseInfo = async () => {
if (currentRoomId.value === 1) return
currentAtUsersList.value = await apis.getAllUserBaseInfo({ params: { roomId: currentRoomId.value } })
currentAtUsersList.value = await apis.getAllUserBaseInfo({ roomId: currentRoomId.value })
}
/**

View File

@ -124,7 +124,7 @@ export const useChatStore = defineStore('chat', () => {
if (currentRoomType.value === RoomTypeEnum.GROUP) {
groupStore.getGroupUserList(true)
groupStore.getCountStatistic()
// cachedStore.getGroupAtUserBaseInfo()
cachedStore.getGroupAtUserBaseInfo()
}
}
@ -188,10 +188,8 @@ export const useChatStore = defineStore('chat', () => {
sessionOptions.isLoading = true
const response = await apis
.getSessionList({
params: {
pageSize: sessionList.length > pageSize ? sessionList.length : pageSize,
cursor: isFresh || !sessionOptions.cursor ? undefined : sessionOptions.cursor
}
pageSize: sessionList.length > pageSize ? sessionList.length : pageSize,
cursor: isFresh || !sessionOptions.cursor ? '' : sessionOptions.cursor
})
.catch(() => {
sessionOptions.isLoading = false
@ -220,7 +218,7 @@ export const useChatStore = defineStore('chat', () => {
// 初始化所有用户基本信息
// userStore.isSign && (await cachedStore.initAllUserBaseInfo())
// 联系人列表
await contactStore.getContactList(true)
await contactStore.getContactList()
}
}

View File

@ -25,7 +25,7 @@ export const useContactStore = defineStore('contact', () => {
.getContactList({
// TODO 先写 100稍后优化
pageSize: 100,
cursor: isFresh || !contactsOptions.cursor ? undefined : contactsOptions.cursor
cursor: isFresh || !contactsOptions.cursor ? '' : contactsOptions.cursor
})
.catch(() => {
contactsOptions.isLoading = false
@ -62,7 +62,7 @@ export const useContactStore = defineStore('contact', () => {
const res = await apis
.requestFriendList({
pageSize,
cursor: isFresh || !requestFriendsOptions.cursor ? undefined : requestFriendsOptions.cursor
cursor: isFresh || !requestFriendsOptions.cursor ? '' : requestFriendsOptions.cursor
})
.catch(() => {
requestFriendsOptions.isLoading = false
@ -84,11 +84,11 @@ export const useContactStore = defineStore('contact', () => {
/** 接受好友请求 */
const onAcceptFriend = (applyId: number) => {
// 同意好友申请
apis.applyFriendRequest({ applyId }).then(() => {
apis.applyFriendRequest({ applyId }).then(async () => {
// 刷新好友申请列表
getRequestFriendsList(true)
await getRequestFriendsList(true)
// 刷新好友列表
getContactList(true)
await getContactList(true)
// 标识为可以发消息的人
if (globalStore.currentSelectedContact) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@ -7,7 +7,6 @@ import { OnlineEnum, RoleEnum } from '@/enums'
import { uniqueUserList } from '@/utils/unique'
import { useCachedStore } from '@/stores/cached'
import { useUserStore } from '@/stores/user'
import { cloneDeep } from 'lodash-es'
const sorAction = (pre: UserItem, next: UserItem) => {
if (pre.activeStatus === OnlineEnum.ONLINE && next.activeStatus === OnlineEnum.ONLINE) {
@ -82,23 +81,17 @@ export const useGroupStore = defineStore('group', () => {
roomId: currentRoomId.value
})
// 移动端控制显隐
const showGroupList = ref(false)
// 获取群成员
const getGroupUserList = async (refresh = false) => {
const res = await apis.getGroupList({
params: {
pageSize,
cursor: refresh ? undefined : userListOptions.cursor,
roomId: currentRoomId.value
}
const data = await apis.getGroupList({
pageSize: pageSize,
cursor: refresh ? '' : userListOptions.cursor,
roomId: currentRoomId.value
})
if (!res) return
const data = res
const tempNew = cloneDeep(uniqueUserList(refresh ? data.list : [...data.list, ...userList.value]))
tempNew.sort(sorAction)
userList.value = tempNew
if (!data) return
const newUserList = uniqueUserList(refresh ? data.list : [...data.list, ...userList.value])
newUserList.sort(sorAction)
userList.value = newUserList
userListOptions.cursor = data.cursor
userListOptions.isLast = data.isLast
userListOptions.loading = false
@ -116,21 +109,23 @@ export const useGroupStore = defineStore('group', () => {
}
// 加载更多群成员
const loadMore = async () => {
if (userListOptions.isLast) return
const loadMoreGroupMembers = async () => {
if (userListOptions.isLast || userListOptions.loading) return
userListOptions.loading = true
await getGroupUserList()
userListOptions.loading = false
}
// 更新用户在线状态
const batchUpdateUserStatus = (items: UserItem[]) => {
const tempNew = cloneDeep(userList.value)
for (let index = 0, len = items.length; index < len; index++) {
const curUser = items[index]
const findIndex = tempNew.findIndex((item) => item.uid === curUser.uid)
findIndex > -1 && (tempNew[findIndex].activeStatus = curUser.activeStatus)
const findIndex = userList.value.findIndex((item) => item.uid === curUser.uid)
userList.value[findIndex] = {
...userList.value[findIndex],
activeStatus: items[index].activeStatus
}
}
tempNew.sort(sorAction)
userList.value = tempNew
}
// 过滤掉小黑子
@ -189,13 +184,12 @@ export const useGroupStore = defineStore('group', () => {
return {
userList,
userListOptions,
loadMore,
loadMoreGroupMembers,
getGroupUserList,
getCountStatistic,
currentLordId,
countInfo,
batchUpdateUserStatus,
showGroupList,
filterUser,
adminUidList,
adminList,

View File

@ -1,14 +1,13 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { StoresEnum } from '../enums'
import { StoresEnum } from '@/enums'
export const useLoginHistoriesStore = defineStore(
StoresEnum.LOGIN_HISTORY,
() => {
const loginHistories = ref<STO.Setting['login']['accountInfo'][]>([
{
account: 'hula',
password: '123456',
account: 'admin',
password: 'admin',
name: '超级GG帮',
avatar: 'https://picsum.photos/140?1',
uid: 123456,
@ -16,7 +15,7 @@ export const useLoginHistoriesStore = defineStore(
},
{
account: 'hula1',
password: '123456',
password: 'hula1',
name: '二狗子',
avatar: 'https://picsum.photos/140?2',
uid: 123456,
@ -24,7 +23,7 @@ export const useLoginHistoriesStore = defineStore(
},
{
account: 'hula2',
password: '123456',
password: 'hula2',
name: '李山离',
avatar: 'https://picsum.photos/140?3',
uid: 123456,
@ -32,7 +31,7 @@ export const useLoginHistoriesStore = defineStore(
},
{
account: 'hula3',
password: '123456',
password: 'hula3',
name: '牛什么呢',
avatar: 'https://picsum.photos/140?4',
uid: 123456,

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { StoresEnum } from '@/enums'
import { statusItem } from '@/views/homeWindow/onlineStatus/config.ts'
import { statusItem } from '@/views/onlineStatusWindow/config.ts'
import Colorthief from 'colorthief'
const colorthief = new Colorthief()

View File

@ -1,10 +1,6 @@
import { defineStore } from 'pinia'
import { CloseBxEnum, StoresEnum, ShowModeEnum, ThemeEnum } from '@/enums'
import apis from '@/services/apis.ts'
import { isDiffNow10Min } from '@/utils/ComputedTime.ts'
import type { CacheBadgeItem } from '@/services/types.ts'
const badgeCachedList = reactive<Record<number, Partial<CacheBadgeItem>>>({})
// TODO 使用indexDB或sqlite缓存数据还需要根据每个账号来进行配置 (nyh -> 2024-03-26 01:22:12)
export const useSettingStore = defineStore(StoresEnum.SETTING, {
state: (): STO.Setting => ({
@ -74,27 +70,6 @@ export const useSettingStore = defineStore(StoresEnum.SETTING, {
setAccountInfo(accountInfo: STO.Setting['login']['accountInfo']) {
this.login.accountInfo = accountInfo
},
/** 批量获取用户徽章详细信息 */
async getBatchBadgeInfo(itemIds: number[]) {
// 没有 lastModifyTime 的要更新lastModifyTime 距离现在 10 分钟已上的也要更新
const result = itemIds
.map((itemId) => {
const cacheBadge = badgeCachedList[itemId]
return { itemId, lastModifyTime: cacheBadge?.lastModifyTime }
})
.filter((item) => !item.lastModifyTime || isDiffNow10Min(item.lastModifyTime))
if (!result.length) return
const data = await apis.getBadgesBatch(result)
data?.forEach(
(item: CacheBadgeItem) =>
// 更新最后更新时间。
(badgeCachedList[item.itemId] = {
...(item?.needRefresh ? item : badgeCachedList[item.itemId]),
needRefresh: void 0,
lastModifyTime: Date.now()
})
)
},
/** 清空账号信息 */
clearAccount() {
this.login.accountInfo.password = ''

View File

@ -1,4 +1,3 @@
import { ref } from 'vue'
import apis from '@/services/apis'
import { defineStore } from 'pinia'
import type { UserInfoType } from '@/services/types'

View File

@ -1,6 +1,4 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import wsIns from '@/services/webSocket.ts'
import { WsRequestMsgType } from '@/utils/wsType'

View File

@ -22,21 +22,17 @@ declare module 'vue' {
InfoPopover: typeof import('./../components/common/InfoPopover.vue')['default']
MsgInput: typeof import('./../components/rightBox/MsgInput.vue')['default']
NaiveProvider: typeof import('./../components/common/NaiveProvider.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NAvatar: typeof import('naive-ui')['NAvatar']
NAvatarGroup: typeof import('naive-ui')['NAvatarGroup']
NBadge: typeof import('naive-ui')['NBadge']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDropdown: typeof import('naive-ui')['NDropdown']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NFlex: typeof import('naive-ui')['NFlex']
NGradientText: typeof import('naive-ui')['NGradientText']
NIcon: typeof import('naive-ui')['NIcon']
NIconWrapper: typeof import('naive-ui')['NIconWrapper']
NImage: typeof import('naive-ui')['NImage']
@ -47,20 +43,16 @@ declare module 'vue' {
NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
NPopselect: typeof import('naive-ui')['NPopselect']
NQrCode: typeof import('naive-ui')['NQrCode']
NRadio: typeof import('naive-ui')['NRadio']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSpin: typeof import('naive-ui')['NSpin']
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
NVirtualList: typeof import('naive-ui')['NVirtualList']
RenderMessage: typeof import('./../components/rightBox/renderMessage/index.vue')['default']

View File

@ -17,7 +17,7 @@ const task = () => {
request?.abort()
if (queue.size > 0) {
// 开始新请求
request = apis.getMsgReadCount({ params: { msgIds: [...queue] } })
request = apis.getMsgReadCount({ msgIds: [...queue] })
request.send().then((res: MsgReadUnReadCountType[]) => {
const result = new Map<number, MsgReadUnReadCountType>()
res.forEach((item) => result.set(item.msgId, item))

View File

@ -101,7 +101,7 @@
<n-flex v-if="!isLogining && !isWrongPassword" justify="space-around" align="center" :size="0" class="options">
<p class="text-(14px #fefefe)" @click="isUnlockPage = false">返回</p>
<p class="text-(14px #fefefe)" @click="logout()">退出登录</p>
<p class="text-(14px #fefefe)" @click="logout">退出登录</p>
<p class="text-(14px #fefefe)">忘记密码</p>
<p class="text-(14px #fff)" @click="unlock">进入系统</p>
</n-flex>

View File

@ -51,7 +51,7 @@
import { useWindow } from '@/hooks/useWindow.ts'
import { invoke } from '@tauri-apps/api/core'
import { exit } from '@tauri-apps/plugin-process'
import { statusItem } from '@/views/homeWindow/onlineStatus/config.ts'
import { statusItem } from '@/views/onlineStatusWindow/config.ts'
import { onlineStatus } from '@/stores/onlineStatus.ts'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { listen } from '@tauri-apps/api/event'

View File

@ -5,13 +5,13 @@
<n-flex vertical align="center" :size="20" class="size-full pt-100px" data-tauri-drag-region>
<div @mousemove="handleMouseMove" @mouseleave="handleMouseLeave" class="box" data-tauri-drag-region>
<div id="computer" class="computer">
<img class="w-224px h-158px relative" src="@/assets/img/win.png" alt="" />
<img class="w-224px h-158px relative" src="../../assets/img/win.png" alt="" />
<div
style="background: rgba(111, 111, 111, 0.1)"
class="w-170px h-113px absolute top-9% left-51% transform -translate-x-51% -translate-y-9%"></div>
<img
class="drop-shadow-md absolute top-30% left-1/2 transform -translate-x-1/2 -translate-y-30% w-140px h-60px"
src="@/assets/logo/hula.png"
src="../../assets/logo/hula.png"
alt="" />
</div>
</div>
@ -102,7 +102,6 @@ onMounted(async () => {
.box {
width: 240px;
height: 200px;
transform-style: preserve-3d;
perspective: 500px;
display: flex;
justify-content: center;
@ -111,7 +110,6 @@ onMounted(async () => {
.computer {
position: relative;
transition: all 0.2s;
transform-style: preserve-3d;
}
}
</style>

View File

@ -29,15 +29,28 @@
:key="item.uid">
<n-flex align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
<n-avatar
v-if="useUserInfo(item.uid).value.avatar"
round
bordered
:color="'#fff'"
:size="44"
class="grayscale"
:class="{ 'grayscale-0': item.activeStatus === OnlineEnum.ONLINE }"
:src="useUserInfo(item.uid).value.avatar"
fallback-src="/logo.png" />
<n-avatar
v-else
round
bordered
:color="'#909090'"
:size="44"
class="grayscale"
:class="{ 'grayscale-0': item.activeStatus === OnlineEnum.ONLINE }"
:src="useUserInfo(item.uid).value.avatar"
fallback-src="/logo.png">
{{ useUserInfo(item.uid).value.name?.slice(0, 1) }}
</n-avatar>
<n-flex vertical justify="space-between" class="h-fit flex-1 truncate">
<span class="text-14px leading-tight flex-1 truncate">{{
useUserInfo(item.uid).value.name

View File

@ -1,6 +1,6 @@
<template>
<main class="flex-1 rounded-8px bg-[--right-bg-color] h-full w-100vw">
<div style="background: var(--right-theme-bg-color)">
<div style="background: var(--right-theme-bg-color); height: 100%">
<ActionBar :shrink="false" :current-label="appWindow.label" />
<ChatBox />
@ -15,8 +15,8 @@ import { EventEnum } from '@/enums'
const appWindow = WebviewWindow.getCurrent()
// ,
appWindow.onCloseRequested(async (e) => {
await emit(EventEnum.WIN_CLOSE, e)
appWindow.onCloseRequested(async () => {
await emit(EventEnum.WIN_CLOSE, appWindow.label)
})
/**! 创建新窗口然后需要通信传递数据时候需要进行提交一次页面创建成功的事件,否则会接收不到数据 */

View File

@ -17,7 +17,11 @@
@dblclick="handleMsgDblclick(item)"
@select="$event.click(item)">
<n-flex :size="10" align="center" class="h-75px pl-6px pr-8px flex-1">
<n-avatar :color="'#fff'" :size="44" :src="item.avatar" bordered fallback-src="/logo.png" round />
<n-avatar v-if="item.avatar" :size="44" :src="item.avatar" bordered fallback-src="/logo.png" round />
<n-avatar v-else :color="'#909090'" :size="44" :src="item.avatar" bordered fallback-src="/logo.png" round>
{{ item.name?.slice(0, 1) }}
</n-avatar>
<n-flex class="h-fit flex-1 truncate" justify="space-between" vertical>
<n-flex :size="4" align="center" class="flex-1 truncate" justify="space-between">
@ -123,7 +127,7 @@ watchEffect(() => {
onBeforeMount(() => {
//
chatStore.getSessionList(true)
chatStore.getSessionList()
})
onMounted(() => {

View File

@ -1,22 +1,25 @@
<template>
<!-- todo 这里设置了 data-tauri-drag-region但是有部分区域不可以拖动 -->
<!-- 单独使用n-config-provider来包裹不需要主题切换的界面 -->
<n-config-provider :theme="lightTheme" data-tauri-drag-region class="login-box size-full rounded-8px select-none">
<!--顶部操作栏-->
<ActionBar :max-w="false" :shrink="false" />
<ActionBar :max-w="false" :shrink="false" proxy />
<!-- 手动登录样式 -->
<n-flex vertical :size="25" v-if="!isAutoLogin" data-tauri-drag-region>
<n-flex vertical :size="25" v-if="!login.autoLogin || !login.accountInfo.token">
<!-- 头像 -->
<n-flex justify="center" class="w-full pt-35px" data-tauri-drag-region>
<img
class="w-80px h-80px rounded-50% bg-#b6d6d9ff border-(2px solid #fff)"
:src="info.avatar || '/logo.png'"
alt="" />
<n-avatar
v-if="info.avatar"
class="size-80px rounded-50% bg-#b6d6d9ff border-(2px solid #fff)"
:src="info.avatar || '/logo.png'" />
<n-avatar v-else class="size-80px text-20px rounded-50% bg-#b6d6d9ff border-(2px solid #fff)">
{{ info.name.slice(0, 1) }}
</n-avatar>
</n-flex>
<!-- 登录菜单 -->
<n-flex class="ma text-center h-full w-260px" vertical :size="16" data-tauri-drag-region>
<n-flex class="ma text-center h-full w-260px" vertical :size="16">
<n-input
style="padding-left: 20px"
size="large"
@ -38,7 +41,7 @@
</template>
</n-input>
<!-- 账号选择框 TODO 尝试使用n-popover组件来实现这个功能 (nyh -> 2024-03-09 02:56:06)-->
<!-- 账号选择框-->
<div
style="border: 1px solid rgba(70, 70, 70, 0.1)"
v-if="loginHistories.length > 0 && arrowStatus"
@ -49,9 +52,12 @@
v-for="item in loginHistories"
:key="item.account"
@click="giveAccount(item)"
class="p-8px cursor-pointer hover:bg-#f3f3f3 hover: rounded-6px">
class="p-8px cursor-pointer hover:bg-#f3f3f3 hover:rounded-6px">
<div class="flex-between-center">
<img :src="item.avatar" class="w-28px h-28px bg-#ccc rounded-50%" alt="" />
<n-avatar v-if="item.avatar" :src="item.avatar" class="size-28px bg-#ccc rounded-50%" />
<n-avatar v-else :src="item.avatar" :color="'#909090'" class="size-28px text-10px bg-#ccc rounded-50%">
{{ item.name?.slice(0, 1) }}
</n-avatar>
<p class="text-14px color-#505050">{{ item.account }}</p>
<svg @click.stop="delAccount(item)" class="w-12px h-12px">
<use href="#close"></use>
@ -87,9 +93,9 @@
:loading="loading"
:disabled="loginDisabled"
class="w-full mt-8px mb-50px"
@click="loginWin"
@click="normalLogin"
color="#13987f">
{{ loginText }}
<span>{{ loginText }}</span>
</n-button>
</n-flex>
</n-flex>
@ -99,7 +105,6 @@
<n-flex justify="center" class="mt-15px">
<img src="@/assets/logo/hula.png" class="w-140px h-60px" alt="" />
</n-flex>
<n-flex :size="30" vertical>
<!-- 头像 -->
<n-flex justify="center">
@ -129,16 +134,26 @@
</n-flex>
<!-- 底部操作栏 -->
<n-flex justify="center" class="text-14px" id="bottomBar" data-tauri-drag-region>
<n-flex justify="center" class="text-14px" id="bottomBar">
<div class="color-#13987f cursor-pointer" @click="router.push('/qrCode')">扫码登录</div>
<div class="w-1px h-14px bg-#ccc"></div>
<div v-if="isAutoLogin" class="color-#13987f cursor-pointer">移除账号</div>
<n-popover v-else trigger="click" :show-checkmark="false" :show-arrow="false">
<div v-if="login.autoLogin" class="color-#13987f cursor-pointer" @click="removeToken">移除账号</div>
<n-popover
v-else
trigger="click"
id="moreShow"
v-model:show="moreShow"
:show-checkmark="false"
:show-arrow="false">
<template #trigger>
<div class="color-#13987f cursor-pointer">更多选项</div>
</template>
<n-flex vertical :size="2">
<div class="text-14px cursor-pointer hover:bg-#f3f3f3 hover:rounded-6px p-8px">注册账号</div>
<div
class="text-14px cursor-pointer hover:bg-#f3f3f3 hover:rounded-6px p-8px"
@click="createWebviewWindow('注册', 'register', 600, 600)">
注册账号
</div>
<div class="text-14px cursor-pointer hover:bg-#f3f3f3 hover:rounded-6px p-8px">忘记密码</div>
</n-flex>
</n-popover>
@ -148,14 +163,19 @@
<script setup lang="ts">
import router from '@/router'
import { useWindow } from '@/hooks/useWindow.ts'
import { delay } from 'lodash-es'
import { lightTheme } from 'naive-ui'
import { useSettingStore } from '@/stores/setting.ts'
import { useLogin } from '@/hooks/useLogin.ts'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useLoginHistoriesStore } from '@/stores/loginHistory.ts'
import apis from '@/services/apis.ts'
import { useUserStore } from '@/stores/user.ts'
import { computedToken } from '@/services/request.ts'
import { useChatStore } from '@/stores/chat.ts'
const settingStore = useSettingStore()
const userStore = useUserStore()
const chatStore = useChatStore()
const loginHistoriesStore = useLoginHistoriesStore()
const { loginHistories } = loginHistoriesStore
const { login } = storeToRefs(settingStore)
@ -167,6 +187,7 @@ const info = ref({
name: '',
uid: 0
})
/** 是否中断登录 */
const interruptLogin = ref(false)
/** 协议 */
@ -174,7 +195,7 @@ const protocol = ref(true)
const loginDisabled = ref(false)
const loading = ref(false)
const arrowStatus = ref(false)
const isAutoLogin = ref(false)
const moreShow = ref(false)
const { setLoginState } = useLogin()
const accountPH = ref('输入HuLa账号')
const passwordPH = ref('输入HuLa密码')
@ -190,6 +211,8 @@ watchEffect(() => {
}
if (interruptLogin.value) {
loginDisabled.value = false
loading.value = false
interruptLogin.value = false
}
})
@ -222,35 +245,99 @@ const giveAccount = (item: STO.Setting['login']['accountInfo']) => {
}
/**登录后创建主页窗口*/
const loginWin = () => {
if (interruptLogin.value) return
const normalLogin = async () => {
loading.value = true
delay(async () => {
await createWebviewWindow('HuLa', 'home', 960, 720, 'login', true)
loading.value = false
if (!login.value.autoLogin || login.value.accountInfo.password === '') {
const account = {
...info.value,
token: 'test'
apis
.login({ ...info.value } as unknown as User)
.then(async (token) => {
if (interruptLogin.value) return
loginDisabled.value = true
loginText.value = '登录成功, 正在跳转'
userStore.isSign = true
login.value.accountInfo.token = token
// localStorage.setItem('USER_INFO', JSON.stringify(rest))
localStorage.setItem('TOKEN', token)
//
if (localStorage.getItem('wsLogin')) {
localStorage.removeItem('wsLogin')
}
// token.
computedToken.clear()
computedToken.get()
//
const userDetail = await apis.getUserDetail()
// 线
// groupStore.batchUpdateUserStatus([
// {
// activeStatus: OnlineEnum.ONLINE,
// avatar: rest.avatar,
// lastOptTime: Date.now(),
// name: rest.name,
// uid: rest.uid
// }
// ])
//
await chatStore.getSessionList(true)
// TODO emoji emoji
// await emojiStore.getEmojiList()
// TODO iduiduid
const account = {
...userDetail,
uid: (userDetail as any).id,
token
}
loading.value = false
settingStore.setAccountInfo(account)
loginHistoriesStore.addLoginHistory(account)
await setLoginState()
}
}, 1000)
//
await openHomeWindow()
})
.catch(() => {
window.$message.error('登录失败')
loading.value = false
})
}
const openHomeWindow = async () => {
await createWebviewWindow('HuLa', 'home', 960, 720, 'login', true)
}
/** 自动登录 */
const autoLogin = () => {
interruptLogin.value = false
isAutoLogin.value = true
loading.value = true
// TODO (nyh -> 2024-03-16 12:06:59)
loginText.value = '网络连接中'
delay(async () => {
loginWin()
loginText.value = '登录'
await setLoginState()
}, 1000)
// TODO 退tokencheckToken401
apis
.checkToken()
.then(async () => {
loginText.value = '登录成功, 正在跳转'
loading.value = false
await openHomeWindow()
await setLoginState()
})
.catch(() => {
window.$message.error('登录失败')
login.value.accountInfo.token = ''
router.push('/login')
loading.value = false
loginText.value = '登录'
})
}
/** 移除已登录账号 */
const removeToken = () => {
login.value.accountInfo = {
account: '',
password: '',
avatar: '/logo.png',
name: '',
uid: 0,
token: ''
}
login.value.autoLogin = false
}
const closeMenu = (event: MouseEvent) => {
@ -258,29 +345,35 @@ const closeMenu = (event: MouseEvent) => {
if (!target.matches('.account-box, .account-box *, .down')) {
arrowStatus.value = false
}
if (target.matches('#bottomBar *') && isAutoLogin.value) {
if (target.matches('#bottomBar *') && login.value.autoLogin) {
interruptLogin.value = true
}
if (!target.matches('#moreShow')) {
moreShow.value = false
}
}
const enterKey = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
normalLogin()
}
}
onMounted(async () => {
//
if (localStorage.getItem('isToQrcode')) {
router.push('/qrCode')
await nextTick(() => {
localStorage.removeItem('isToQrcode')
})
}
await getCurrentWebviewWindow().show()
if (login.value.autoLogin && login.value.accountInfo.password !== '') {
//
if (login.value.autoLogin && login.value.accountInfo.token) {
autoLogin()
} else {
loginHistories.length > 0 && giveAccount(loginHistories[0])
}
loginHistories.length > 0 && giveAccount(loginHistories[0])
window.addEventListener('click', closeMenu, true)
window.addEventListener('keyup', enterKey)
})
onUnmounted(() => {
window.removeEventListener('click', closeMenu, true)
window.removeEventListener('keyup', enterKey)
})
</script>

View File

@ -0,0 +1,23 @@
<template>
<n-config-provider :theme="lightTheme" data-tauri-drag-region class="login-box size-full rounded-8px select-none">
<!--顶部操作栏-->
<ActionBar :max-w="false" :shrink="false" proxy data-tauri-drag-region />
<n-flex vertical :size="25" align="center" class="pt-30px">
<span class="text-(16px #70938c) font-bold textFont">网络代理设置</span>
<p>敬请期待</p>
<p @click="router.push('/login')" class="text-(14px #13987f) cursor-pointer">返回</p>
</n-flex>
</n-config-provider>
</template>
<script setup lang="ts">
import { lightTheme } from 'naive-ui'
import router from '@/router'
</script>
<style scoped lang="scss">
@use '@/styles/scss/global/login-bg';
.textFont {
font-family: AliFangYuan, sans-serif !important;
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<n-config-provider :theme="lightTheme" data-tauri-drag-region class="login-box size-full rounded-8px select-none">
<!--顶部操作栏-->
<ActionBar :max-w="false" :shrink="false" data-tauri-drag-region />
<ActionBar :max-w="false" :shrink="false" proxy data-tauri-drag-region />
<n-flex justify="center" class="mt-15px" data-tauri-drag-region>
<img src="@/assets/logo/hula.png" class="w-140px h-60px drop-shadow-xl" alt="" data-tauri-drag-region />
@ -36,7 +36,9 @@
<n-flex justify="center" class="text-14px mt-48px" data-tauri-drag-region>
<div class="color-#13987f cursor-pointer" @click="router.push('/login')">账密登录</div>
<div class="w-1px h-14px bg-#ccc"></div>
<div class="color-#13987f cursor-pointer">注册账号</div>
<div class="color-#13987f cursor-pointer" @click="createWebviewWindow('注册', 'register', 600, 600)">
注册账号
</div>
</n-flex>
</n-config-provider>
</template>

View File

@ -8,7 +8,7 @@
<p class="text-(12px #13987f) cursor-pointer">GitHub 给添加星标</p>
</template>
<n-flex vertical class="w-360px h-fit">
<video class="w-full h-240px rounded-t-8px object-cover" src="@/assets/video/star.mp4" autoplay loop />
<video class="w-full h-240px rounded-t-8px object-cover" src="../../../assets/video/star.mp4" autoplay loop />
<n-flex vertical :size="10" class="p-14px">
<p class="text-(16px [--text-color] font-bold)"> GitHub 为我们点亮星标</p>
<p class="text-(12px [--chat-text-color]) leading-5">
@ -40,7 +40,11 @@
<p class="text-(12px #13987f) cursor-pointer">分享您宝贵的建议</p>
</template>
<n-flex vertical class="w-360px h-fit">
<video class="w-full h-240px rounded-t-8px object-cover" src="@/assets/video/issue.mp4" autoplay loop />
<video
class="w-full h-240px rounded-t-8px object-cover"
src="../../../assets/video/issue.mp4"
autoplay
loop />
<n-flex vertical :size="10" class="p-14px">
<p class="text-(16px [--text-color] font-bold)"> GitHub 分享您宝贵的反馈</p>
<p class="text-(12px [--chat-text-color]) leading-5">

View File

@ -27,7 +27,7 @@
@click="handleVersatile('simple')"
:class="{ 'outline outline-2 outline-[--border-active-color] outline-offset': themes.versatile === 'simple' }"
class="w-108px h-84px flex-col-center gap-10px cursor-pointer rounded-8px bg-#f1f1f1">
<img class="size-34px" src="@/assets/img/hula_bg_l.png" alt="" />
<img class="size-34px" src="../../../assets/img/hula_bg_l.png" alt="" />
<p class="text-(12px [--chat-text-color])">极简素雅</p>
</div>
</n-flex>

View File

@ -62,7 +62,7 @@
import router from '@/router'
import { sideOptions } from './config.ts'
import { useSettingStore } from '@/stores/setting.ts'
import Foot from '@/views/homeWindow/more/settings/Foot.vue'
import Foot from '@/views/moreWindow/settings/Foot.vue'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
const settingStore = useSettingStore()
@ -93,6 +93,7 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
@use '@/styles/scss/global/variable' as *;
.left-bar {
@include menu-list();
background: var(--bg-left-menu);

View File

@ -0,0 +1,143 @@
<template>
<!-- 单独使用n-config-provider来包裹不需要主题切换的界面 -->
<n-config-provider :theme="lightTheme" data-tauri-drag-region class="login-box size-full rounded-8px select-none">
<!--顶部操作栏-->
<ActionBar :max-w="false" :shrink="false" />
<n-flex vertical :size="25" class="pt-80px">
<n-flex justify="center" align="center">
<span class="text-(24px #70938c) font-bold textFont">欢迎注册</span>
<img class="w-100px h-40px" src="@/assets/logo/hula.png" alt="" />
</n-flex>
<!-- 注册菜单 -->
<n-flex class="ma text-center h-full w-260px" vertical :size="16">
<n-input
maxlength="8"
minlength="1"
size="large"
v-model:value="info.name"
type="text"
:allow-input="noSideSpace"
:placeholder="namePH"
@focus="namePH = ''"
@blur="namePH = '输入HuLa昵称'"
clearable />
<n-input
size="large"
maxlength="12"
minlength="6"
v-model:value="info.account"
type="text"
:allow-input="noSideSpace"
:placeholder="accountPH"
@focus="accountPH = ''"
@blur="accountPH = '输入HuLa账号'"
clearable>
</n-input>
<n-input
maxlength="16"
minlength="6"
size="large"
v-model:value="info.password"
type="password"
:allow-input="noSideSpace"
:placeholder="passwordPH"
@focus="passwordPH = ''"
@blur="passwordPH = '输入HuLa密码'"
clearable />
<!-- 协议 -->
<n-flex justify="center" :size="6">
<n-checkbox v-model:checked="protocol" />
<div class="text-12px color-#909090 cursor-default lh-14px">
<span>已阅读并同意</span>
<span class="color-#13987f cursor-pointer">服务协议</span>
<span></span>
<span class="color-#13987f cursor-pointer">HuLa隐私保护指引</span>
</div>
</n-flex>
<n-button
:loading="loading"
:disabled="btnEnable"
class="w-full mt-8px mb-50px"
@click="register"
color="#13987f">
{{ btnText }}
</n-button>
</n-flex>
</n-flex>
<!-- 底部栏 -->
<n-flex class="text-(12px #909090)" :size="8" justify="center">
<span>Copyright © {{ currentYear - 1 }}-{{ currentYear }} HuLaSpark All Rights Reserved.</span>
</n-flex>
</n-config-provider>
</template>
<script setup lang="ts">
import { lightTheme } from 'naive-ui'
import apis from '@/services/apis.ts'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import dayjs from 'dayjs'
/** 账号信息 */
const info = ref({
account: '',
password: '',
name: ''
})
/** 协议 */
const protocol = ref(true)
const btnEnable = ref(false)
const loading = ref(false)
const namePH = ref('输入HuLa昵称')
const accountPH = ref('输入HuLa账号')
const passwordPH = ref('输入HuLa密码')
/** 登录按钮的文本内容 */
const btnText = ref('注册')
// 使day.js
const currentYear = dayjs().year()
/** 不允许输入空格 */
const noSideSpace = (value: string) => !value.startsWith(' ') && !value.endsWith(' ')
const register = async () => {
btnEnable.value = true
loading.value = true
btnText.value = '注册中...'
//
await apis
.register({ ...info.value } as User)
.then(() => {
window.$message.success('注册成功')
btnText.value = '注册'
})
.finally(() => {
loading.value = false
btnEnable.value = false
btnText.value = '注册'
})
}
watchEffect(() => {
btnEnable.value = !(info.value.account && info.value.password && protocol.value)
})
onMounted(async () => {
await getCurrentWebviewWindow().show()
})
onUnmounted(() => {})
</script>
<style scoped lang="scss">
@use '@/styles/scss/global/login-bg';
@use '@/styles/scss/login';
.textFont {
font-family: AliFangYuan, sans-serif !important;
}
</style>

View File

@ -25,14 +25,11 @@ export default defineConfig(({ mode }: ConfigEnv) => {
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "./src/styles/scss/global/variable.scss" as *;' // 加载全局样式使用scss特性
api: 'modern-compiler',
additionalData: '@use "@/styles/scss/global/variable.scss" as *;' // 加载全局样式使用scss特性
}
}
},
define: {
// enable hydration mismatch details in production build 3.4新增水化不匹配的警告
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true'
},
plugins: [
/**
* vue3.5.0
@ -99,7 +96,7 @@ export default defineConfig(({ mode }: ConfigEnv) => {
// “/api” 以及前置字符串会被替换为真正域名
target: config.VITE_SERVICE_URL, // 请求域名
changeOrigin: true, // 是否跨域
rewrite: (path) => path.replace(/^\/api/, '/api')
rewrite: (path) => path.replace(/^\/api/, '')
}
},
hmr: true, // 热更新