mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: chat layout
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
export interface robot{
|
||||
description: string
|
||||
time: Date,
|
||||
id: string | number,
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface user{
|
||||
description: string, time: Date, id: number | string
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './model'
|
||||
export * from './schedule'
|
||||
export * from './platform'
|
||||
export * from './mcp'
|
||||
export * from './mcp'
|
||||
export * from './chatInfo'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollAreaRootProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaViewport,
|
||||
} from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
import ScrollBar from "./ScrollBar.vue"
|
||||
|
||||
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaRoot
|
||||
data-slot="scroll-area"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('relative', props.class)"
|
||||
>
|
||||
<ScrollAreaViewport
|
||||
data-slot="scroll-area-viewport"
|
||||
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
<slot />
|
||||
</ScrollAreaViewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaCorner />
|
||||
</ScrollAreaRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollAreaScrollbarProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes["class"] }>(), {
|
||||
orientation: "vertical",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn('flex touch-none p-px transition-colors select-none',
|
||||
orientation === 'vertical'
|
||||
&& 'h-full w-2.5 border-l border-l-transparent',
|
||||
orientation === 'horizontal'
|
||||
&& 'h-2.5 flex-col border-t border-t-transparent',
|
||||
props.class)"
|
||||
>
|
||||
<ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
class="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaScrollbar>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as ScrollArea } from "./ScrollArea.vue"
|
||||
export { default as ScrollBar } from "./ScrollBar.vue"
|
||||
@@ -16,6 +16,7 @@ export * from './components/input-group/index'
|
||||
export * from './components/kbd/index'
|
||||
export * from './components/label/index'
|
||||
export * from './components/radio-group/index'
|
||||
export * from './components/scroll-area/index'
|
||||
export * from './components/select/index'
|
||||
export * from './components/separator/index'
|
||||
export * from './components/sheet/index'
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class=" p-2 rounded-full bg-[#F9F9F9]">
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiRobotOutline"
|
||||
/>
|
||||
</div>
|
||||
<section>
|
||||
<sup class="font-semibold">
|
||||
{{ robotSay.type }}
|
||||
</sup>
|
||||
<p class="leading-7 text-muted-foreground">
|
||||
{{ robotSay.description }}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiRobotOutline } from '@mdi/js'
|
||||
import type {robot} from '@memoh/shared'
|
||||
|
||||
|
||||
const {robotSay}=defineProps<{
|
||||
robotSay: robot
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<p
|
||||
class="leading-7 not-first:mt-6 max-w-[90%] ml-auto text-muted-foreground bg-[#F9F9F9] p-4 rounded-xl rounded-tr-none
|
||||
"
|
||||
>
|
||||
{{ userSay.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { user } from '@memoh/shared'
|
||||
const { userSay } = defineProps<{
|
||||
userSay: user
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<template
|
||||
v-for="chatItem in chatList"
|
||||
:key="chatItem.id"
|
||||
>
|
||||
<UserChat
|
||||
v-if="chatItem.action === 'user'"
|
||||
:user-say="chatItem"
|
||||
/>
|
||||
<RobotChat
|
||||
v-if="chatItem.action === 'robot'"
|
||||
:robot-say="chatItem as robot"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UserChat from './UserChat/index.vue'
|
||||
import RobotChat from './RobotChat/index.vue'
|
||||
import { reactive } from 'vue'
|
||||
import type { user, robot } from '@memoh/shared'
|
||||
|
||||
// 模拟一下数据
|
||||
const chatList = reactive<(((user | robot) & { action: 'robot' | 'user' }))[]>([{
|
||||
description: 'fjiwofwofjewifwe', time: new Date, id: 2, action: 'user'
|
||||
}, {
|
||||
description: 'fjiwofwofjefwfewfwifwe', time: new Date, id: 1000, action: 'robot', type: 'Openai Gpt5'
|
||||
}, {
|
||||
description: 'fjiwofwofjewifwe', time: new Date, id: 2, action: 'user'
|
||||
}, {
|
||||
description: 'fjiwofwofjefwfewfwifwe', time: new Date, id: 1000, action: 'robot', type: 'Openai Gpt5'
|
||||
}])
|
||||
|
||||
</script>
|
||||
@@ -11,38 +11,40 @@
|
||||
/>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem class="hidden md:block">
|
||||
<!-- <BreadcrumbItem class="hidden md:block">
|
||||
<BreadcrumbLink href="#">
|
||||
Building Your Application
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbItem> -->
|
||||
<BreadcrumbSeparator class="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
|
||||
<BreadcrumbPage>对话</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div class="bg-muted/50 aspect-video rounded-xl" />
|
||||
<div class="bg-muted/50 aspect-video rounded-xl" />
|
||||
<div class="bg-muted/50 aspect-video rounded-xl" />
|
||||
</div>
|
||||
<div class="bg-muted/50 min-h-[100vh] flex-1 rounded-xl md:min-h-min" />
|
||||
</div>
|
||||
<main class="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<router-view v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</router-view>
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { SidebarTrigger, SidebarInset, Breadcrumb,
|
||||
import {
|
||||
SidebarTrigger, SidebarInset, Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator, } from '@memoh/ui'
|
||||
BreadcrumbSeparator,
|
||||
Separator
|
||||
} from '@memoh/ui'
|
||||
|
||||
const open = inject('sideBarIsOpen')
|
||||
</script>
|
||||
@@ -1,46 +1,38 @@
|
||||
<template>
|
||||
<aside>
|
||||
<SidebarProvider :open="open as boolean">
|
||||
<Sidebar>
|
||||
<SidebarHeader>
|
||||
<img
|
||||
src="../../../public/logo.png"
|
||||
width="80"
|
||||
class="m-auto"
|
||||
alt="logo.png"
|
||||
>
|
||||
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight text-center text-muted-foreground">
|
||||
Memoh
|
||||
</h4>
|
||||
<!-- <SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg">
|
||||
<div
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
>
|
||||
<GalleryVerticalEnd class="size-4" />
|
||||
</div>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">Acme Inc</span>
|
||||
<span class="truncate text-xs">Enterprise</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu> -->
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>
|
||||
对话操作
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem
|
||||
v-for="sidebarItem in sidebarInfo"
|
||||
:key="sidebarItem.title"
|
||||
>
|
||||
<aside class="[&_[data-state=collapsed]_.title-container]:hidden">
|
||||
<Sidebar
|
||||
collapsible="icon"
|
||||
>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<img
|
||||
src="../../../public/logo.png"
|
||||
width="80"
|
||||
class="m-auto"
|
||||
alt="logo.png"
|
||||
>
|
||||
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight text-center text-muted-foreground title-container">
|
||||
Memoh
|
||||
</h4>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>
|
||||
对话操作
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<Collapsible
|
||||
v-for="sidebarItem in sidebarInfo"
|
||||
:key="sidebarItem.title"
|
||||
class="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton>
|
||||
<SidebarMenuButton :tooltip="sidebarItem.title">
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="sidebarItem.icon"
|
||||
@@ -49,35 +41,35 @@
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter />
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
</Collapsible>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</aside>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
CollapsibleTrigger,
|
||||
Collapsible
|
||||
|
||||
} from '@memoh/ui'
|
||||
import { reactive, inject } from 'vue'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiRobot, mdiChatOutline, mdiCogBox, mdiListBox } from '@mdi/js'
|
||||
import { mdiRobot, mdiChatOutline, mdiCogBox, mdiListBox, mdiHome } from '@mdi/js'
|
||||
|
||||
|
||||
const open=inject('sideBarIsOpen')
|
||||
@@ -87,6 +79,11 @@ const sidebarInfo = reactive([{
|
||||
path: '/',
|
||||
icon: mdiChatOutline
|
||||
}, {
|
||||
title: '主页',
|
||||
path: '/',
|
||||
icon: mdiHome
|
||||
},
|
||||
{
|
||||
title: '模型配置',
|
||||
path: '/',
|
||||
icon: mdiRobot
|
||||
@@ -95,7 +92,7 @@ const sidebarInfo = reactive([{
|
||||
path: '/',
|
||||
icon: mdiCogBox
|
||||
}, {
|
||||
title: '调度规则',
|
||||
title: '平台',
|
||||
path: '/',
|
||||
icon: mdiListBox
|
||||
}])
|
||||
|
||||
@@ -1,22 +1,58 @@
|
||||
<template>
|
||||
<section>
|
||||
<MainLayout>
|
||||
<template #sidebar>
|
||||
<SideBar />
|
||||
</template>
|
||||
<template #main>
|
||||
<MainContainer />
|
||||
</template>
|
||||
</MainLayout>
|
||||
<section class="h-[calc(100vh-calc(var(--spacing)*20))] max-w-187 gap-8 w-full *:w-full m-auto flex flex-col">
|
||||
<!-- <ScrollArea class="flex-none w-full rounded-md border">
|
||||
<div class="p-4">
|
||||
<h4 class="mb-4 text-sm leading-none font-medium">
|
||||
Tags
|
||||
</h4>
|
||||
<template
|
||||
v-for="tag in 1000"
|
||||
:key="tag"
|
||||
>
|
||||
<div class="text-sm">
|
||||
{{ tag }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ScrollArea> -->
|
||||
<section class="flex-1 h-0">
|
||||
<ScrollArea class="max-h-full h-full w-full rounded-md border p-4">
|
||||
<ChatList />
|
||||
</ScrollArea>
|
||||
</section>
|
||||
<section class="flex-none relative">
|
||||
<Textarea
|
||||
class="pb-16 pt-4"
|
||||
placeholder="请输入想要获取的内容"
|
||||
/>
|
||||
<section
|
||||
class="absolute bottom-0 h-14 px-2 inset-x-0 flex items-center"
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
class="ml-auto"
|
||||
>
|
||||
发送
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiSendOutline"
|
||||
/>
|
||||
</Button>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MainLayout from '@/layout/mainLayout/index.vue'
|
||||
import SideBar from '@/components/Sidebar/index.vue'
|
||||
import MainContainer from '@/components/MainContainer/index.vue'
|
||||
import { provide,ref } from 'vue'
|
||||
import {
|
||||
ScrollArea,
|
||||
Textarea,
|
||||
Button
|
||||
} from '@memoh/ui'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiSendOutline } from '@mdi/js'
|
||||
import ChatList from '@/components/ChatList/index.vue'
|
||||
|
||||
|
||||
|
||||
provide('sideBarIsOpen',ref(true))
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<section>
|
||||
<MainLayout>
|
||||
<template #sidebar>
|
||||
<SideBar />
|
||||
</template>
|
||||
<template #main>
|
||||
<MainContainer />
|
||||
</template>
|
||||
</MainLayout>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MainLayout from '@/layout/mainLayout/index.vue'
|
||||
import SideBar from '@/components/Sidebar/index.vue'
|
||||
import MainContainer from '@/components/MainContainer/index.vue'
|
||||
import { provide,ref } from 'vue'
|
||||
|
||||
|
||||
provide('sideBarIsOpen',ref(true))
|
||||
</script>
|
||||
@@ -11,9 +11,14 @@ const routes = [
|
||||
path: '/login',
|
||||
component: () => import('@/pages/login/index.vue')
|
||||
}, {
|
||||
name: 'Chat',
|
||||
path: '/chat',
|
||||
component: () => import('@/pages/chat/index.vue'),
|
||||
name: 'Main',
|
||||
component: () => import('@/pages/mainSection/index.vue'),
|
||||
path: '/main',
|
||||
redirect:'/main/chat',
|
||||
children: [{
|
||||
path: 'chat',
|
||||
component: () => import('@/pages/chat/index.vue')
|
||||
}]
|
||||
}
|
||||
|
||||
]
|
||||
@@ -28,7 +33,7 @@ router.beforeEach((to) => {
|
||||
if (to.fullPath !== '/login') {
|
||||
return token ? true : { name: 'Login' }
|
||||
} else {
|
||||
return token ? { name: 'Chat' } : true
|
||||
return token ? { name: 'Main' } : true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user