perf(component): 升级插件版本内容及其样式

This commit is contained in:
nongyehong
2024-09-13 17:49:21 +08:00
parent ce68fa1343
commit 8d65ca198f
53 changed files with 2325 additions and 1052 deletions
+1 -1
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>
+13 -13
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",
@@ -90,11 +90,11 @@
"prettier": "^3.3.2",
"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"
"typescript": "^5.6.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4",
"vite": "5.4.3",
"vue-tsc": "^2.1.4"
},
"config": {
"commitizen": {
+1015 -488
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+1 -1
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",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "hula"
version = "2.0.0"
version = "2.1.0"
description = "hula"
authors = ["nongyehong"]
license = ""
+1 -1
View File
@@ -1,6 +1,6 @@
{
"productName": "HuLa",
"version": "2.0.0",
"version": "2.1.0",
"identifier": "com.tauri.build",
"build": {
"beforeDevCommand": "pnpm dev",
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

-2
View File
@@ -76,8 +76,6 @@ const commonTheme: GlobalThemeOverrides = {
},
Tabs: {
tabTextColorSegment: '#707070',
tabTextColorActiveSegment: '#13987f',
tabTextColorHoverSegment: '#13987f',
tabPaddingMediumSegment: '4px'
},
Popover: {
+1 -2
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
})
+16 -10
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"
@@ -58,15 +58,6 @@
<!-- 发送按钮 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 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>
<n-config-provider :theme="lightTheme">
<n-button-group size="small" class="pr-20px">
<n-button
@@ -91,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>
+4 -6
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
}
+24 -32
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))
@@ -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)
}
}
+7 -1
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,
/** 下载中 */
+63 -15
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"
: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]"
@@ -138,30 +150,66 @@
</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 { useTemplateRef } from '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) => {
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>
@@ -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 absolute top-6px right-6px 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>
@@ -0,0 +1,280 @@
<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 as STO.Plugins<PluginEnum>[]" :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.value"
: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) => {
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
updatePlugins(plugin)
}
}, 500)
}
const handleUnload = (plugin) => {
plugin.state = PluginEnum.UNINSTALLING
setTimeout(() => {
handleDelete(plugin)
plugin.isAdd = false
plugin.state = PluginEnum.NOT_INSTALLED
updatePlugins(plugin)
}, 2000)
}
const handleDelete = (plugin) => {
// 找到 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) => {
// 判断如果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>
@@ -0,0 +1,293 @@
<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.value"
: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) => {
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
updatePlugins(plugin)
}
}, 500)
}
const handleUnload = (plugin) => {
plugin.state = PluginEnum.UNINSTALLING
setTimeout(() => {
handleDelete(plugin)
plugin.isAdd = false
plugin.state = PluginEnum.NOT_INSTALLED
updatePlugins(plugin)
}, 2000)
}
const handleDelete = (plugin) => {
// 找到 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) => {
// 判断如果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>
@@ -0,0 +1,94 @@
<template>
<!-- 弹出框 -->
<n-modal v-model:show="isShow" :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>
+98 -52
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,99 @@ 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,
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,
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,
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,
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,
size: {
minWidth: 780,
width: 980,
height: 800
},
window: {
resizable: true
}
}
])
export { itemsBottom, moreList, pluginsList }
+6 -8
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
}
+41 -24
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>
)
}}
+1
View File
@@ -11,6 +11,7 @@
.left {
background: var(--left-bg-color);
//background: #64a29c;
}
.top-action,
+19 -9
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
}
})
+17 -10
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
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: STO.Plugins<PluginEnum>[] = [
{
url: 'message',
icon: 'message',
iconAction: 'message-action'
},
{
url: 'friendsList',
icon: 'avatar',
iconAction: 'avatar-action'
}
]
const menuTop = ref<STO.Plugins<PluginEnum>[]>(initialState)
onBeforeMount(() => {
if (!localStorage.getItem(StoresEnum.MENUTOP)) {
menuTop.value.push(pluginType)
}
})
return {
menuTop
}
},
{
share: {
enable: true,
initialize: true
}
}
)
+1 -1
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
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
}
}
)
+1 -10
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'
}
}),
+4
View File
@@ -92,6 +92,8 @@ html, body, svg {
--bate-bg: #d9ccf7;
// 卡片hover样式
--card-bg: #13987f60;
// 安装进度条样式
--progress-bg: #e0e9fc;
}
html[data-theme='dark'] {
@@ -171,6 +173,8 @@ html[data-theme='dark'] {
--bate-bg: #403555;
// 卡片hover样式
--card-bg: #13987f20;
// 安装进度条样式
--progress-bg: transparent;
}
/**! end */
// 线性动画
+58 -56
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']
}
}
+41 -6
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
+40 -9
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,43 @@ 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
} & OPT.L.Common
}
+1 -1
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}`
)
+64 -4
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>
@@ -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" />
@@ -160,6 +160,7 @@ onMounted(() => {
}
}
.options {
padding-left: 4px;
svg {
@apply size-24px cursor-pointer outline-none;
}
@@ -22,7 +22,7 @@ type ChatConfig = {
export const content: ChatConfig = {
system: [
{
title: `当前版本:${pkg.version}`,
title: `当前版本:v${pkg.version}`,
description: '已是最新版本',
features: <Button title={'检查更新'} icon={'refresh'} />
},
+2 -3
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({