!21 发布2.1.0版本

Merge pull request !21 from nongyehong/dev
This commit is contained in:
nongyehong 2024-09-13 12:04:30 +00:00 committed by Gitee
commit 7938f35fea
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
64 changed files with 2430 additions and 1086 deletions

10
.env.production Normal file
View File

@ -0,0 +1,10 @@
# 后端服务地址
VITE_SERVICE_URL="http://127.0.0.1:9190"
# websocket服务地址
VITE_WEBSOCKET_URL="ws://127.0.0.1:8090"
# 项目标题
VITE_APP_TITLE="HuLa—IM"
# 项目名称
VITE_APP_NAME="HuLa-IM-Tauri"
# gitee token
VITE_GITEE_TOKEN="0312a213a6b6882beb96f487e75661a6"

1
.gitignore vendored
View File

@ -24,4 +24,3 @@ dist-ssr
*.sw?
src-tauri/target
.env.production

View File

@ -1,3 +1,33 @@
# [2.1.0](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v2.0.0...v2.1.0) (2024-09-13)
### Bug Fixes
* **component:** :bug: 修复windows上的样式问题 ([bb6a9d4](https://github.com/nongyehong/HuLa-IM-Tauri/commit/bb6a9d440db4777989d9a922a5135350e2dbf894))
* **component:** :bug: 修复系统托盘功能和一些样式问题 ([18277ef](https://github.com/nongyehong/HuLa-IM-Tauri/commit/18277ef0f1ce286b77b91dbc8c6ea8a628eba7d3))
* **style:** :bug: 统一修复svg点击时有轮廓问题 ([ce68fa1](https://github.com/nongyehong/HuLa-IM-Tauri/commit/ce68fa134368b34802d5b101a1f98a2493f7120b))
* **system:** :bug: 修复mac端右键菜单透明度问题 ([39d795f](https://github.com/nongyehong/HuLa-IM-Tauri/commit/39d795ff655afd699340d3021a0b471c3060b11c))
* **system:** :bug: 修复win下窗口高度不一致问题 ([30bb3de](https://github.com/nongyehong/HuLa-IM-Tauri/commit/30bb3de5d10ffea949c32b505f6501b3f7d0f573))
### Features
* chatbot删除全部会话功能和右键菜单重命名 ([3426c5f](https://github.com/nongyehong/HuLa-IM-Tauri/commit/3426c5f24fafe66c3543ee8f4172d2dae05740e4))
* **component:** :sparkles: 新增插件功能(Bate) ([392b7c9](https://github.com/nongyehong/HuLa-IM-Tauri/commit/392b7c99bd38fd2f298e7732499dc7510e4d286a))
### Performance Improvements
* :zap: 优化mac标签栏 ([a7c587d](https://github.com/nongyehong/HuLa-IM-Tauri/commit/a7c587d74b771e32e3b61eaef2ba5c902c0e4f6f))
* **component:** :zap: 升级插件版本内容及其样式 ([8d65ca1](https://github.com/nongyehong/HuLa-IM-Tauri/commit/8d65ca198fa8a01252e0dc7f07f4bd6c796dbfe1))
### BREAKING CHANGES
* **system:** 新增mac端弹出框的关闭按钮
# [2.0.0](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v1.6.0...v2.0.0) (2024-08-15)

View File

@ -7,7 +7,7 @@
<title>HuLa</title>
<!--引入iconpark图标库-->
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_118.ee039811e5b75b41f6c09e4bb8e9edcd.js"></script>
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_122.6a2196c3637253b659223f2334a8756e.js"></script>
</head>
<body>

View File

@ -2,7 +2,7 @@
"name": "hula-im-tauri",
"private": true,
"type": "module",
"version": "v2.0.0",
"version": "2.1.0",
"license": "Apache-2.0",
"engines": {
"node": ">=18.12.0",
@ -53,8 +53,8 @@
"pinia": "^2.2.1",
"pinia-plugin-persistedstate": "^3.2.1",
"pinia-shared-state": "^0.5.1",
"vue": "^3.4.37",
"vue-draggable-plus": "^0.4.1",
"vue": "^3.5.4",
"vue-draggable-plus": "^0.5.3",
"vue-router": "^4.4.2"
},
"devDependencies": {
@ -67,11 +67,11 @@
"@types/node": "^20.14.14",
"@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "^7.15.0",
"@unocss/preset-uno": "^0.61.9",
"@unocss/reset": "^0.61.9",
"@unocss/transformer-directives": "^0.61.9",
"@unocss/transformer-variant-group": "^0.61.9",
"@unocss/vite": "^0.61.9",
"@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",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"@vueuse/core": "^10.11.0",
@ -91,10 +91,10 @@
"sass": "1.77.6",
"sass-loader": "^14.2.1",
"typescript": "^5.5.3",
"unplugin-auto-import": "^0.17.8",
"unplugin-vue-components": "^0.26.0",
"vite": "5.4.0",
"vue-tsc": "^2.0.29"
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4",
"vite": "5.4.3",
"vue-tsc": "^2.1.4"
},
"config": {
"commitizen": {

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
public/emoji/bug.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
public/emoji/comet.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
public/emoji/fire.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
public/emoji/gear.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
public/emoji/lipstick.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/emoji/memo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/emoji/package.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/emoji/robot.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
public/emoji/rocket.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

BIN
public/emoji/test-tube.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

2
src-tauri/Cargo.lock generated
View File

@ -2018,7 +2018,7 @@ checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
[[package]]
name = "hula"
version = "2.0.0"
version = "2.1.0"
dependencies = [
"base64 0.22.1",
"lazy_static",

View File

@ -1,6 +1,6 @@
[package]
name = "hula"
version = "2.0.0"
version = "2.1.0"
description = "hula"
authors = ["nongyehong"]
license = ""

View File

@ -1,6 +1,6 @@
{
"productName": "HuLa",
"version": "2.0.0",
"version": "2.1.0",
"identifier": "com.tauri.build",
"build": {
"beforeDevCommand": "pnpm dev",

BIN
src/assets/img/win.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -76,8 +76,6 @@ const commonTheme: GlobalThemeOverrides = {
},
Tabs: {
tabTextColorSegment: '#707070',
tabTextColorActiveSegment: '#13987f',
tabTextColorHoverSegment: '#13987f',
tabPaddingMediumSegment: '4px'
},
Popover: {

View File

@ -120,10 +120,9 @@ import { useChatStore } from '@/stores/chat.ts'
const { handleMsgClick } = useMessage()
const globalStore = useGlobalStore()
const chatStore = useChatStore()
const props = defineProps<{
const { content } = defineProps<{
content: any
}>()
const { content } = toRefs(props)
const item = computed(() => {
return useUserInfo(content.value.uid).value
})

View File

@ -1,7 +1,7 @@
<template>
<!-- 输入框 -->
<ContextMenu class="w-full h-110px" @select="$event.click()" :menu="menuList">
<n-scrollbar style="max-height: 110px">
<n-scrollbar style="max-height: 100px">
<div
id="message-input"
ref="messageInputDom"
@ -12,6 +12,7 @@
@keydown.exact.enter="inputKeyDown"
@keydown.exact.meta.enter="inputKeyDown"
@keydown.exact.ctrl.enter="inputKeyDown"></div>
<!-- TODO 这里的在win上会有延迟显示的bug (nyh -> 2024-09-01 23:40:44) -->
<span
v-if="isEntering"
@click.stop="messageInputDom.focus()"
@ -55,17 +56,8 @@
</n-virtual-list>
</div>
<!-- 发送按钮 -->
<!-- 发送按钮 TODO 建议不要放在外面会影响视觉效果可以放在发送按钮里面做提示发送按钮需要修改一下大小 (nyh -> 2024-09-01 23:41:34) -->
<n-flex align="center" justify="space-between" :size="12">
<n-flex align="center" :size="4" class="text-(12px #777) tracking-1">
<svg class="size-12px"><use href="#Enter"></use></svg>
发送/
<n-flex align="center" :size="0">
{{ type() === 'macos' ? MacOsKeyEnum['⌘'] : WinKeyEnum.ctrl }}
<svg class="size-12px"><use href="#Enter"></use></svg>
</n-flex>
换行
</n-flex>
<n-config-provider :theme="lightTheme">
<n-button-group size="small" class="pr-20px">
<n-button
@ -90,6 +82,21 @@
<svg @click="arrow = false" v-else class="w-22px h-22px mt-2px outline-none">
<use href="#up"></use>
</svg>
<template #action>
<n-flex
justify="center"
align="center"
:size="4"
class="text-(12px #777) cursor-default tracking-1 select-none">
<svg class="size-12px"><use href="#Enter"></use></svg>
发送/
<n-flex align="center" :size="0">
{{ type() === 'macos' ? MacOsKeyEnum['⌘'] : WinKeyEnum.ctrl }}
<svg class="size-12px"><use href="#Enter"></use></svg>
</n-flex>
换行
</n-flex>
</template>
</n-popselect>
</n-config-provider>
</template>

View File

@ -1,8 +1,6 @@
<template>
<!-- 底部栏 -->
<main
style="box-shadow: var(--shadow-enabled) -4px 4px var(--box-shadow-color)"
class="size-full relative z-10 bg-[--right-bg-color] border-t-(1px solid [--line-color]) color-[--icon-color]">
<main class="size-full relative z-10 bg-[--right-bg-color] border-t-(1px solid [--line-color]) color-[--icon-color]">
<!-- 输入框顶部选项栏 -->
<n-flex align="center" justify="space-between" class="p-[10px_22px_5px] select-none">
<n-flex align="center" :size="0" class="input-options">
@ -159,7 +157,6 @@ onMounted(() => {
width: 22px;
height: 22px;
cursor: pointer;
outline: none;
&:hover {
color: #13987f;
}

View File

@ -1,7 +1,6 @@
<template>
<!-- 顶部操作栏和显示用户名 -->
<main
style="box-shadow: var(--shadow-enabled) 4px 4px var(--box-shadow-color)"
class="relative z-30 flex-y-center border-b-(1px solid [--line-color]) justify-between p-[6px_20px_12px] select-none">
<n-flex align="center">
<span class="color-[--text-color]">{{ activeItem.name }}</span>
@ -62,9 +61,9 @@
</nav>
<!-- 侧边选项栏 -->
<transition name="sidebar">
<Transition name="sidebar">
<div v-if="sidebarShow" style="border: 1px solid rgba(90, 90, 90, 0.1)" class="sidebar">
<div class="setting-item flex-col-y-center">
<div class="box-item flex-col-y-center">
<div class="flex-between-center">
<p>设为置顶</p>
<n-switch size="small" />
@ -76,24 +75,24 @@
</div>
</div>
<div class="setting-item">
<div class="box-item">
<div class="flex-between-center">
<p>屏蔽此人</p>
<n-switch size="small" />
</div>
</div>
<div class="setting-item cursor-pointer" @click="handleDelete('chat-history')">
<div class="box-item cursor-pointer" @click="handleDelete('chat-history')">
<p>删除聊天记录</p>
</div>
<div class="setting-item flex-x-center cursor-pointer" @click="handleDelete('friends')">
<div class="box-item flex-x-center cursor-pointer" @click="handleDelete('friends')">
<p class="color-#d03553">删除好友</p>
</div>
<p class="m-[0_auto] text-(12px #13987f) mt-20px cursor-pointer">被骚扰了?&nbsp;&nbsp;举报该用户</p>
</div>
</transition>
</Transition>
</main>
<!-- 弹出框 -->

View File

@ -1,7 +1,7 @@
<template>
<!-- 头部 -->
<ChatHeader :active-item="activeItemRef as any" />
<n-flex :size="0" class="h-full">
<n-flex :class="{ 'shadow-inner': page.shadow }" :size="0" class="h-full">
<n-flex vertical :size="0" class="flex-1 relative">
<!-- 中间聊天框内容 -->
<ChatMain :active-item="activeItemRef as any" />
@ -15,7 +15,11 @@
import { MockItem } from '@/services/types.ts'
import { listen } from '@tauri-apps/api/event'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { setting } from '@/stores/setting.ts'
import { storeToRefs } from 'pinia'
const settingStore = setting()
const { page } = storeToRefs(settingStore)
const appWindow = WebviewWindow.getCurrent()
const { activeItem } = defineProps<{
activeItem?: MockItem

View File

@ -57,8 +57,7 @@
</template>
<script setup lang="ts">
import { getAllTypeEmojis } from '@/utils/Emoji.ts'
import { history } from '@/stores/history.ts'
import { storeToRefs } from 'pinia'
import { useHistoryStore } from '@/stores/history.ts'
type EmojiType = {
expressionEmojis: EmojiItem
@ -70,8 +69,7 @@ interface EmojiItem {
value: any[]
}
const historyStore = history()
const { emoji } = storeToRefs(historyStore)
const { emoji, setEmoji } = useHistoryStore()
const activeIndex = ref(0)
const emit = defineEmits(['emojiHandle'])
@ -102,7 +100,7 @@ const emojiRef = reactive<{
allEmoji: EmojiType
}>({
chooseItem: '',
historyList: emoji.value,
historyList: emoji,
allEmoji: emojiObj.value
})
@ -121,7 +119,7 @@ const chooseEmoji = (item: string) => {
if (emojiRef.historyList.length > 18) {
emojiRef.historyList.splice(18) // 18
}
historyStore.setEmoji([...emojiRef.historyList])
setEmoji([...emojiRef.historyList])
emit('emojiHandle', item)
return item
}

View File

@ -46,7 +46,7 @@
</div>
</template>
<!-- 是否退到托盘提示框 -->
<n-modal v-if="!tips.notTips" v-model:show="tipsRef.show" class="rounded-8px">
<n-modal v-if="!tips.notTips && osType === 'windows'" v-model:show="tipsRef.show" class="rounded-8px">
<div class="bg-[--bg-popover] w-290px h-full p-6px box-border flex flex-col">
<svg @click="tipsRef.show = false" class="size-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
@ -80,7 +80,7 @@
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import Mitt from '@/utils/Bus'
import { useWindow } from '@/hooks/useWindow.ts'
import { alwaysOnTop } from '@/stores/alwaysOnTop.ts'
import { useAlwaysOnTopStore } from '@/stores/alwaysOnTop.ts'
import { setting } from '@/stores/setting.ts'
import { emit, listen } from '@tauri-apps/api/event'
import { CloseBxEnum, EventEnum, MittEnum } from '@/enums'
@ -90,30 +90,23 @@ import { exit } from '@tauri-apps/plugin-process'
import { type } from '@tauri-apps/plugin-os'
const appWindow = WebviewWindow.getCurrent()
/**
* 新版defineProps可以直接结构 { minW, maxW, closeW } 如果需要使用默认值withDefaults的时候使用新版解构方式会报错
* @description W结尾为窗口图标是否显示 shrink表示是否收缩图标 shrinkStatus表示是否收缩状态
* */
const props = withDefaults(
defineProps<{
minW?: boolean
maxW?: boolean
closeW?: boolean
shrink?: boolean
topWinLabel?: string
currentLabel?: string
shrinkStatus?: boolean
}>(),
{
minW: true,
maxW: true,
closeW: true,
shrink: true,
shrinkStatus: true
}
)
const { minW, maxW, closeW, topWinLabel, shrinkStatus } = toRefs(props)
const alwaysOnTopStore = alwaysOnTop()
const {
topWinLabel,
minW = true,
maxW = true,
closeW = true,
shrink = true,
shrinkStatus = true
} = defineProps<{
minW?: boolean
maxW?: boolean
closeW?: boolean
shrink?: boolean
topWinLabel?: string
currentLabel?: string
shrinkStatus?: boolean
}>()
const { getWindowTop, setWindowTop } = useAlwaysOnTopStore()
const settingStore = setting()
const { tips, escClose } = storeToRefs(settingStore)
const { resizeWindow } = useWindow()
@ -126,8 +119,8 @@ const tipsRef = reactive({
const windowMaximized = ref(false)
//
const alwaysOnTopStatus = computed(() => {
if (topWinLabel.value === void 0) return false
return alwaysOnTopStore.getWindowTop(topWinLabel.value)
if (topWinLabel === void 0) return false
return getWindowTop(topWinLabel)
})
/** 判断当前是windows还是mac系统 */
const osType = ref()
@ -140,7 +133,6 @@ watchEffect(() => {
listen(EventEnum.LOGOUT, async () => {
/** 退出账号前把窗口全部关闭 */
if (appWindow.label !== 'login') {
console.log('logout')
await appWindow.close()
}
})
@ -148,7 +140,7 @@ watchEffect(() => {
await exit(0)
})
if (escClose.value) {
if (escClose.value && type() === 'windows') {
window.addEventListener('keydown', (e) => isEsc(e))
} else {
window.removeEventListener('keydown', (e) => isEsc(e))
@ -167,8 +159,8 @@ const restoreWindow = async () => {
/** 收缩窗口 */
const shrinkWindow = async () => {
/**使用mitt给兄弟组件更新*/
Mitt.emit(MittEnum.SHRINK_WINDOW, shrinkStatus.value)
if (shrinkStatus.value) {
Mitt.emit(MittEnum.SHRINK_WINDOW, shrinkStatus)
if (shrinkStatus) {
await resizeWindow('home', 310, 700)
} else {
await resizeWindow('home', 960, 700)
@ -177,9 +169,9 @@ const shrinkWindow = async () => {
/** 设置窗口置顶 */
const handleAlwaysOnTop = async () => {
if (topWinLabel.value !== void 0) {
if (topWinLabel !== void 0) {
const isTop = !alwaysOnTopStatus.value
alwaysOnTopStore.setWindowTop(topWinLabel.value, isTop)
setWindowTop(topWinLabel, isTop)
await appWindow.setAlwaysOnTop(isTop)
}
}

View File

@ -97,7 +97,11 @@ export enum StoresEnum {
/** 历史内容 */
HISTORY = 'history',
/** 聊天列表 */
CHAT_LIST = 'chatList'
CHAT_LIST = 'chatList',
/** 插件列表 */
PLUGINS = 'plugins',
/** 侧边栏头部菜单栏 */
MENUTOP = 'menuTop'
}
/**
@ -257,6 +261,8 @@ export enum WinKeyEnum {
/** 插件状态 */
export enum PluginEnum {
/** 已内置 */
BUILTIN,
/** 已安装 */
INSTALLED,
/** 下载中 */

View File

@ -259,7 +259,6 @@ export const useMsgInput = (messageInputDom: Ref) => {
/** input的keydown事件 */
const inputKeyDown = (e: KeyboardEvent) => {
console.log(chat.value.sendKey)
const isWindows = type() === 'windows'
const isEnterKey = e.key === 'Enter'
const isCtrlOrMetaKey = isWindows ? e.ctrlKey : e.metaKey

View File

@ -42,7 +42,11 @@
</n-input>
<!-- 添加面板 -->
<n-popover v-model:show="addPanels.show" style="padding: 0" :show-arrow="false" trigger="click">
<n-popover
v-model:show="addPanels.show"
style="padding: 0; background: transparent"
:show-arrow="false"
trigger="click">
<template #trigger>
<n-button size="small" secondary style="padding: 0 5px">
<template #icon>

View File

@ -1,9 +1,9 @@
<template>
<div class="flex-1 mt-20px flex-col-x-center justify-between" data-tauri-drag-region>
<div ref="actionList" class="flex-1 mt-20px flex-col-x-center justify-between" data-tauri-drag-region>
<!-- 上部分操作栏 -->
<header class="flex-col-x-center gap-10px color-[--icon-color]">
<header ref="header" class="flex-col-x-center gap-10px color-[--icon-color]">
<div
v-for="(item, index) in itemsTop"
v-for="(item, index) in menuTop"
:key="index"
:class="[
{ active: activeUrl === item.url && item.url !== 'dynamic' },
@ -16,7 +16,7 @@
<n-badge :max="99" :value="item.badge">
<svg class="size-22px" @click="tipShow = false">
<use
:href="`#${activeUrl === item.url || openWindowsList.has(item.url) ? item.iconAction : item.icon}`"></use>
:href="`#${activeUrl === item.url || openWindowsList.has(item.url) ? item.iconAction || item.icon : item.icon}`"></use>
</svg>
</n-badge>
</template>
@ -25,8 +25,8 @@
<!-- 该选项有提示时展示 -->
<n-popover style="padding: 12px" v-else-if="item.tip" trigger="manual" v-model:show="tipShow" placement="right">
<template #trigger>
<n-badge :max="99" :value="item.badge" dot :show="dotShow">
<svg class="size-22px" @click="handleTipShow">
<n-badge :max="99" :value="item.badge" dot :show="item.dot">
<svg class="size-22px" @click="handleTipShow(item)">
<use
:href="`#${activeUrl === item.url || openWindowsList.has(item.url) ? item.iconAction : item.icon}`"></use>
</svg>
@ -34,7 +34,7 @@
</template>
<n-flex align="center" justify="space-between">
<p class="select-none">{{ item.tip }}</p>
<svg @click="handleTipShow" class="size-12px cursor-pointer"><use href="#close"></use></svg>
<svg @click="handleTipShow(item)" class="size-12px cursor-pointer"><use href="#close"></use></svg>
</n-flex>
</n-popover>
<!-- 该选项无提示时展示 -->
@ -47,12 +47,24 @@
</div>
<!-- (独立)菜单选项 -->
<n-popover style="padding: 8px; margin-left: 4px" :show-arrow="false" trigger="hover" placement="right">
<n-popover
style="padding: 8px; margin-left: 4px; background: var(--bg-setting-item)"
:show-arrow="false"
trigger="hover"
placement="right">
<template #trigger>
<svg class="size-22px top-action">
<use href="#menu"></use>
</svg>
</template>
<div v-if="excessItems.length">
<div
v-for="(item, index) in excessItems as any"
:key="'excess-' + index"
class="p-[6px_10px] rounded-4px cursor-pointer hover:bg-[--setting-item-line]">
{{ item.title }}
</div>
</div>
<n-flex
@click="menuShow = true"
class="p-[6px_10px] rounded-4px cursor-pointer hover:bg-[--setting-item-line]"
@ -111,7 +123,11 @@
</div>
<!-- 更多选项面板 -->
<n-popover v-model:show="settingShow" style="padding: 0" :show-arrow="false" trigger="click">
<n-popover
v-model:show="settingShow"
style="padding: 0; background: transparent"
:show-arrow="false"
trigger="click">
<template #trigger>
<svg
:class="{ 'color-#13987f': settingShow }"
@ -134,30 +150,65 @@
</footer>
</div>
<DefinePlugins :show="menuShow" @close="(e) => (menuShow = e)" />
<DefinePlugins v-model="menuShow" />
</template>
<script setup lang="ts">
import { itemsBottom, itemsTop, moreList } from '../config.tsx'
import { itemsBottom, moreList } from '../config.tsx'
import { leftHook } from '../hook.ts'
import DefinePlugins from './DefinePlugins.vue'
import DefinePlugins from './definePlugins/index.vue'
import { useMenuTopStore } from '@/stores/menuTop.ts'
import { PluginEnum } from '@/enums'
const { menuTop } = useMenuTopStore()
// const headerRef = useTemplateRef('header')
// const actionListRef = useTemplateRef('actionList')
const excessItems = ref([]) //
const menuShow = ref(false)
const dotShow = ref(false)
const { activeUrl, openWindowsList, settingShow, tipShow, pageJumps } = leftHook()
const handleTipShow = () => {
const handleTipShow = (item: any) => {
tipShow.value = false
dotShow.value = false
item.dot = false
}
onMounted(() => {
if (tipShow.value) {
dotShow.value = true
menuTop.filter((item) => {
if (item.state !== PluginEnum.BUILTIN) {
item.dot = true
}
})
}
/** 十秒后关闭提示 */
setTimeout(() => {
tipShow.value = false
}, 5000)
// // ResizeObserver headerRef
// const resizeObserver = new ResizeObserver((entries) => {
// for (let entry of entries) {
// console.log(entry.contentRect.height)
// // if (entry.contentRect.height > 220) {
// // //
// // const itemsToMove = Math.floor(itemsTop.value.length / 2) //
// // excessItems.value = itemsTop.value.splice(-itemsToMove, itemsToMove)
// // }
// }
// })
//
// // headerRef
// if (headerRef.value) {
// resizeObserver.observe(headerRef.value)
// }
//
// if (actionListRef.value) {
// resizeObserver.observe(actionListRef.value)
// }
//
// //
// onUnmounted(() => {
// resizeObserver.disconnect()
// })
})
</script>
<style lang="scss" scoped>

View File

@ -1,283 +0,0 @@
<template>
<!-- 弹出框 -->
<n-modal v-model:show="isShow" :mask-closable="false" class="w-450px border-rd-8px">
<div class="bg-[--bg-popover] h-full box-border flex flex-col">
<!-- 顶部图片加上操作栏 -->
<div class="h-140px relative w-full p-6px box-border">
<img
class="absolute blur-6px rounded-t-6px z-1 top-0 left-0 w-full h-140px object-cover"
src="@/assets/img/dispersion-bg.png"
alt="" />
<img
class="absolute rounded-t-6px z-2 top-0 left-0 w-full h-140px object-cover"
src="@/assets/img/dispersion-bg.png"
alt="" />
<div
v-if="type() === 'macos'"
@click="handleClose"
class="mac-close z-10 relative size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
<svg
v-if="type() === 'windows'"
@click="handleClose"
class="z-10 w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
</div>
<n-flex :size="4" align="center" class="p-18px">
<p class="text-(16px [--text-color])">插件管理</p>
<div class="ml-6px p-[4px_8px] size-fit bg-[--bate-bg] rounded-8px text-(12px [--bate-color] center)">Beta</div>
</n-flex>
<n-scrollbar style="max-height: 320px">
<n-flex :size="26" class="z-10 p-[4px_18px] bg-#cc w-full h-280px">
<template v-for="(plugin, index) in plugins" :key="index">
<n-flex :size="12">
<Transition name="fade" mode="out-in">
<!-- 未安装和下载中状态 -->
<n-flex
v-if="plugin.state === PluginEnum.NOT_INSTALLED || plugin.state === PluginEnum.DOWNLOADING"
vertical
justify="center"
align="center"
:size="8"
class="box bg-#f1f1f1">
<svg class="size-38px color-#555"><use :href="`#${plugin.icon}`"></use></svg>
<p class="text-(12px #666)">{{ plugin.title }}</p>
<n-flex
@click="handleState(plugin)"
class="relative rounded-22px border-(1px solid #4C77BD)"
:class="[
plugin.state === PluginEnum.DOWNLOADING ? 'downloading' : 'bg-#e0e9fc size-fit p-[4px_8px]'
]">
<div
:style="{
width: plugin.state === PluginEnum.DOWNLOADING ? `${plugin.progress * 0.8}px` : 'auto'
}"
:class="[
plugin.progress < 100 ? 'rounded-l-24px rounded-r-0' : 'rounded-24px',
plugin.progress > 0 ? 'h-18px border-(1px solid transparent)' : 'h-20px'
]"
v-if="plugin.state === PluginEnum.DOWNLOADING"
class="bg-#8CA9F4">
<p class="absolute-center text-(12px #4C77BD)">{{ plugin.progress }}%</p>
</div>
<p v-else class="text-(12px #4C77BD center)">安装</p>
</n-flex>
<!-- 闪光效果 -->
<div class="flash"></div>
</n-flex>
<!-- 可卸载状态 -->
<n-flex v-else vertical justify="center" align="center" :size="8" class="box colorful">
<svg class="size-38px color-#555"><use :href="`#${plugin.iconActive || plugin.icon}`"></use></svg>
<p class="text-(12px #666)">{{ plugin.title }}</p>
<n-flex
v-if="plugin.state === PluginEnum.UNINSTALLING"
class="relative rounded-22px border-(1px solid #c14053) bg-#f6dfe3 p-[4px_8px]">
<p class="text-(12px #c14053 center)">卸载中</p>
</n-flex>
<n-flex
v-else
class="relative rounded-22px border-(1px solid #4C77BD) bg-#e0e9fc size-fit p-[4px_8px]">
<p class="text-(12px #4C77BD center)">{{ plugin.version }}</p>
</n-flex>
<!-- 闪光效果 -->
<div class="flash"></div>
<!-- 插件操作 -->
<n-popover
v-model:show="plugin.isAction"
style="padding: 0"
:show-arrow="false"
trigger="click"
placement="bottom-end">
<template #trigger>
<svg class="absolute color-#666 right-0 top-0 size-18px rotate-90"><use href="#more"></use></svg>
</template>
<div @click.stop="plugin.isAction = false" class="action-item">
<div class="menu-list">
<div @click="handleAdd(index)" class="menu-item">
<svg class="color-#4C77BD"><use href="#add"></use></svg>
<p class="text-#4C77BD">添加侧边栏</p>
</div>
<div @click="handleDelete(index)" class="menu-item">
<svg class="color-#c14053"><use href="#reduce"></use></svg>
<p class="text-#c14053">删除</p>
</div>
<div @click="handleUnload(index)" class="menu-item">
<svg><use href="#delete"></use></svg>
<p>卸载</p>
</div>
</div>
</div>
</n-popover>
</n-flex>
</Transition>
</n-flex>
</template>
</n-flex>
</n-scrollbar>
</div>
</n-modal>
</template>
<script setup lang="ts">
import { type } from '@tauri-apps/plugin-os'
import { PluginEnum } from '@/enums'
import { itemsTop } from '../config.tsx'
// todo: OPT.L.Common[]
type Plugins = {
title: string
state: PluginEnum
version: string
isAction: boolean
icon: string
iconActive: string
}
const props = defineProps<{
show: boolean
}>()
const { show } = toRefs(props)
const isShow = ref(show.value)
const plugins = ref<Plugins[]>([
{
title: 'HuLa云音乐',
state: PluginEnum.NOT_INSTALLED,
version: 'v1.0.0-Bate',
isAction: false,
icon: 'Music'
},
{
title: 'HuLa AI',
state: PluginEnum.NOT_INSTALLED,
version: 'v2.0.0-Bate',
isAction: false,
icon: 'robot',
icon_active: 'robot-action'
}
])
const emits = defineEmits(['close'])
watch(show, (newVal) => {
isShow.value = newVal
})
const handleClose = () => {
isShow.value = false
emits('close', isShow.value)
}
const handleState = (plugin) => {
if (plugin.state === PluginEnum.INSTALLED) return
plugin.state = PluginEnum.DOWNLOADING
plugin.progress = 0
const interval = setInterval(() => {
if (plugin.progress < 100) {
plugin.progress += 10
} else {
clearInterval(interval)
plugin.state = PluginEnum.INSTALLED
}
}, 500)
}
const handleUnload = (index: number) => {
plugins.value.filter((item, i) => {
if (i === index) {
item.state = PluginEnum.UNINSTALLING
setTimeout(() => {
handleDelete(index)
item.state = PluginEnum.NOT_INSTALLED
}, 2000)
}
})
}
const handleDelete = (index) => {
plugins.value.filter((item, i) => {
if (i === index) {
// itemsTop item.icon
const itemIndex = itemsTop.value.findIndex((topItem) => topItem.icon === item.icon)
if (itemIndex !== -1) {
itemsTop.value.splice(itemIndex, 1)
}
}
})
}
const handleAdd = (index) => {
plugins.value.filter((item, i) => {
if (i === index) {
// itemsTop
const itemIndex = itemsTop.value.findIndex((topItem) => topItem.icon === item.icon)
if (itemIndex !== -1) {
return
}
itemsTop.value.push(item)
}
})
}
</script>
<style scoped lang="scss">
.box {
@apply relative select-none custom-shadow cursor-pointer size-fit w-100px h-100px rounded-8px overflow-hidden;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.2));
transition: all 0.2s;
.flash {
position: absolute;
left: -130%;
top: 0;
width: 100px;
height: 100px;
background-image: linear-gradient(90deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
transform: skew(-30deg);
pointer-events: none;
}
&:hover .flash {
left: 130%;
transition: all 0.8s ease-in-out;
}
}
.downloading {
width: 80px;
background: #f1f1f1;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.action-item {
@include menu-item-style();
left: -80px;
@include menu-list();
}
.colorful {
background: linear-gradient(45deg, #fdcbf1 0%, #fdcbf1 1%, #e6dee9 100%);
}
</style>

View File

@ -0,0 +1,279 @@
<template>
<div>
<n-scrollbar style="max-height: 280px">
<n-flex :size="26" class="z-10 p-[18px_18px_36px_18px] box-border w-full">
<template v-for="(plugin, index) in plugins" :key="index">
<Transition name="fade" mode="out-in">
<!-- 未安装和下载中状态 -->
<n-flex
v-if="plugin.state === PluginEnum.NOT_INSTALLED || plugin.state === PluginEnum.DOWNLOADING"
vertical
justify="center"
align="center"
:size="8"
:class="{ 'filter-shadow': page.shadow }"
class="box bg-[--info-hover]">
<svg class="size-38px color-#777"><use :href="`#${plugin.icon}`"></use></svg>
<p class="text-(12px #666)">{{ plugin.title }}</p>
<!-- 在下载中进度条 -->
<n-flex
@click="handleState(plugin)"
class="relative rounded-22px border-(1px solid #4C77BD)"
:class="[
plugin.state === PluginEnum.DOWNLOADING ? 'downloading' : 'bg-[--progress-bg] size-fit p-[4px_8px]'
]">
<div
:style="{
width: plugin.state === PluginEnum.DOWNLOADING ? `${plugin?.progress * 0.8}px` : 'auto'
}"
:class="[
plugin?.progress < 100 ? 'rounded-l-24px rounded-r-0' : 'rounded-24px',
plugin?.progress > 0 ? 'h-18px border-(1px solid transparent)' : 'h-20px'
]"
v-if="plugin.state === PluginEnum.DOWNLOADING"
class="bg-#8CA9F4">
<p class="absolute-center text-(12px #4C77BD)">{{ plugin?.progress }}%</p>
</div>
<p v-else class="text-(12px #4C77BD center)">安装</p>
</n-flex>
<!-- 闪光效果 -->
<div class="flash"></div>
</n-flex>
<!-- 可卸载状态或内置插件状态 -->
<n-flex
v-else
vertical
justify="center"
align="center"
:size="8"
class="box"
:class="[
plugin.state === PluginEnum.BUILTIN
? 'built'
: plugin.state === PluginEnum.UNINSTALLING
? 'unload'
: 'colorful',
{
'filter-shadow': page.shadow
}
]">
<img v-if="plugin.isAnimate" class="size-38px" :src="`/emoji/${plugin.icon}.webp`" alt="" />
<svg v-else class="size-38px color-#555"><use :href="`#${plugin.iconAction || plugin.icon}`"></use></svg>
<p class="text-(12px #666)">{{ plugin.title }}</p>
<n-flex
v-if="plugin.state === PluginEnum.UNINSTALLING"
class="relative rounded-22px border-(1px solid #c14053) bg-#f6dfe3 p-[4px_8px]">
<p class="text-(12px #c14053 center)">卸载中</p>
</n-flex>
<n-flex
v-if="plugin.state === PluginEnum.BUILTIN"
class="relative rounded-22px border-(1px solid #777) bg-#e3e3e3 size-fit p-[4px_8px]">
<p class="text-(12px #777 center)">已内置</p>
</n-flex>
<n-flex
v-if="plugin.state === PluginEnum.INSTALLED"
class="relative rounded-22px border-(1px solid #4C77BD) bg-#e0e9fc p-[4px_8px]">
<p class="text-(12px #4C77BD center)">{{ plugin.version }}</p>
</n-flex>
<!-- 闪光效果 -->
<div class="flash"></div>
<Transition>
<svg
v-if="plugin.isAdd && plugin.state !== PluginEnum.BUILTIN"
class="absolute color-#666 left-2px top-2px size-14px">
<use href="#notOnTop"></use>
</svg>
</Transition>
<!-- 插件操作 -->
<n-popover
v-if="plugin.state === PluginEnum.INSTALLED || index === isCurrently"
:show="isCurrently === index"
style="padding: 0"
:show-arrow="false"
trigger="click"
placement="bottom">
<template #trigger>
<svg @click.stop="isCurrently = index" class="absolute color-#666 right-0 top-0 size-18px rotate-90">
<use href="#more"></use>
</svg>
</template>
<div class="action-item">
<div class="menu-list">
<div v-if="!plugin.isAdd" @click="handleAdd(plugin)" class="menu-item">
<svg class="color-#4C77BD"><use href="#add"></use></svg>
<p class="text-#4C77BD">固定侧边栏</p>
</div>
<div v-else @click="handleDelete(plugin)" class="menu-item">
<svg class="color-#c14053"><use href="#reduce"></use></svg>
<p class="text-#c14053">取消固定</p>
</div>
<div @click="handleUnload(plugin)" class="menu-item">
<svg><use href="#delete"></use></svg>
<p>卸载</p>
</div>
</div>
</div>
</n-popover>
</n-flex>
</Transition>
</template>
</n-flex>
</n-scrollbar>
</div>
</template>
<script setup lang="ts">
import { PluginEnum } from '@/enums'
import { setting } from '@/stores/setting.ts'
import { usePluginsStore } from '@/stores/plugins.ts'
import { useMenuTopStore } from '@/stores/menuTop.ts'
import { storeToRefs } from 'pinia'
const settingStore = setting()
const { updatePlugins, plugins } = usePluginsStore()
const { menuTop } = useMenuTopStore()
const { page } = storeToRefs(settingStore)
const isCurrently = ref(-1)
const handleState = (plugin: STO.Plugins<PluginEnum>) => {
if (plugin.state === PluginEnum.INSTALLED) return
plugin.state = PluginEnum.DOWNLOADING
const interval = setInterval(() => {
if (plugin.progress < 100) {
plugin.progress += 10
} else {
clearInterval(interval)
plugin.state = PluginEnum.INSTALLED
updatePlugins(plugin)
}
}, 500)
}
const handleUnload = (plugin: STO.Plugins<PluginEnum>) => {
plugin.state = PluginEnum.UNINSTALLING
setTimeout(() => {
handleDelete(plugin)
plugin.isAdd = false
plugin.state = PluginEnum.NOT_INSTALLED
updatePlugins(plugin)
}, 2000)
}
const handleDelete = (plugin: STO.Plugins<PluginEnum>) => {
// menuTop item.url
const itemIndex = menuTop.findIndex((topItem) => topItem.title === plugin.title)
if (itemIndex !== -1) {
setTimeout(() => {
plugin.isAdd = false
updatePlugins(plugin)
menuTop.splice(itemIndex, 1)
}, 300)
}
}
const handleAdd = (plugin: STO.Plugins<PluginEnum>) => {
// itemsTop
const itemIndex = menuTop.findIndex((topItem) => topItem.title === plugin.title)
if (itemIndex !== -1) {
return
}
setTimeout(() => {
plugin.isAdd = true
updatePlugins(plugin)
menuTop.push(plugin)
}, 300)
}
const closeMenu = (event: Event) => {
const e = event.target as HTMLInputElement
if (!e.matches('.action-item')) {
isCurrently.value = -1
}
}
onMounted(() => {
window.addEventListener('click', closeMenu, true)
})
onUnmounted(() => {
window.removeEventListener('click', closeMenu, true)
})
</script>
<style scoped lang="scss">
.box {
@apply relative select-none custom-shadow cursor-pointer size-fit w-100px h-100px rounded-8px overflow-hidden;
transition: all 0.2s;
.flash {
position: absolute;
left: -130%;
top: 0;
width: 100px;
height: 100px;
background-image: linear-gradient(90deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
transform: skew(-30deg);
pointer-events: none;
}
&:hover .flash {
left: 130%;
transition: all 0.8s ease-in-out;
}
}
.downloading {
width: 80px;
background: var(--progress-bg);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.action-item {
@include menu-item-style();
left: -80px;
@include menu-list();
}
.colorful {
background-image: linear-gradient(45deg, #a8edea 0%, #fed6e3 100%);
}
.built {
background-image: linear-gradient(-20deg, #e9defa 0%, #fbfcdb 100%);
}
.unload {
background-image: linear-gradient(to top, #feada6 0%, #f5efef 100%);
}
.filter-shadow {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.2));
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,292 @@
<template>
<div>
<n-scrollbar style="max-height: 280px" @scroll="handleScroll($event)">
<n-flex vertical :size="0" class="z-10 box-border w-full">
<template v-for="(plugin, index) in plugins as STO.Plugins<PluginEnum>[]" :key="index">
<n-flex align="center" justify="space-between" class="float-block p-[0_20px]">
<n-flex :size="14" align="center">
<n-flex align="center" justify="center" class="size-48px rounded-50% bg-#7676760f">
<Transition mode="out-in">
<svg
v-if="plugin.state === PluginEnum.NOT_INSTALLED || plugin.state === PluginEnum.DOWNLOADING"
class="size-34px color-#666">
<use :href="`#${plugin.icon}`"></use>
</svg>
<template v-else>
<img v-if="plugin.isAnimate" class="size-34px" :src="`/emoji/${plugin.icon}.webp`" alt="" />
<svg v-else class="size-34px color-#666">
<use :href="`#${plugin.iconAction || plugin.icon}`"></use>
</svg>
</template>
</Transition>
</n-flex>
<n-flex vertical :size="10">
<n-flex align="center" :size="6">
<p class="text-(14px #666) pl-4px">{{ plugin.title }}</p>
<Transition>
<svg v-if="plugin.isAdd && plugin.state !== PluginEnum.BUILTIN" class="color-#666 size-14px">
<use href="#notOnTop"></use>
</svg>
</Transition>
</n-flex>
<Transition mode="out-in">
<n-flex
v-if="plugin.state === PluginEnum.UNINSTALLING"
class="relative rounded-22px bg-#f6dfe3 size-fit p-[4px_8px]">
<p class="text-(12px #c14053 center)">卸载中</p>
</n-flex>
<n-flex
v-else-if="plugin.state === PluginEnum.BUILTIN"
class="relative rounded-22px bg-#e3e3e3 size-fit p-[4px_8px]">
<p class="text-(12px #777 center)">已内置</p>
</n-flex>
<n-flex v-else class="relative rounded-22px bg-#e0e9fc size-fit p-[4px_8px]">
<p class="text-(12px #4C77BD center)">{{ plugin.version }}</p>
</n-flex>
</Transition>
</n-flex>
</n-flex>
<!-- 未安装和下载中状态 -->
<n-flex
v-if="plugin.state === PluginEnum.NOT_INSTALLED || plugin.state === PluginEnum.DOWNLOADING"
vertical
justify="center"
align="center"
:size="8"
class="box bg-[--left-active-color]">
<!-- 在下载中进度条 -->
<n-flex
@click="handleState(plugin)"
align="center"
class="relative"
:class="[plugin.state === PluginEnum.DOWNLOADING ? 'downloading' : 'bg-[--primary-color] size-full']">
<div
:style="{
width: plugin.state === PluginEnum.DOWNLOADING ? `${plugin.progress * 0.6}px` : 'auto'
}"
:class="[
plugin.progress < 100 ? 'rounded-l-0 rounded-r-0' : 'rounded-2px',
plugin.progress > 0 ? 'h-40px border-(1px solid transparent)' : 'h-40px'
]"
v-if="plugin.state === PluginEnum.DOWNLOADING"
class="bg-#8CA9F4">
<p class="absolute-center text-(12px #4C77BD)">{{ plugin.progress }}%</p>
</div>
<p v-else class="text-(12px [--chat-text-color] center) w-full">安装</p>
</n-flex>
</n-flex>
<!-- 卸载中 -->
<n-spin v-if="plugin.state === PluginEnum.UNINSTALLING" :stroke="'#c14053'" :size="22" />
<!-- 插件操作 -->
<n-popover
v-if="plugin.state === PluginEnum.INSTALLED || index === isCurrently"
:show="isCurrently === index"
style="padding: 0"
:show-arrow="false"
trigger="click"
placement="bottom">
<template #trigger>
<svg @click.stop="isCurrently = index" class="size-22px rotate-90">
<use href="#more"></use>
</svg>
</template>
<div class="action-item">
<div class="menu-list">
<div v-if="!plugin.isAdd" @click="handleAdd(plugin)" class="menu-item">
<svg class="color-#4C77BD"><use href="#add"></use></svg>
<p class="text-#4C77BD">固定侧边栏</p>
</div>
<div v-else @click="handleDelete(plugin)" class="menu-item">
<svg class="color-#c14053"><use href="#reduce"></use></svg>
<p class="text-#c14053">取消固定</p>
</div>
<div @click="handleUnload(plugin)" class="menu-item">
<svg><use href="#delete"></use></svg>
<p>卸载</p>
</div>
</div>
</div>
</n-popover>
</n-flex>
</template>
</n-flex>
</n-scrollbar>
</div>
</template>
<script setup lang="ts">
import { PluginEnum } from '@/enums'
import { usePluginsStore } from '@/stores/plugins.ts'
import { useMenuTopStore } from '@/stores/menuTop.ts'
const { plugins, updatePlugins } = usePluginsStore()
const { menuTop } = useMenuTopStore()
const scrollTop = ref(-1)
const itemCount = Object.values(plugins).length
const isCurrently = ref(-1)
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement
scrollTop.value = target.scrollTop
updateHoverClasses()
}
const handleState = (plugin: STO.Plugins<PluginEnum>) => {
if (plugin.state === PluginEnum.INSTALLED) return
plugin.state = PluginEnum.DOWNLOADING
const interval = setInterval(() => {
if (plugin.progress < 100) {
plugin.progress += 10
} else {
clearInterval(interval)
plugin.state = PluginEnum.INSTALLED
updatePlugins(plugin)
}
}, 500)
}
const handleUnload = (plugin: STO.Plugins<PluginEnum>) => {
plugin.state = PluginEnum.UNINSTALLING
setTimeout(() => {
handleDelete(plugin)
plugin.isAdd = false
plugin.state = PluginEnum.NOT_INSTALLED
updatePlugins(plugin)
}, 2000)
}
const handleDelete = (plugin: STO.Plugins<PluginEnum>) => {
// menuTop item.url
const itemIndex = menuTop.findIndex((topItem) => topItem.title === plugin.title)
if (itemIndex !== -1) {
setTimeout(() => {
plugin.isAdd = false
updatePlugins(plugin)
menuTop.splice(itemIndex, 1)
}, 300)
}
}
const handleAdd = (plugin: STO.Plugins<PluginEnum>) => {
// itemsTop
const itemIndex = menuTop.findIndex((topItem) => topItem.title === plugin.title)
if (itemIndex !== -1) {
return
}
setTimeout(() => {
plugin.isAdd = true
updatePlugins(plugin)
menuTop.push(plugin)
}, 300)
}
const closeMenu = (event: Event) => {
const e = event.target as HTMLInputElement
if (!e.matches('.action-item')) {
isCurrently.value = -1
}
}
const updateHoverClasses = () => {
let styleStr = ''
for (let i = 0, len = itemCount; i < len - 1; i++) {
styleStr += `
.float-block:nth-child(${i + 1}):hover~.float-block:last-child::before {
--y: calc(var(--height) * ${i} - ${scrollTop.value}px);
}
`
}
styleStr += `.float-block:nth-child(${itemCount}):hover::before {
--y: calc(var(--height) * ${itemCount - 1} - ${scrollTop.value}px);
opacity: .06;
}`
const styleTag = document.getElementById('hover-classes')
if (styleTag) styleTag.remove()
const style = document.createElement('style')
style.id = 'hover-classes'
style.innerHTML = styleStr
document.head.appendChild(style)
}
onMounted(() => {
updateHoverClasses()
window.addEventListener('click', closeMenu, true)
})
onUnmounted(() => {
window.removeEventListener('click', closeMenu, true)
})
</script>
<style scoped lang="scss">
.float-block {
--y: 0;
--height: 70px;
--surface-2: #767676;
width: 100%;
height: var(--height);
cursor: pointer;
box-sizing: border-box;
}
.float-block:last-child::before {
content: '';
display: block;
position: absolute;
background: var(--surface-2);
opacity: 0;
width: 100%;
transform: translateY(var(--y));
top: 0;
left: 0;
height: var(--height);
pointer-events: none;
transition: all 0.5s cubic-bezier(0.2, 1, 0.2, 1);
}
.float-block:hover ~ .float-block:last-child:before {
opacity: 0.06;
}
.box {
@apply relative select-none cursor-pointer size-fit w-60px h-40px rounded-8px overflow-hidden;
transition: all 0.2s;
}
.downloading {
width: 60px;
background: var(--progress-bg);
}
.action-item {
@include menu-item-style();
left: -80px;
@include menu-list();
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<!-- 弹出框 -->
<n-modal v-model:show="isShow as boolean" :mask-closable="false" class="w-390px border-rd-8px">
<div class="bg-[--bg-popover] h-full box-border flex flex-col">
<!-- 顶部图片加上操作栏 -->
<div class="h-140px relative w-full p-6px box-border">
<img
class="absolute blur-6px rounded-t-6px z-1 top-0 left-0 w-full h-140px object-cover"
src="@/assets/img/dispersion-bg.png"
alt="" />
<img
class="absolute rounded-t-6px z-2 top-0 left-0 w-full h-140px object-cover"
src="@/assets/img/dispersion-bg.png"
alt="" />
<div
v-if="type() === 'macos'"
@click="isShow = false"
class="mac-close z-10 relative size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
<svg
v-if="type() === 'windows'"
@click="isShow = false"
class="z-10 color-#333 w-12px h-12px absolute top-6px right-6px cursor-pointer select-none">
<use href="#close"></use>
</svg>
</div>
<n-flex justify="space-between" align="center">
<n-flex :size="4" align="center" class="p-18px">
<p class="text-(16px [--text-color])">插件管理</p>
<div class="ml-6px p-[4px_8px] size-fit bg-[--bate-bg] rounded-8px text-(12px [--bate-color] center)">
Beta
</div>
</n-flex>
<n-tabs
:value="viewMode"
:on-update:value="(v) => (viewMode = v)"
class="w-76px h-28px mr-22px"
type="segment"
animated>
<n-tab name="card">
<template #default>
<svg class="size-16px"><use href="#view-grid-card"></use></svg>
</template>
</n-tab>
<n-tab name="list">
<template #default>
<svg class="size-16px"><use href="#view-grid-list"></use></svg>
</template>
</n-tab>
</n-tabs>
</n-flex>
<Transition name="slide-up" mode="out-in">
<Card v-if="viewMode === 'card'" />
<List v-else />
</Transition>
</div>
</n-modal>
</template>
<script setup lang="ts">
import { type } from '@tauri-apps/plugin-os'
import { usePluginsStore } from '@/stores/plugins.ts'
import { storeToRefs } from 'pinia'
import Card from './Card.vue'
import List from './List.vue'
/** 是否展示插件管理弹窗 */
const isShow = defineModel()
const { viewMode } = storeToRefs(usePluginsStore())
</script>
<style scoped lang="scss">
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.25s ease-out;
}
.slide-up-enter-from {
opacity: 0;
transform: translateX(30px);
}
.slide-up-leave-to {
opacity: 0;
transform: translateY(-30px);
}
</style>

View File

@ -1,62 +1,13 @@
import { useWindow } from '@/hooks/useWindow.ts'
import { MittEnum, ModalEnum } from '@/enums'
import { MittEnum, ModalEnum, PluginEnum } from '@/enums'
import Mitt from '@/utils/Bus.ts'
import { useLogin } from '@/hooks/useLogin.ts'
const { createWebviewWindow } = useWindow()
const { logout } = useLogin()
/**
*
* @param url
* @param icon
* @param title
* @param iconAction
* @param badge
* @param tip
* @param size
* @param window
* 使pinia写入了localstorage中
*/
/* todo: 这里需要存入到localStore中, 需要区分固定的和不固定的,固定时有会话和用户列表的,其他都为动态添加 */
const itemsTop = ref<OPT.L.Common[]>([
{
url: 'message',
icon: 'message',
iconAction: 'message-action'
},
{
url: 'friendsList',
icon: 'avatar',
iconAction: 'avatar-action'
},
{
url: 'dynamic',
icon: 'fire',
title: '动态',
iconAction: 'fire-action2',
size: {
width: 840,
height: 800
},
window: {
resizable: false
}
},
{
url: 'robot',
icon: 'robot',
title: 'GPT',
iconAction: 'robot-action',
tip: '机器人新功能在开发中',
size: {
minWidth: 780,
width: 980,
height: 800
},
window: {
resizable: true
}
}
])
/** 下半部分操作栏配置 */
const itemsBottom: OPT.L.Common[] = [
{
@ -140,4 +91,104 @@ const moreList = ref<OPT.L.MoreList[]>([
}
])
export { itemsTop, itemsBottom, moreList }
/** 插件列表 */
const pluginsList = ref<STO.Plugins<PluginEnum>[]>([
{
url: 'dynamic',
icon: 'fire',
title: '动态',
isAnimate: true,
iconAction: 'fire-action2',
state: PluginEnum.BUILTIN,
isAdd: true,
dot: false,
progress: 0,
size: {
width: 840,
height: 800
},
window: {
resizable: false
}
},
{
icon: 'robot',
iconAction: 'robot-action',
url: 'robot',
title: 'ChatBot',
isAnimate: true,
tip: 'ChatBot新应用上线',
state: PluginEnum.NOT_INSTALLED,
version: 'v2.0.0-Bate',
isAdd: false,
dot: true,
progress: 0,
size: {
minWidth: 780,
width: 980,
height: 800
},
window: {
resizable: true
}
},
{
icon: 'Music',
url: 'music',
title: 'HuLa云音乐',
tip: 'HuLa云音乐开发中敬请期待',
state: PluginEnum.NOT_INSTALLED,
version: 'v1.0.0-Alpha',
isAdd: false,
dot: true,
progress: 0,
size: {
minWidth: 780,
width: 980,
height: 800
},
window: {
resizable: true
}
},
{
icon: 'UimSlack',
url: 'collaboration',
title: 'HuLa协作',
tip: 'HuLa协作开发中敬请期待',
state: PluginEnum.NOT_INSTALLED,
version: 'v1.0.0-Alpha',
isAdd: false,
dot: true,
progress: 0,
size: {
minWidth: 780,
width: 980,
height: 800
},
window: {
resizable: true
}
},
{
icon: 'vigo',
url: 'collaboration',
title: 'HuLa短视频',
tip: 'HuLa短视频开发中敬请期待',
state: PluginEnum.NOT_INSTALLED,
version: 'v1.0.0-Alpha',
isAdd: false,
dot: true,
progress: 0,
size: {
minWidth: 780,
width: 980,
height: 800
},
window: {
resizable: true
}
}
])
export { itemsBottom, moreList, pluginsList }

View File

@ -4,7 +4,6 @@ import { useUserStore } from '@/stores/user.ts'
import { useCachedStore } from '@/stores/cached.ts'
import { storeToRefs } from 'pinia'
import { onlineStatus } from '@/stores/onlineStatus.ts'
import { itemsTop } from '@/layout/left/config.tsx'
import { EventEnum, IsYetEnum, MittEnum, MsgEnum, ThemeEnum } from '@/enums'
import { BadgeType, UserInfoType } from '@/services/types.ts'
import { useChatStore } from '@/stores/chat.ts'
@ -17,21 +16,21 @@ import GraphemeSplitter from 'grapheme-splitter'
import { delay } from 'lodash-es'
import router from '@/router'
import { listen } from '@tauri-apps/api/event'
import { useMenuTopStore } from '@/stores/menuTop.ts'
export const leftHook = () => {
const prefers = matchMedia('(prefers-color-scheme: dark)')
const { createWebviewWindow } = useWindow()
const settingStore = setting()
const { menuTop } = useMenuTopStore()
const userStore = useUserStore()
const cachedStore = useCachedStore()
const { themes, login } = storeToRefs(settingStore)
const OLStatusStore = onlineStatus()
const { url, title, bgColor } = storeToRefs(OLStatusStore)
/**当前选中的元素 默认选中itemsTop的第一项*/
const activeUrl = ref<string>(itemsTop.value[0].url)
const activeUrl = ref<string>(menuTop[0].url)
const settingShow = ref(false)
const shrinkStatus = ref(false)
const isNewWindows = ref(['dynamic', 'robot', 'mail'])
/** 是否展示个人信息浮窗 */
const infoShow = ref(false)
/** 是否显示上半部分操作栏中的提示 */
@ -90,7 +89,7 @@ export const leftHook = () => {
}
watchEffect(() => {
itemsTop.value.find((item) => {
menuTop.find((item) => {
if (item.url === 'message') {
item.badge = msgTotal.value
}
@ -174,8 +173,7 @@ export const leftHook = () => {
size?: { width: number; height: number; minWidth?: number },
window?: { resizable: boolean }
) => {
// 判断url是否等于isNewWindows.value数组中的值如果是则创建新的窗口
if (isNewWindows.value.includes(url)) {
if (window) {
delay(async () => {
await createWebviewWindow(
title!,
@ -225,7 +223,7 @@ export const leftHook = () => {
infoShow.value = false
})
Mitt.on(MittEnum.UPDATE_MSG_TOTAL, (event) => {
itemsTop.value.find((item) => {
menuTop.find((item) => {
if (item.url === 'message') {
item.badge = event as number
}

View File

@ -109,7 +109,7 @@ export const LockScreen = defineComponent(() => {
/** 检查更新弹窗 */
export const CheckUpdate = defineComponent(() => {
const url = `https://gitee.com/api/v5/repos/nongyehong/HuLa-IM-Tauri/releases/tags/${pkg.version}?access_token=${import.meta.env.VITE_GITEE_TOKEN}`
const url = `https://gitee.com/api/v5/repos/nongyehong/HuLa-IM-Tauri/releases/tags/v${pkg.version}?access_token=${import.meta.env.VITE_GITEE_TOKEN}`
/** 项目提交日志记录 */
const commitLog = ref<{ message: string; icon: string }[]>([])
const loading = ref(false)
@ -117,18 +117,32 @@ export const CheckUpdate = defineComponent(() => {
/** 版本更新日期 */
const versionTime = ref('')
// const commitTypeMap: { [key: string]: string } = {
// feat: 'feat',
// fix: 'fix',
// docs: 'docs',
// style: 'style',
// refactor: 'refactor',
// perf: 'perf',
// test: 'test',
// build: 'build',
// ci: 'ci',
// revert: 'revert',
// chore: 'chore'
// }
const commitTypeMap: { [key: string]: string } = {
feat: 'feat',
fix: 'fix',
docs: 'docs',
style: 'style',
refactor: 'refactor',
perf: 'perf',
test: 'test',
build: 'build',
ci: 'ci',
revert: 'revert',
chore: 'chore'
feat: 'comet',
fix: 'bug',
docs: 'memo',
style: 'lipstick',
refactor: 'recycling-symbol',
perf: 'rocket',
test: 'test-tube',
build: 'package',
ci: 'gear',
revert: 'right-arrow-curving-left',
chore: 'hammer-and-wrench'
}
const mapCommitType = (commitMessage: string) => {
@ -144,7 +158,7 @@ export const CheckUpdate = defineComponent(() => {
const checkUpdate = () => {
const url = `https://gitee.com/api/v5/repos/nongyehong/HuLa-IM-Tauri/tags?access_token=${import.meta.env.VITE_GITEE_TOKEN}&sort=name&direction=desc&page=1&per_page=1`
if (lastVersion && lastVersion === pkg.version) {
if (lastVersion && lastVersion === `v${pkg.version}`) {
window.$message.success('当前已是最新版本')
return
}
@ -153,10 +167,12 @@ export const CheckUpdate = defineComponent(() => {
res
.json()
.then(async (data) => {
if (data[0].name === pkg.version) {
checkLoading.value = false
window.$message.success('当前已是最新版本')
lastVersion = pkg.version
if (data[0].name === `v${pkg.version}`) {
setTimeout(() => {
window.$message.success('当前已是最新版本')
lastVersion = `v${pkg.version}`
checkLoading.value = false
}, 600)
} else {
// TODO 获取最新版本的提交日志,并且更换按钮文字为下载最新版本 (nyh -> 2024-07-11 22:20:33)
}
@ -193,7 +209,7 @@ export const CheckUpdate = defineComponent(() => {
const message = lastColonIndex !== -1 ? commit.substring(lastColonIndex + 1).trim() : commit
return {
message: message,
icon: mapCommitType(commit)!
icon: mapCommitType(commit) || 'alien-monster'
}
})
loading.value = false
@ -232,7 +248,7 @@ export const CheckUpdate = defineComponent(() => {
<NFlex justify={'space-between'} align={'center'}>
<NFlex align={'center'} size={10}>
<p>:</p>
<p class="text-(24px #909090) font-500">{pkg.version}</p>
<p class="text-(24px #909090) font-500">v{pkg.version}</p>
</NFlex>
<NFlex align={'center'} size={10}>
<p class="text-(12px #909090)">:</p>
@ -241,15 +257,16 @@ export const CheckUpdate = defineComponent(() => {
</NFlex>
<p class="text-(14px #909090)"></p>
<NScrollbar class="max-h-460px p-[0_10px] box-border">
<NTimeline class="p-[0_6px] box-border">
<NTimeline class="p-16px box-border">
{commitLog.value.map((log, index) => (
<NTimelineItem key={index} content={log.message}>
{{
icon: () => (
<NIcon size={20}>
<svg>
<use href={`#${log.icon}`}></use>
</svg>
<NIcon size={32}>
{/*<svg>*/}
{/* <use href={`#${log.icon}`}></use>*/}
{/*</svg>*/}
<img class="size-32px" src={`/emoji/${log.icon}.webp`} alt="" />
</NIcon>
)
}}

View File

@ -11,6 +11,7 @@
.left {
background: var(--left-bg-color);
//background: #64a29c;
}
.top-action,
@ -27,8 +28,6 @@
.setting-item {
@include menu-item-style(absolute);
left: 58px;
bottom: 10px;
@include menu-list();
}

View File

@ -1,14 +1,24 @@
import { defineStore } from 'pinia'
import { StoresEnum } from '@/enums'
export const alwaysOnTop = defineStore(StoresEnum.ALWAYS_ON_TOP, {
state: (): STO.AlwaysOnTop => ({}),
actions: {
setWindowTop(key: string, data: boolean) {
this.$state[key] = data
},
getWindowTop(key: string) {
return this.$state[key]
}
interface AlwaysOnTopState {
[key: string]: boolean
}
export const useAlwaysOnTopStore = defineStore(StoresEnum.ALWAYS_ON_TOP, () => {
const alwaysOnTop = ref<AlwaysOnTopState>({})
const setWindowTop = (key: string, data: boolean) => {
alwaysOnTop.value[key] = data
}
const getWindowTop = (key: string) => {
return alwaysOnTop.value[key]
}
return {
alwaysOnTop,
setWindowTop,
getWindowTop
}
})

View File

@ -1,16 +1,23 @@
import { defineStore } from 'pinia'
import { StoresEnum } from '@/enums'
export const history = defineStore(StoresEnum.HISTORY, {
state: (): STO.History => ({
emoji: []
}),
actions: {
setEmoji(item: string[]) {
this.emoji = item
export const useHistoryStore = defineStore(
StoresEnum.HISTORY,
() => {
const emoji = ref<string[]>([])
const setEmoji = (item: string[]) => {
emoji.value = item
}
return {
emoji,
setEmoji
}
},
share: {
enable: true
{
share: {
enable: true
}
}
})
)

42
src/stores/menuTop.ts Normal file
View File

@ -0,0 +1,42 @@
import { defineStore } from 'pinia'
import { PluginEnum, StoresEnum } from '@/enums'
import { usePluginsStore } from '@/stores/plugins.ts'
export const useMenuTopStore = defineStore(
StoresEnum.MENUTOP,
() => {
const { getPluginType } = usePluginsStore()
const pluginType = getPluginType(PluginEnum.BUILTIN)
// 初始状态
const initialState: OPT.L.Common[] = [
{
url: 'message',
icon: 'message',
iconAction: 'message-action'
},
{
url: 'friendsList',
icon: 'avatar',
iconAction: 'avatar-action'
}
]
const menuTop = ref<STO.Plugins<PluginEnum>[]>(initialState as any)
onBeforeMount(() => {
if (!localStorage.getItem(StoresEnum.MENUTOP)) {
menuTop.value.push(pluginType)
}
})
return {
menuTop
}
},
{
share: {
enable: true,
initialize: true
}
}
)

View File

@ -5,7 +5,7 @@ import Colorthief from 'colorthief'
const colorthief = new Colorthief()
export const onlineStatus = defineStore(StoresEnum.ONLINE_STATUS, {
state: (): OPT.Online => ({
state: (): STO.OnlineStatus => ({
url: '',
title: '',
bgColor: ''

71
src/stores/plugins.ts Normal file
View File

@ -0,0 +1,71 @@
import { defineStore } from 'pinia'
import { PluginEnum, StoresEnum } from '@/enums'
import { pluginsList } from '@/layout/left/config.tsx'
export const usePluginsStore = defineStore(
StoresEnum.PLUGINS,
() => {
/** 插件内容 */
const plugins = ref<STO.Plugins<PluginEnum>[]>([])
/** 插件查看模式 */
const viewMode = ref<string>('card')
/**
*
* @param newPlugins
*/
const setPlugins = (newPlugins: STO.Plugins<PluginEnum>[]) => {
plugins.value = newPlugins
}
/**
*
* @param P
*/
const getPluginType = (P: PluginEnum) => {
if (Object.keys(plugins.value).length === 0) {
return pluginsList.value[P]
} else {
return plugins.value[P]
}
}
/**
*
* @param P
*/
const updatePlugins = (P: STO.Plugins<PluginEnum>) => {
Object.values(plugins.value).find((item: STO.Plugins<PluginEnum>) => {
if (item.title === P.title) {
// 修改对应项插件状态
item.state = P.state
item.isAdd = P.isAdd
setPlugins(plugins.value)
}
})
}
onBeforeMount(() => {
// 读取本地存储的插件数据
if (!localStorage.getItem(StoresEnum.PLUGINS)) {
setPlugins(pluginsList.value)
} else {
Object.assign(pluginsList.value, JSON.parse(localStorage.getItem(StoresEnum.PLUGINS)!))
}
})
return {
plugins,
viewMode,
setPlugins,
getPluginType,
updatePlugins
}
},
{
share: {
enable: true,
initialize: true
}
}
)

View File

@ -8,27 +8,22 @@ const badgeCachedList = reactive<Record<number, Partial<CacheBadgeItem>>>({})
// TODO 使用indexDB或者把配置写出到文件中还需要根据每个账号来进行配置 (nyh -> 2024-03-26 01:22:12)
export const setting = defineStore(StoresEnum.SETTING, {
state: (): STO.Setting => ({
/** 主题设置 */
themes: {
content: '',
pattern: ''
},
/** 是否启用ESC关闭窗口 */
escClose: true,
lockScreen: {
enable: false,
password: ''
},
/** 系统托盘 */
tips: {
type: CloseBxEnum.HIDE,
notTips: false
},
/** 登录设置 */
login: {
autoLogin: false,
autoStartup: false,
/** 用户保存的登录信息 */
accountInfo: {
account: '',
password: '',
@ -39,16 +34,12 @@ export const setting = defineStore(StoresEnum.SETTING, {
},
badgeList: []
},
/** 聊天设置 */
chat: {
/** 发送快捷键 */
sendKey: 'Enter',
/** 是否双击打开独立会话窗口 */
isDouble: true
},
/** 界面设置 */
page: {
shadow: false,
shadow: true,
fonts: 'AliFangYuan'
}
}),

View File

@ -4,7 +4,6 @@
svg {
width: 22px;
height: 22px;
outline: none;
}
&:hover svg {
color: #13987f;
@ -13,7 +12,7 @@
}
}
.setting-item {
.box-item {
@apply border-(solid 1px [--line-color]) custom-shadow;
&:first-child {
margin-top: 0;

View File

@ -4,7 +4,8 @@
font-weight: 500 !important;
caret-color: #13987f !important; /* 改变输入框光标的颜色 */
}
html, body {
html, body, svg {
outline: none;
overscroll-behavior: none; // 禁止mac的触底反弹效果
}
/**! 主题变量 */
@ -91,6 +92,8 @@ html, body {
--bate-bg: #d9ccf7;
// 卡片hover样式
--card-bg: #13987f60;
// 安装进度条样式
--progress-bg: #e0e9fc;
}
html[data-theme='dark'] {
@ -170,6 +173,8 @@ html[data-theme='dark'] {
--bate-bg: #403555;
// 卡片hover样式
--card-bg: #13987f20;
// 安装进度条样式
--progress-bg: transparent;
}
/**! end */
// 线性动画

View File

@ -7,7 +7,6 @@
background: none; /** 无背景 */
border: none; /** 边框样式,可根据需求修改 */
border-radius: 0; /** 边框圆角 */
outline: none; /** 点击时无轮廓 */
min-height: 90px; /** 最小高度 */
line-height: 20px; /** 行高 */
overflow: auto; /** 内容过多时允许滚动 */
@ -25,4 +24,4 @@
}
.active {
background-color: var(--bg-group-hover);
}
}

View File

@ -1,66 +1,68 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ActionBar: (typeof import('./../components/windows/ActionBar.vue'))['default']
ChatBox: (typeof import('./../components/rightBox/chatBox/index.vue'))['default']
ChatFooter: (typeof import('./../components/rightBox/chatBox/ChatFooter.vue'))['default']
ChatHeader: (typeof import('./../components/rightBox/chatBox/ChatHeader.vue'))['default']
ChatMain: (typeof import('./../components/rightBox/chatBox/ChatMain.vue'))['default']
ChatSidebar: (typeof import('./../components/rightBox/chatBox/ChatSidebar.vue'))['default']
ContextMenu: (typeof import('./../components/common/ContextMenu.vue'))['default']
Details: (typeof import('./../components/rightBox/Details.vue'))['default']
Emoji: (typeof import('./../components/rightBox/emoji/index.vue'))['default']
Image: (typeof import('./../components/rightBox/renderMessage/Image.vue'))['default']
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']
NIcon: (typeof import('naive-ui'))['NIcon']
NIconWrapper: (typeof import('naive-ui'))['NIconWrapper']
NImage: (typeof import('naive-ui'))['NImage']
NImageGroup: (typeof import('naive-ui'))['NImageGroup']
NInput: (typeof import('naive-ui'))['NInput']
NLoadingBarProvider: (typeof import('naive-ui'))['NLoadingBarProvider']
NMessageProvider: (typeof import('naive-ui'))['NMessageProvider']
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']
NSwitch: (typeof import('naive-ui'))['NSwitch']
NTabPane: (typeof import('naive-ui'))['NTabPane']
NTabs: (typeof import('naive-ui'))['NTabs']
NTooltip: (typeof import('naive-ui'))['NTooltip']
NVirtualList: (typeof import('naive-ui'))['NVirtualList']
RenderMessage: (typeof import('./../components/rightBox/renderMessage/index.vue'))['default']
RouterLink: (typeof import('vue-router'))['RouterLink']
RouterView: (typeof import('vue-router'))['RouterView']
Screenshot: (typeof import('./../components/common/Screenshot.vue'))['default']
Text: (typeof import('./../components/rightBox/renderMessage/Text.vue'))['default']
ActionBar: typeof import('./../components/windows/ActionBar.vue')['default']
ChatBox: typeof import('./../components/rightBox/chatBox/index.vue')['default']
ChatFooter: typeof import('./../components/rightBox/chatBox/ChatFooter.vue')['default']
ChatHeader: typeof import('./../components/rightBox/chatBox/ChatHeader.vue')['default']
ChatMain: typeof import('./../components/rightBox/chatBox/ChatMain.vue')['default']
ChatSidebar: typeof import('./../components/rightBox/chatBox/ChatSidebar.vue')['default']
ContextMenu: typeof import('./../components/common/ContextMenu.vue')['default']
Details: typeof import('./../components/rightBox/Details.vue')['default']
Emoji: typeof import('./../components/rightBox/emoji/index.vue')['default']
Image: typeof import('./../components/rightBox/renderMessage/Image.vue')['default']
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']
NIcon: typeof import('naive-ui')['NIcon']
NIconWrapper: typeof import('naive-ui')['NIconWrapper']
NImage: typeof import('naive-ui')['NImage']
NImageGroup: typeof import('naive-ui')['NImageGroup']
NInput: typeof import('naive-ui')['NInput']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
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']
NTooltip: typeof import('naive-ui')['NTooltip']
NVirtualList: typeof import('naive-ui')['NVirtualList']
RenderMessage: typeof import('./../components/rightBox/renderMessage/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Screenshot: typeof import('./../components/common/Screenshot.vue')['default']
Text: typeof import('./../components/rightBox/renderMessage/Text.vue')['default']
}
}

View File

@ -2,7 +2,17 @@
declare namespace OPT {
/** 主页左侧选项 */
namespace L {
/** 顶部的选项 */
/**
*
* @param url
* @param icon
* @param title
* @param iconAction
* @param badge
* @param tip
* @param size
* @param window
*/
type Common = {
url: string
icon: string
@ -20,14 +30,24 @@ declare namespace OPT {
}
}
/** 更多的选项 */
/**
*
* @param label
* @param icon
* @param click
*/
type MoreList = {
label: string
icon: string
click: () => void
}
/** 设置页面的侧边栏选项 */
/**
*
* @param url
* @param label
* @param icon
*/
type SettingSide = {
url: string
label: string
@ -35,21 +55,36 @@ declare namespace OPT {
}
}
/** 右键菜单选项 */
/**
*
* @param label
* @param icon
* @param click
*/
type RightMenu = {
label: string
icon: string
click?: (...args: any[]) => void
} | null
/** 详情页选项 */
/**
*
* @param url
* @param title
* @param click
*/
type Details = {
url: string
title: string
click: (...args: any[]) => void
}
/** 在线状态 */
/**
* 线
* @param url
* @param title
* @param bgColor
*/
type Online = {
url: string
title: string

View File

@ -1,6 +1,15 @@
/** pinia的store的命名空间 */
declare namespace STO {
/** 设置 */
/**
*
* @param themes
* @param tips
* @param escClose ESC关闭窗口
* @param lockScreen
* @param login
* @param chat
* @param page
*/
type Setting = {
/** 主题设置 */
themes: {
@ -54,21 +63,44 @@ declare namespace STO {
}
}
/** 置顶窗口列表 */
/**
*
* @param key
*/
type AlwaysOnTop = {
/** 是否置顶窗口列表 */
[key: string]: boolean
}
/** 隐藏窗口列表 */
type HideWindow = {
/** 是否隐藏窗口列表 */
[key: string]: boolean
}
/** 历史内容 */
/**
*
* @param emoji emoji列表
*/
type History = {
/** emoji列表 */
emoji: string[]
}
/**
* 线
* @param OPT.Online 线
*/
type OnlineStatus = OPT.Online
/**
*
* @param state
* @param version
* @param isAdd
* @param isAnimate
* @param { OPT.L.Common }
*/
type Plugins<T> = {
state: T
version?: string
isAdd: boolean
isAnimate?: boolean
dot?: boolean
progress: number
} & OPT.L.Common
}

View File

@ -3,7 +3,7 @@ import pkg from '../../package.json'
/** 控制台打印版本信息 */
export const consolePrint = () => {
console.log(
`%c 🍀 ${pkg.name} ${pkg.version}`,
`%c 🍀 ${pkg.name} v${pkg.version}`,
'font-size:20px;border-left: 4px solid #13987f;background: #cef9ec;font-family: Comic Sans MS, cursive;color:#581845;padding:10px;border-radius:4px;',
`${pkg.author.url}`
)

View File

@ -2,22 +2,35 @@
<main class="login-box rounded-8px size-full select-none">
<ActionBar :shrink="false" :max-w="false" />
<n-flex vertical align="center" justify="center" :size="30" class="size-full">
<img class="w-220px h-100px" src="@/assets/logo/hula.png" alt="" />
<n-flex vertical align="center" :size="20" class="size-full pt-100px">
<div @mousemove="handleMouseMove" @mouseleave="handleMouseLeave" class="box">
<div id="computer" class="computer">
<img class="w-224px h-158px relative" src="@/assets/img/win.png" alt="" />
<div
style="background: rgba(80, 80, 80, 0.1)"
class="w-170px h-113px backdrop-blur-0 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"
alt="" />
</div>
</div>
<n-flex vertical align="center" :size="20">
<span class="text-(15px #707070)">版本{{ _pkg.version }}({{ osArch }})</span>
<span class="text-(15px #707070)">版本v{{ _pkg.version }}({{ osArch }})</span>
<span class="text-(15px #707070)">当前设备{{ osType }}{{ osVersion }}</span>
<n-flex vertical class="text-(12px #909090)" :size="8" align="center">
<span>Copyright © 2023-2024 nongyehong</span>
<span>Copyright © {{ currentYear - 1 }}-{{ currentYear }} nongyehong</span>
<span>All Rights Reserved.</span>
</n-flex>
</n-flex>
</n-flex>
</main>
</template>
<script setup lang="ts">
import pkg from '~/package.json'
import dayjs from 'dayjs'
import { type, arch, version } from '@tauri-apps/plugin-os'
const _pkg = reactive({
@ -26,6 +39,36 @@ const _pkg = reactive({
const osType = ref()
const osArch = ref()
const osVersion = ref()
// 使day.js
const currentYear = dayjs().year()
const element = ref<HTMLElement | null>(null)
/** 鼠标移动时,对元素进行旋转的指数 */
const multiple = 20
const transformElement = (x: number, y: number) => {
if (element.value) {
const box = element.value.getBoundingClientRect()
const calcX = -(y - box.y - box.height / 2) / multiple
const calcY = (x - box.x - box.width / 2) / multiple
element.value.style.transform = `rotateX(${calcX}deg) rotateY(${calcY}deg)`
}
}
const handleMouseMove = (event: MouseEvent) => {
window.requestAnimationFrame(() => {
transformElement(event.clientX, event.clientY)
})
}
const handleMouseLeave = () => {
window.requestAnimationFrame(() => {
if (element.value) {
element.value.style.transform = 'rotateX(0) rotateY(0)'
}
})
}
onMounted(() => {
osType.value = type()
@ -36,6 +79,7 @@ onMounted(() => {
let build_number = Number(parts[2])
osVersion.value = build_number > 22000 ? '11' : '10'
}
element.value = document.getElementById('computer')
})
</script>
@ -52,4 +96,20 @@ onMounted(() => {
color: #404040;
}
}
.box {
width: 240px;
height: 200px;
transform-style: preserve-3d;
perspective: 500px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
.computer {
position: relative;
transition: all 0.2s;
transform-style: preserve-3d;
}
}
</style>

View File

@ -24,7 +24,7 @@
</n-flex>
<!-- 系统设置 -->
<n-flex vertical class="text-(14px [--text-color])" :size="16">
<n-flex v-if="type() === 'windows'" vertical class="text-(14px [--text-color])" :size="16">
<span class="pl-10px">系统</span>
<n-flex class="item" :size="15" vertical>
@ -50,7 +50,7 @@
<span v-if="type() === 'windows'" class="w-full h-1px bg-[--line-color]"></span>
<!-- ESC关闭面板 -->
<n-flex align="center" justify="space-between">
<n-flex v-if="type() === 'windows'" align="center" justify="space-between">
<span>是否启用ESC关闭窗口</span>
<n-switch size="small" v-model:value="escClose" />

View File

@ -2,9 +2,7 @@
<!-- 主体内容 -->
<!-- // TODO 使 (nyh -> 2024-07-01 10:44:14)-->
<main>
<div
style="box-shadow: var(--shadow-enabled) 4px 4px var(--box-shadow-color)"
class="flex truncate p-[8px_20px_14px_20px] justify-between items-center gap-50px">
<div class="flex truncate p-[8px_20px_14px_20px] justify-between items-center gap-50px">
<n-flex :size="10" vertical class="truncate">
<p
v-if="!isEdit"
@ -40,7 +38,10 @@
<div class="h-1px bg-[--line-color]"></div>
<!-- 聊天信息框 -->
<div class="w-full p-[28px_16px] box-border" style="height: calc(100vh - 300px)">
<div
:class="{ 'shadow-inner': page.shadow }"
class="w-full p-[28px_16px] box-border"
style="height: calc(100vh - 300px)">
<n-flex :size="6">
<n-avatar
class="rounded-8px"
@ -60,11 +61,7 @@
<div class="h-1px bg-[--line-color]"></div>
<!-- 下半部分输入框以及功能栏 -->
<n-flex
vertical
:size="6"
style="box-shadow: var(--shadow-enabled) -4px 4px 0 rgba(0, 0, 0, 0.05)"
class="size-full p-[14px_22px] box-border">
<n-flex vertical :size="6" class="size-full p-[14px_22px] box-border">
<n-flex :size="26" class="options">
<n-popover v-for="(item, index) in features" :key="index" trigger="hover" :show-arrow="false" placement="top">
<template #trigger>
@ -84,7 +81,11 @@
import MsgInput from '@/components/rightBox/MsgInput.vue'
import Mitt from '@/utils/Bus.ts'
import { InputInst, NIcon } from 'naive-ui'
import { setting } from '@/stores/setting.ts'
import { storeToRefs } from 'pinia'
const settingStore = setting()
const { page } = storeToRefs(settingStore)
/** 是否是编辑模式 */
const isEdit = ref(false)
const inputInstRef = ref<InputInst | null>(null)
@ -159,6 +160,7 @@ onMounted(() => {
}
}
.options {
padding-left: 4px;
svg {
@apply size-24px cursor-pointer outline-none;
}

View File

@ -22,7 +22,7 @@ type ChatConfig = {
export const content: ChatConfig = {
system: [
{
title: `当前版本:${pkg.version}`,
title: `当前版本:v${pkg.version}`,
description: '已是最新版本',
features: <Button title={'检查更新'} icon={'refresh'} />
},

View File

@ -88,7 +88,7 @@ const handleLoginSuccess = async (e: any) => {
scanStatus.value.show = true
loadText.value = '登录中...'
delay(async () => {
await createWebviewWindow('HuLa', 'home', 960, 720, 'login', false)
await createWebviewWindow('HuLa', 'home', 960, 720, 'login', true)
settingStore.setAccountInfo({
avatar: e.avatar,
name: e.name,

View File

@ -38,10 +38,9 @@ export default defineConfig(({ mode }: ConfigEnv) => {
/**! 启动时候打印项目信息(不需要可关闭) */
atStartup(config, mode),
/**
* !
* defineProps解构语法
* vue3.5.0
* */
vue({ script: { propsDestructure: true } }),
vue(),
vueJsx(), // 开启jsx功能
unocss(), // 开启unocss
AutoImport({