mirror of
https://gitee.com/HuLaSpark/HuLa.git
synced 2024-11-29 10:18:35 +08:00
fix(component): 🐛修复一些已知问题 (#47)
This commit is contained in:
commit
0e0955f3ec
@ -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"
|
||||
# 项目标题
|
||||
|
@ -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
2
.gitignore
vendored
@ -13,8 +13,6 @@ dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
|
@ -7,5 +7,6 @@
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="ts-external-references" level="project" />
|
||||
</component>
|
||||
</module>
|
6
.idea/jsLibraryMappings.xml
Normal file
6
.idea/jsLibraryMappings.xml
Normal 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>
|
14
.idea/libraries/ts_external_references.xml
Normal file
14
.idea/libraries/ts_external_references.xml
Normal 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
18
.vscode/launch.json
vendored
Normal 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
13
.vscode/tasks.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -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)
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
12
package.json
12
package.json
@ -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",
|
||||
|
4880
pnpm-lock.yaml
4880
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) => {
|
||||
// 设置定时器,在1600毫秒后更新悬浮气泡的key值
|
||||
|
@ -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">群聊成员 {{ userList.length }}</span>
|
||||
<span class="text-14px">在线群聊成员 {{ 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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -221,5 +221,5 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'style';
|
||||
@use 'style';
|
||||
</style>
|
||||
|
@ -1,3 +1,5 @@
|
||||
@use '@/styles/scss/global/variable.scss' as *;
|
||||
|
||||
.resizable {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
@ -355,7 +355,7 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../style';
|
||||
@use '../style';
|
||||
|
||||
.setting-item {
|
||||
left: 24px;
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -242,6 +242,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@/styles/scss/global/variable.scss' as *;
|
||||
.float-block {
|
||||
--y: 0;
|
||||
--height: 70px;
|
||||
|
@ -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('退出账号失败')
|
||||
})
|
||||
}
|
||||
}
|
||||
])
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -42,5 +42,5 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'style';
|
||||
@use 'style';
|
||||
</style>
|
||||
|
@ -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
16
src/model/User.ts
Normal 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
|
||||
}
|
@ -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>
|
@ -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')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
// 判断是否需要添加请求体
|
||||
|
@ -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 {
|
||||
|
@ -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 = {
|
||||
|
@ -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`
|
||||
}
|
||||
|
@ -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 过期
|
||||
|
@ -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 })
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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 = ''
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { ref } from 'vue'
|
||||
import apis from '@/services/apis'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { UserInfoType } from '@/services/types'
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import wsIns from '@/services/webSocket.ts'
|
||||
import { WsRequestMsgType } from '@/utils/wsType'
|
||||
|
||||
|
8
src/typings/components.d.ts
vendored
8
src/typings/components.d.ts
vendored
@ -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']
|
||||
|
@ -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))
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
@ -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>
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
|
||||
/**! 创建新窗口然后需要通信传递数据时候需要进行提交一次页面创建成功的事件,否则会接收不到数据 */
|
||||
|
@ -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(() => {
|
||||
|
@ -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 这里的id暂时赋值给uid,因为后端没有统一返回uid,待后端调整
|
||||
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 退出账号后不知道为什么请求头的token被移除了导致checkToken请求401
|
||||
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>
|
||||
|
||||
|
23
src/views/loginWindow/Proxy.vue
Normal file
23
src/views/loginWindow/Proxy.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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">
|
@ -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>
|
@ -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);
|
143
src/views/registerWindow/index.vue
Normal file
143
src/views/registerWindow/index.vue
Normal 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>
|
@ -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, // 热更新
|
||||
|
Loading…
Reference in New Issue
Block a user