改进 首页和输入框

This commit is contained in:
ivamp 2024-10-10 11:27:36 +08:00
parent 97574df592
commit c2a5d6340b
9 changed files with 258 additions and 11 deletions

1
src/components.d.ts vendored
View File

@ -14,6 +14,7 @@ declare module 'vue' {
Chat: typeof import('./components/chat/Chat.vue')['default'] Chat: typeof import('./components/chat/Chat.vue')['default']
ChatMenu: typeof import('./components/ChatMenu.vue')['default'] ChatMenu: typeof import('./components/ChatMenu.vue')['default']
ChatSettings: typeof import('./components/settings/ChatSettings.vue')['default'] ChatSettings: typeof import('./components/settings/ChatSettings.vue')['default']
Layout: typeof import('./components/chat/Layout.vue')['default']
LeftSettings: typeof import('./components/settings/LeftSettings.vue')['default'] LeftSettings: typeof import('./components/settings/LeftSettings.vue')['default']
LibrarySettings: typeof import('./components/settings/LibrarySettings.vue')['default'] LibrarySettings: typeof import('./components/settings/LibrarySettings.vue')['default']
Lottie: typeof import('./components/Lottie.vue')['default'] Lottie: typeof import('./components/Lottie.vue')['default']

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col items-center justify-between"> <div class="flex flex-col items-center justify-between overflow-hidden">
<div class="min-w-full md:w-4/5"> <div class="min-w-full md:w-4/5">
<n-scrollbar <n-scrollbar
style="max-height: calc(100vh - (var(--header-height) * 3.5))" style="max-height: calc(100vh - (var(--header-height) * 3.5))"
@ -11,7 +11,10 @@
v-if="!chatMessages?.length" v-if="!chatMessages?.length"
> >
<div class="text-5xl"> <div class="text-5xl">
<n-gradient-text type="success" class="pr-3 pb-2 pt-2 max-w-96 lg:max-w-full overflow-ellipsis overflow-hidden whitespace-nowrap"> <n-gradient-text
type="success"
class="pr-3 pb-2 pt-2 max-w-96 lg:max-w-full overflow-ellipsis overflow-hidden whitespace-nowrap"
>
<!-- 你好{{ userStore.user.name }} --> <!-- 你好{{ userStore.user.name }} -->
你好 你好
</n-gradient-text> </n-gradient-text>
@ -57,8 +60,8 @@
</n-scrollbar> </n-scrollbar>
</div> </div>
<div class="w-full pt-2 relative"> <div class="min-w-full pt-2 relative " style="max-height: 120px">
<div class="fixed bottom-0 left-0 right-0 pr-2 pl-2"> <div class="relative bottom-0 pr-2 pl-2 min-w-full" ref="textContainer">
<div <div
class="mx-auto w-2xl max-w-2xl text-center mb-3 animate__animated animate__pulse text-lg" class="mx-auto w-2xl max-w-2xl text-center mb-3 animate__animated animate__pulse text-lg"
v-if="isMobile && chatStore.toolName != ''" v-if="isMobile && chatStore.toolName != ''"
@ -87,6 +90,8 @@
contenteditable="true" contenteditable="true"
placeholder="请输入文本..." placeholder="请输入文本..."
class="input-text max-w-full outline-none text-lg text-pretty pl-2 min-h-6" class="input-text max-w-full outline-none text-lg text-pretty pl-2 min-h-6"
@change="updateInputPosition"
@keyup="updateInputPosition"
@keydown="onKeydown" @keydown="onKeydown"
@input="updateInputHeight" @input="updateInputHeight"
@compositionstart="handleCompositionStart" @compositionstart="handleCompositionStart"
@ -204,7 +209,6 @@
</n-modal> </n-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useUserStore } from "../../stores/user";
import { onMounted, onUnmounted, Ref, ref } from "vue"; import { onMounted, onUnmounted, Ref, ref } from "vue";
import { import {
SendOutline, SendOutline,
@ -260,6 +264,7 @@ const compositionStart = ref(false);
const inputContainer: any = ref(null); const inputContainer: any = ref(null);
const inputText: any = ref(null); const inputText: any = ref(null);
const actionContainer: any = ref(null); const actionContainer: any = ref(null);
const textContainer: any = ref(null);
const isPlaceholderVisible = ref(true); const isPlaceholderVisible = ref(true);
const triggerTimes = ref(0); const triggerTimes = ref(0);
const showSendBtn = ref(false); const showSendBtn = ref(false);
@ -301,7 +306,16 @@ const updateInputContent = (content: string) => {
updateInputHeight(); updateInputHeight();
}; };
const updateInputPosition = () => {
const container = inputContainer.value;
const text = textContainer.value;
// bottom
text.style.bottom = `${container.offsetHeight - 80}px`;
};
function onKeydown(e: KeyboardEvent) { function onKeydown(e: KeyboardEvent) {
updateInputPosition();
// Esc // Esc
if (e.code === "Escape") { if (e.code === "Escape") {
assistantStore.selectMenu = false; assistantStore.selectMenu = false;
@ -336,6 +350,8 @@ function updateInputHeight() {
return; return;
} }
updateInputPosition();
const input = inputText.value; const input = inputText.value;
const container = inputContainer.value; const container = inputContainer.value;
const action = actionContainer.value; const action = actionContainer.value;
@ -398,10 +414,14 @@ function updateInputHeight() {
} }
function handleCompositionStart() { function handleCompositionStart() {
updateInputPosition();
compositionStart.value = true; compositionStart.value = true;
} }
function handleCompositionEnd() { function handleCompositionEnd() {
updateInputPosition();
compositionStart.value = false; compositionStart.value = false;
} }

View File

@ -0,0 +1,144 @@
<template>
<n-layout :has-sider="!isMobile">
<n-layout-sider :native-scrollbar="false" v-if="!isMobile">
<n-scrollbar :style="'max-height: ' + clientHeight + 'px'">
<n-list hoverable clickable v-if="chatStore.chats?.length">
<n-list-item
v-for="c in chatStore.chats"
:key="c.id"
:class="
c.id === chatStore.currentChat?.id
? ' bg-gray-100 dark:bg-gray-700'
: ''
"
@click="viewChat(c.id ?? 0)"
>
<n-thing>
<div class="flex justify-between items-center">
<div>
{{ c.name }}
</div>
<div>
<n-button
quaternary
circle
type="warning"
@click.stop="deleteChat(c.id ?? 0)"
>
<template #icon>
<n-icon size="16" class="cursor-pointer">
<TrashBinOutline />
</n-icon>
</template>
</n-button>
</div>
</div>
</n-thing>
</n-list-item>
</n-list>
<div v-else>
<n-result
status="404"
title="你还没有对话"
description="不如现在就开始?"
>
</n-result>
</div>
</n-scrollbar>
</n-layout-sider>
<n-layout>
<n-layout-content>
<slot />
</n-layout-content>
</n-layout>
</n-layout>
</template>
<script setup lang="ts">
import { useUserStore } from "@/stores/user";
import { useDialog } from "naive-ui";
import { TrashBinOutline } from "@vicons/ionicons5";
import getApi from "../../plugins/api";
import { useChatStore } from "../../stores/chat";
import router from "@/router";
import { ref } from "vue";
import { EntityAssistant, EntityChat } from "@/api";
import { useIsMobile } from "@/utils/composables";
import { useAppStore } from "@/stores/app";
const clientHeight = ref(100);
const isMobile = useIsMobile();
const dialog = useDialog();
const chatStore = useChatStore();
const showSettingsDialog = ref(false);
const currentChatId = ref();
const currentChat: Ref<EntityChat> = ref({});
const assistants: Ref<EntityAssistant[]> = ref([]);
const assistantSelects = ref([
{
label: "不使用",
value: null,
},
]);
async function getChats() {
chatStore.chats = (await getApi().Chat.apiV1ChatsGet()).data.data;
}
const viewChat = (chatId: number) => {
router.push("/chat/" + chatId);
};
const deleteChat = async (chatId: number) => {
dialog.warning({
title: "删除对话",
content: "删除后,将不能恢复",
positiveText: "确定",
negativeText: "取消",
onPositiveClick: async () => {
await getApi().Chat.apiV1ChatsIdDelete(chatId);
await getChats();
},
});
};
const getAssistants = async () => {
assistants.value =
(await getApi().Assistant.apiV1AssistantsGet()).data.data ?? [];
assistantSelects.value = [];
assistantSelects.value.push({
label: "不使用",
value: null,
});
assistants.value.forEach((a) => {
if (a.name !== undefined && a.id !== undefined) {
return assistantSelects.value.push({
label: a.name,
// @ts-ignore
value: a.id,
});
}
});
};
const appStore = useAppStore();
const updateClientHeight = () => {
clientHeight.value = window.innerHeight - appStore.headerHeight;
};
onMounted(() => {
updateClientHeight();
window.addEventListener("resize", updateClientHeight);
});
onUnmounted(() => {
window.removeEventListener("resize", updateClientHeight);
});
getChats();
getAssistants();
</script>

View File

@ -19,6 +19,7 @@ if (process.env.NODE_ENV === "production") {
config.backend = "https://amber-api.leaflow.cn"; config.backend = "https://amber-api.leaflow.cn";
// console.log("api endpoint: " + config.backend); // console.log("api endpoint: " + config.backend);
export default config; export default config;

View File

@ -1,15 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { NLayout } from "naive-ui"; import { NLayout } from "naive-ui";
// import { useUserStore } from "../stores/user"; import { useUserStore } from "../stores/user";
// import Guest from "../pages/guest/index.vue"; // import Guest from "../pages/guest/index.vue";
// import router from "../router"; // import router from "../router";
import Header from "./Header.vue"; import Header from "./Header.vue";
import element from "@/config/element"; import element from "@/config/element";
import { useIsMobile } from "@/utils/composables"; import { useIsMobile } from "@/utils/composables";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
import ChatLayout from "@/components/chat/Layout.vue";
// const currentRoute = computed(() => router.currentRoute.value.name); // const currentRoute = computed(() => router.currentRoute.value.name);
// const userStore = useUserStore(); const userStore = useUserStore();
const appStore = useAppStore(); const appStore = useAppStore();
const route = useRoute(); const route = useRoute();
@ -82,7 +83,14 @@ const onScroll = (e: Event) => {
<n-layout :native-scrollbar="isMobile"> <n-layout :native-scrollbar="isMobile">
<div class="!pt-2"> <div class="!pt-2">
<div v-if="userStore.logined">
<ChatLayout>
<router-view :key="route.path"> </router-view> <router-view :key="route.path"> </router-view>
</ChatLayout>
</div>
<div v-else>
<router-view :key="route.path"> </router-view>
</div>
</div> </div>
<!-- <Guest v-if="!userStore.logined && currentRoute != '/auth/login'" /> <!-- <Guest v-if="!userStore.logined && currentRoute != '/auth/login'" />

70
src/pages/chat/Chat.vue Normal file
View File

@ -0,0 +1,70 @@
<template>
<ChatComponent />
</template>
<script setup lang="ts">
import ChatComponent from "@/components/chat/Chat.vue";
import { useDialog } from "naive-ui";
import getApi from "../../plugins/api";
import { useChatStore } from "../../stores/chat";
import router from "@/router";
import { ref } from "vue";
import { EntityAssistant, EntityChat } from "@/api";
import { useAppStore } from "@/stores/app";
const clientHeight = ref(100);
const dialog = useDialog();
const chatStore = useChatStore();
const showSettingsDialog = ref(false);
const currentChatId = ref();
const currentChat: Ref<EntityChat> = ref({});
const assistants: Ref<EntityAssistant[]> = ref([]);
const assistantSelects = ref([
{
label: "不使用",
value: null,
},
]);
async function getChats() {
chatStore.chats = (await getApi().Chat.apiV1ChatsGet()).data.data;
}
const getAssistants = async () => {
assistants.value =
(await getApi().Assistant.apiV1AssistantsGet()).data.data ?? [];
assistantSelects.value = [];
assistantSelects.value.push({
label: "不使用",
value: null,
});
assistants.value.forEach((a) => {
if (a.name !== undefined && a.id !== undefined) {
return assistantSelects.value.push({
label: a.name,
// @ts-ignore
value: a.id,
});
}
});
};
const appStore = useAppStore();
const updateClientHeight = () => {
clientHeight.value = window.innerHeight - appStore.headerHeight;
};
onMounted(() => {
updateClientHeight();
window.addEventListener("resize", updateClientHeight);
});
onUnmounted(() => {
window.removeEventListener("resize", updateClientHeight);
});
getChats();
getAssistants();
</script>

View File

@ -1,11 +1,12 @@
<template> <template>
<Chat v-if="userStore.logined" /> <Chat v-if="userStore.logined" />
<Guest v-else /> <Home v-else />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Guest from "@/pages/Guest.vue"; import Home from "@/pages/home/index.vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import Chat from "@/pages/chat/Chat.vue";
const userStore = useUserStore(); const userStore = useUserStore();
</script> </script>

View File

@ -4,6 +4,7 @@ import { defineStore } from "pinia";
export const useAppStore = defineStore("app", { export const useAppStore = defineStore("app", {
persist: false, persist: false,
state: () => ({ state: () => ({
headerHeight: 64,
updating: false, updating: false,
contentScrollHeight: 0, contentScrollHeight: 0,
contentScrollPosition: 0, contentScrollPosition: 0,

View File

@ -24,6 +24,7 @@ declare module 'vue-router/auto-routes' {
'/auth/login': RouteRecordInfo<'/auth/login', '/auth/login', Record<never, never>, Record<never, never>>, '/auth/login': RouteRecordInfo<'/auth/login', '/auth/login', Record<never, never>, Record<never, never>>,
'/auth/logout': RouteRecordInfo<'/auth/logout', '/auth/logout', Record<never, never>, Record<never, never>>, '/auth/logout': RouteRecordInfo<'/auth/logout', '/auth/logout', Record<never, never>, Record<never, never>>,
'/chat/[id]/': RouteRecordInfo<'/chat/[id]/', '/chat/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>, '/chat/[id]/': RouteRecordInfo<'/chat/[id]/', '/chat/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/chat/Chat': RouteRecordInfo<'/chat/Chat', '/chat/Chat', Record<never, never>, Record<never, never>>,
'/errors/400': RouteRecordInfo<'/errors/400', '/errors/400', Record<never, never>, Record<never, never>>, '/errors/400': RouteRecordInfo<'/errors/400', '/errors/400', Record<never, never>, Record<never, never>>,
'/errors/401': RouteRecordInfo<'/errors/401', '/errors/401', Record<never, never>, Record<never, never>>, '/errors/401': RouteRecordInfo<'/errors/401', '/errors/401', Record<never, never>, Record<never, never>>,
'/errors/404': RouteRecordInfo<'/errors/404', '/errors/404', Record<never, never>, Record<never, never>>, '/errors/404': RouteRecordInfo<'/errors/404', '/errors/404', Record<never, never>, Record<never, never>>,