feat(component): 新增GPT页面设置功能

This commit is contained in:
nongyehong
2024-06-29 23:54:34 +08:00
parent 5d9794cdb6
commit 4c85b4afcc
14 changed files with 1084 additions and 925 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_93.eea70ae0cbccb2d5c48884bf0db72efb.js"></script>
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_95.0ab6745fae3ccd4f96f945bab1f8bd0d.js"></script>
</head>
<body>
+650 -728
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -4,17 +4,17 @@ use window_shadows::set_shadow;
/// 设置窗口属性
pub fn set_window_attribute<R: Runtime>(app: &tauri::App<R>) {
for (_, window) in app.windows() {
// 设置窗口阴影和圆角
// 设置窗口阴影和圆角 win11默认有圆角效果,这里win10需要取舍阴影效果,阴影效果导致win10窗口拖动卡顿
#[cfg(any(windows, target_os = "macos"))]
set_shadow(&window, true).unwrap();
// // 设置窗口的磨砂背景
// #[cfg(target_os = "macos")]
// window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
// .expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
//
// #[cfg(target_os = "windows")]
// window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
// .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
// // 设置窗口的磨砂背景
// #[cfg(target_os = "macos")]
// window_vibrancy::apply_acrylic(&window, NSVisualEffectMaterial::Sidebar)
// .expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
//
// #[cfg(target_os = "windows")]
// window_vibrancy::apply_acrylic(&window, Some((255, 255, 255, 1)))
// .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
}
}
}
+8
View File
@@ -105,6 +105,14 @@ const commonTheme: GlobalThemeOverrides = {
iconColorLoading: '#13987f',
loadingColor: '#13987f',
borderRadius: '8px'
},
Slider: {
handleSize: '12px',
fontSize: '10px',
markFontSize: '8px',
fillColor: '#13987f',
fillColorHover: '#13987f',
indicatorBorderRadius: '8px'
}
}
+13 -1
View File
@@ -44,7 +44,19 @@ const routes: Array<RouteRecordRaw> = [
{
path: '/robot',
name: 'robot',
component: () => import('@/views/home-window/robot/index.vue')
component: () => import('@/views/home-window/robot/index.vue'),
children: [
{
path: '/chat',
name: 'chat',
component: () => import('@/views/home-window/robot/views/Chat.vue')
},
{
path: '/chatSettings',
name: 'chatSettings',
component: () => import('@/views/home-window/robot/views/chatSettings/index.vue')
}
]
},
{
path: '/mail',
-1
View File
@@ -34,7 +34,6 @@ declare module 'vue' {
NDropdown: (typeof import('naive-ui'))['NDropdown']
NEllipsis: (typeof import('naive-ui'))['NEllipsis']
NFlex: (typeof import('naive-ui'))['NFlex']
NGradientText: (typeof import('naive-ui'))['NGradientText']
NIcon: (typeof import('naive-ui'))['NIcon']
NIconWrapper: (typeof import('naive-ui'))['NIconWrapper']
NImage: (typeof import('naive-ui'))['NImage']
@@ -1,152 +0,0 @@
<template>
<n-flex vertical :size="0" class="flex-1 truncate shadow-md select-none text-[--text-color]">
<!-- 右上角操作栏 -->
<ActionBar class="w-full" :shrink="false" :current-label="appWindow.label" :top-win-label="appWindow.label" />
<!-- 主体内容 -->
<main>
<div class="flex flex-1 truncate p-[14px_20px] justify-between items-center gap-50px">
<n-flex :size="10" vertical class="truncate">
<p
v-if="!isEdit"
@click="handleEdit"
class="text-(22px [--chat-text-color]) truncate font-bold hover:underline cursor-pointer">
{{ currentChat.title }}
</p>
<n-input
v-else
@blur="handleBlur"
ref="inputInstRef"
v-model:value="currentChat.title"
clearable
placeholder="输入标题"
type="text"
size="tiny"
style="width: 200px"
class="h-22px lh-22px rounded-6px">
</n-input>
<p class="text-(14px #707070)">共0条对话</p>
</n-flex>
<n-flex class="min-w-fit">
<div class="right-btn" @click="handleEdit">
<svg><use href="#edit"></use></svg>
</div>
<div class="right-btn">
<svg><use href="#Sharing"></use></svg>
</div>
</n-flex>
</div>
<div class="h-1px bg-[--line-color]"></div>
<!-- 聊天信息框 -->
<div class="w-full shadow-inner p-[28px_16px] box-border" style="height: calc(100vh - 300px)">
<n-flex :size="6">
<n-avatar
class="rounded-8px"
src="https://img1.baidu.com/it/u=3613958228,3522035000&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500" />
<n-flex vertical justify="space-between">
<p class="text-(12px [--chat-text-color])">GPT-4</p>
<!-- 气泡样式 -->
<ContextMenu>
<div style="white-space: pre-wrap" class="bubble">
<span v-html="'你好,我是GPT4机器人,很高兴为您服务。'"></span>
</div>
</ContextMenu>
</n-flex>
</n-flex>
</div>
<div class="h-1px bg-[--line-color]"></div>
<!-- 下半部分输入框以及功能栏 -->
<n-flex vertical :size="6" class="size-full p-[14px_22px] box-border shadow-inner">
<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>
<svg><use :href="`#${item.icon}`"></use></svg>
</template>
<p>{{ item.label }}</p>
</n-popover>
</n-flex>
<div class="flex flex-col items-end gap-6px">
<MsgInput />
</div>
</n-flex>
</main>
</n-flex>
</template>
<script setup lang="ts">
import { appWindow } from '@tauri-apps/api/window'
import { useWindowState } from '@/hooks/useWindowState.ts'
import MsgInput from '@/components/rightBox/MsgInput.vue'
import Mitt from '@/utils/Bus.ts'
import { InputInst } from 'naive-ui'
useWindowState(appWindow.label)
/** 是否是编辑模式 */
const isEdit = ref(false)
const inputInstRef = ref<InputInst | null>(null)
/** 当前聊天的标题和id */
const currentChat = ref({
id: 0,
title: ''
})
const features = ref([
{
icon: 'SmilingFace',
label: '所有人物'
},
{
icon: 'MagicWand',
label: '快捷指令'
},
{
icon: 'explosion',
label: '增强插件'
},
{
icon: 'robot-action',
label: 'GPT4'
}
])
const handleBlur = () => {
isEdit.value = false
if (currentChat.value.title === '') {
currentChat.value.title = '新的聊天'
}
Mitt.emit('update-chat-title', { title: currentChat.value.title, id: currentChat.value.id })
}
const handleEdit = () => {
isEdit.value = true
nextTick(() => {
inputInstRef.value?.select()
})
}
onMounted(() => {
Mitt.on('chat-active', (e) => {
const { title, id } = e
currentChat.value.title = title
currentChat.value.id = id
})
})
</script>
<style lang="scss" scoped>
@import '@/styles/scss/chat-main';
.right-btn {
@apply size-fit cursor-pointer bg-[--chat-bt-color] color-[--chat-text-color] rounded-8px shadow-md p-[10px_11px];
svg {
@apply size-18px;
}
}
.options {
svg {
@apply size-24px cursor-pointer outline-none;
}
}
</style>
+2 -2
View File
@@ -9,6 +9,6 @@
</div>
</template>
<script setup lang="ts">
import Left from './components/Left.vue'
import Right from './components/Right.vue'
import Left from './layout/Left.vue'
import Right from './layout/Right.vue'
</script>
@@ -34,7 +34,7 @@
</n-flex>
<!-- 会话列表 -->
<n-scrollbar ref="scrollbar" style="max-height: calc(100vh - 266px); padding-right: 8px">
<VueDraggable v-model="chatList" :animation="150" target=".sort-target">
<VueDraggable dragClass="outline-none" v-model="chatList" :animation="150" target=".sort-target">
<TransitionGroup name="list" tag="div" style="padding: 4px" class="sort-target flex flex-col-center gap-12px">
<n-flex
vertical
@@ -44,24 +44,30 @@
@click="handleActive(item)"
:class="{ 'outline-dashed outline-2 outline-#13987f outline-offset-1': activeItem === item.id }"
class="chat-item">
<div class="absolute flex flex-col gap-14px w-full p-[8px_14px] box-border">
<n-flex justify="space-between" align="center" :size="0" class="leading-22px">
<n-ellipsis
style="width: calc(100% - 20px)"
class="text-(14px [--chat-text-color]) truncate font-semibold select-none">
{{ item.title }}
</n-ellipsis>
<svg
@click.stop="deleteChat(item)"
class="color-[--chat-text-color] size-20px opacity-0 absolute right-0px top-4px">
<use href="#squareClose"></use>
</svg>
</n-flex>
<n-flex justify="space-between" align="center" :size="0" class="text-(12px #909090)">
<p>0条对话</p>
<p>{{ item.time }}</p>
</n-flex>
</div>
<ContextMenu
:menu="menuList"
:special-menu="specialMenuList"
class="msg-box w-full h-75px mb-5px"
@select="$event.click(item)">
<div class="absolute flex flex-col gap-14px w-full p-[8px_14px] box-border">
<n-flex justify="space-between" align="center" :size="0" class="leading-22px">
<n-ellipsis
style="width: calc(100% - 20px)"
class="text-(14px [--chat-text-color]) truncate font-semibold select-none">
{{ item.title }}
</n-ellipsis>
<svg
@click.stop="deleteChat(item)"
class="color-[--chat-text-color] size-20px opacity-0 absolute right-0px top-4px">
<use href="#squareClose"></use>
</svg>
</n-flex>
<n-flex justify="space-between" align="center" :size="0" class="text-(12px #909090)">
<p>0条对话</p>
<p>{{ item.time }}</p>
</n-flex>
</div>
</ContextMenu>
</n-flex>
</TransitionGroup>
</VueDraggable>
@@ -70,10 +76,20 @@
<!-- 底部选项栏 -->
<n-flex justify="space-between" align="center" class="m-[auto_4px_10px_4px]">
<div
class="bg-[--chat-bt-color] color-[--chat-text-color] size-fit p-[8px_9px] rounded-8px shadow-md cursor-pointer">
<svg class="size-18px"><use href="#settings"></use></svg>
</div>
<n-flex :size="12" align="center">
<div
@click="jump"
class="bg-[--chat-bt-color] color-[--chat-text-color] size-fit p-[8px_9px] rounded-8px shadow-md cursor-pointer">
<svg class="size-18px"><use href="#settings"></use></svg>
</div>
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/nongyehong/HuLa-IM-Tauri"
class="bg-[--chat-bt-color] color-[--chat-text-color] size-fit p-[8px_9px] rounded-8px shadow-md cursor-pointer">
<svg class="size-18px"><use href="#github"></use></svg>
</a>
</n-flex>
<n-flex
:size="4"
@@ -92,6 +108,7 @@ import { storeToRefs } from 'pinia'
import { NIcon, VirtualListInst } from 'naive-ui'
import Mitt from '@/utils/Bus.ts'
import { VueDraggable } from 'vue-draggable-plus'
import router from '@/router'
const settingStore = setting()
const { login } = storeToRefs(settingStore)
@@ -119,14 +136,56 @@ const chatList = ref([
time: '2022-01-01 12:00:00'
}
])
const menuList = ref<OPT.RightMenu[]>([
{
label: '置顶',
icon: 'topping',
click: (item: any) => {
const index = chatList.value.findIndex((e) => e.id === item.id)
//
if (index !== 0) {
//
const temp = chatList.value[index]
chatList.value[index] = chatList.value[0]
chatList.value[0] = temp
}
}
},
{
label: '打开独立聊天窗口',
icon: 'freezing-line-column',
click: (item: any) => {
console.log(item)
}
}
])
const specialMenuList = ref<OPT.RightMenu[]>([
{
label: '删除',
icon: 'delete',
click: (item: any) => {
deleteChat(item)
}
}
])
/** 跳转到设置 */
const jump = () => {
router.push('/chatSettings')
activeItem.value = 0
}
/** 选中会话 */
const handleActive = (item: any) => {
activeItem.value = item.id
nextTick(() => {
Mitt.emit('chat-active', item)
router.push('/chat').then(() => {
nextTick(() => {
Mitt.emit('chat-active', item)
})
})
}
/** 添加会话 */
const add = () => {
const id = chatList.value.length + 1
chatList.value.push({ id: id, title: `新的聊天${id}`, time: '2022-01-01 12:00:00' })
@@ -136,6 +195,7 @@ const add = () => {
})
}
/** 删除会话 */
const deleteChat = (item: any) => {
// keyitems
const index = chatList.value.indexOf(item)
@@ -153,18 +213,20 @@ const deleteChat = (item: any) => {
nextTick(() => {
add()
//
// TODO (nyh -> 2024-06-27 18:52:11)
activeItem.value = chatList.value[0].id
handleActive(chatList.value[0])
window.$message.success(`已删除 ${item.title}`, {
icon: () => h(NIcon, null, { default: () => h('svg', null, [h('use', { href: '#face' })]) })
})
})
}
//
activeItem.value = chatList.value[chatList.value.length - 1].id
handleActive(chatList.value[chatList.value.length - 1])
}
window.$message.success(`已删除 ${item.title}`, {
icon: () => h(NIcon, null, { default: () => h('svg', null, [h('use', { href: '#face' })]) })
})
}
window.$message.success(`已删除 ${item.title}`, {
icon: () => h(NIcon, null, { default: () => h('svg', null, [h('use', { href: '#face' })]) })
})
}
}
@@ -178,6 +240,9 @@ onMounted(() => {
}
})
})
Mitt.on('return-chat', () => {
handleActive(chatList.value[0])
})
})
</script>
<style scoped lang="scss">
@@ -0,0 +1,14 @@
<template>
<n-flex vertical :size="0" class="flex-1 truncate shadow-md select-none text-[--text-color]">
<!-- 右上角操作栏 -->
<ActionBar class="w-full" :shrink="false" :current-label="appWindow.label" :top-win-label="appWindow.label" />
<RouterView />
</n-flex>
</template>
<script setup lang="ts">
import { appWindow } from '@tauri-apps/api/window'
import { useWindowState } from '@/hooks/useWindowState.ts'
useWindowState(appWindow.label)
</script>
+144
View File
@@ -0,0 +1,144 @@
<template>
<!-- 主体内容 -->
<main>
<div class="flex truncate p-[14px_20px] justify-between items-center gap-50px">
<n-flex :size="10" vertical class="truncate">
<p
v-if="!isEdit"
@click="handleEdit"
class="text-(22px [--chat-text-color]) truncate font-bold hover:underline cursor-pointer">
{{ currentChat.title }}
</p>
<n-input
v-else
@blur="handleBlur"
ref="inputInstRef"
v-model:value="currentChat.title"
clearable
placeholder="输入标题"
type="text"
size="tiny"
style="width: 200px"
class="h-22px lh-22px rounded-6px">
</n-input>
<p class="text-(14px #707070)">共0条对话</p>
</n-flex>
<n-flex class="min-w-fit">
<div class="right-btn" @click="handleEdit">
<svg><use href="#edit"></use></svg>
</div>
<div class="right-btn">
<svg><use href="#Sharing"></use></svg>
</div>
</n-flex>
</div>
<div class="h-1px bg-[--line-color]"></div>
<!-- 聊天信息框 -->
<div class="w-full shadow-inner p-[28px_16px] box-border" style="height: calc(100vh - 300px)">
<n-flex :size="6">
<n-avatar
class="rounded-8px"
src="https://img1.baidu.com/it/u=3613958228,3522035000&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500" />
<n-flex vertical justify="space-between">
<p class="text-(12px [--chat-text-color])">GPT-4</p>
<!-- 气泡样式 -->
<ContextMenu>
<div style="white-space: pre-wrap" class="bubble select-text">
<span v-html="'你好,我是GPT4机器人,很高兴为您服务。'"></span>
</div>
</ContextMenu>
</n-flex>
</n-flex>
</div>
<div class="h-1px bg-[--line-color]"></div>
<!-- 下半部分输入框以及功能栏 -->
<n-flex vertical :size="6" class="size-full p-[14px_22px] box-border shadow-inner">
<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>
<svg><use :href="`#${item.icon}`"></use></svg>
</template>
<p>{{ item.label }}</p>
</n-popover>
</n-flex>
<div class="flex flex-col items-end gap-6px">
<MsgInput />
</div>
</n-flex>
</main>
</template>
<script setup lang="ts">
import MsgInput from '@/components/rightBox/MsgInput.vue'
import Mitt from '@/utils/Bus.ts'
import { InputInst } from 'naive-ui'
/** 是否是编辑模式 */
const isEdit = ref(false)
const inputInstRef = ref<InputInst | null>(null)
/** 当前聊天的标题和id */
const currentChat = ref({
id: 0,
title: ''
})
const features = ref([
{
icon: 'SmilingFace',
label: '所有人物'
},
{
icon: 'MagicWand',
label: '快捷指令'
},
{
icon: 'explosion',
label: '增强插件'
},
{
icon: 'robot-action',
label: 'GPT4'
}
])
const handleBlur = () => {
isEdit.value = false
if (currentChat.value.title === '') {
currentChat.value.title = '新的聊天'
}
Mitt.emit('update-chat-title', { title: currentChat.value.title, id: currentChat.value.id })
}
const handleEdit = () => {
isEdit.value = true
nextTick(() => {
inputInstRef.value?.select()
})
}
onMounted(() => {
Mitt.on('chat-active', (e) => {
const { title, id } = e
currentChat.value.title = title
currentChat.value.id = id
})
})
</script>
<style scoped lang="scss">
@import '@/styles/scss/chat-main';
.right-btn {
@apply size-fit cursor-pointer bg-[--chat-bt-color] color-[--chat-text-color] rounded-8px shadow-md p-[10px_11px];
svg {
@apply size-18px;
}
}
.options {
svg {
@apply size-24px cursor-pointer outline-none;
}
}
</style>
@@ -0,0 +1,45 @@
import pkg from '~/package.json'
import { Button, Select, Slider, Switch } from './model.tsx'
type ChatConfig = {
system: {
title: string
description?: string
features: JSX.Element
}[]
}
export const content: ChatConfig = {
system: [
{
title: `当前版本:${pkg.version}`,
description: '已是最新版本',
features: Button('检查更新', 'refresh')
},
{
title: '发送键',
features: Select([
{ label: 'Enter', value: 'Enter' },
{ label: 'Ctrl + Enter', value: 'Ctrl+Enter' }
])
},
{
title: '主题',
features: Select([
{ label: '亮色', value: 'light' },
{ label: '暗黑模式', value: 'dark' },
{ label: '跟随系统', value: 'auto' }
])
},
{
title: '字体大小',
description: '聊天内容的字体大小',
features: Slider(20, 12)
},
{
title: '自动生成标题',
description: '根据对话内容生成合适的标题',
features: Switch()
}
]
}
@@ -0,0 +1,54 @@
<template>
<div class="flex truncate p-[14px_20px] justify-between items-center gap-50px">
<n-flex :size="10" vertical class="truncate">
<p class="text-(22px [--chat-text-color]) truncate font-bold">设置</p>
<p class="text-(14px #707070)">所有设置选项</p>
</n-flex>
<n-flex class="min-w-fit">
<div @click="handleClose" class="right-btn">
<svg><use href="#close"></use></svg>
</div>
</n-flex>
</div>
<div class="h-1px bg-[--line-color]"></div>
<!-- 设置的主体内容 -->
<div class="flex flex-1 shadow-inner p-20px">
<n-flex vertical class="w-full h-fit border-(solid 1px [--line-color]) shadow-md rounded-8px p-10px">
<n-flex vertical justify="center" v-for="(item, index) in content.system" :key="index">
<n-flex justify="space-between" :size="0" align="center" class="p-8px">
<n-flex vertical :size="4">
<p class="text-(15px [--chat-text-color]) font-bold">{{ item.title }}</p>
<p v-if="item.description" class="text-(12px [--chat-text-color])">{{ item.description }}</p>
</n-flex>
<component :is="item.features" />
</n-flex>
<div v-if="index !== content.system.length - 1" class="h-1px bg-[--line-color]"></div>
</n-flex>
</n-flex>
</div>
</template>
<script setup lang="tsx">
import router from '@/router'
import Mitt from '@/utils/Bus.ts'
import { content } from './config.tsx'
const handleClose = () => {
router.push('/chat').then(() => {
nextTick(() => {
Mitt.emit('return-chat')
})
})
}
</script>
<style scoped lang="scss">
.right-btn {
@apply size-fit cursor-pointer bg-[--chat-bt-color] color-[--chat-text-color] rounded-8px shadow-md p-[10px_11px];
svg {
@apply size-18px;
}
}
</style>
@@ -0,0 +1,48 @@
import { NButton, NFlex, NSelect, NSlider, NSwitch } from 'naive-ui'
export const Button = (title: string, icon: string) => {
return (
<>
<NButton quaternary>
<NFlex justify="center" align="center" size={6}>
<svg class={'size-16px'}>
<use href={`#${icon}`}></use>
</svg>
{title}
</NFlex>
</NButton>
</>
)
}
export const Select = (content: any[]) => {
return (
<>
<NSelect class={'w-120px rounded-8px'} options={content} value={content[0].value}></NSelect>
</>
)
}
export const Slider = (max: number, min: number) => {
const formatTooltip = (value: number) => `${value}px`
return (
<>
<NSlider
class={'w-160px'}
defaultValue={14}
formatTooltip={formatTooltip}
showTooltip={true}
max={max}
min={min}
step={1}></NSlider>
</>
)
}
export const Switch = () => {
return (
<>
<NSwitch></NSwitch>
</>
)
}