feat: chat layout

This commit is contained in:
Quicy
2026-01-19 11:36:34 +08:00
parent 2f643e3c13
commit 85efecb736
14 changed files with 309 additions and 86 deletions
+10
View File
@@ -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
}
+2 -1
View File
@@ -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"
+1
View File
@@ -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>
+51 -54
View File
@@ -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
}])
+50 -14
View File
@@ -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>
+9 -4
View File
@@ -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
}
})