feat: Add a back to bottom button to the chat page(#2957)

This commit is contained in:
wangdan-fit2cloud 2025-09-12 16:52:55 +08:00
parent 1fb280aa3d
commit 0c53e31b2a
9 changed files with 145 additions and 66 deletions

View File

@ -3,8 +3,10 @@
<div class="text-center mb-8" v-if="loading">
<el-button class="border-primary video-stop-button" @click="stopChat">
<app-icon iconName="app-video-stop" class="mr-8"></app-icon>
{{ $t('chat.operation.stopChat') }}</el-button>
{{ $t('chat.operation.stopChat') }}</el-button
>
</div>
<div class="operate-textarea">
<el-scrollbar max-height="136">
<div
@ -332,7 +334,7 @@ const props = withDefaults(
available: true,
},
)
const emit = defineEmits(['update:chatId', 'update:loading', 'update:showUserInput'])
const emit = defineEmits(['update:chatId', 'update:loading', 'update:showUserInput', 'backBottom'])
const chartOpenId = ref<string>()
const chatId_context = computed({
get: () => {

View File

@ -7,6 +7,12 @@
position: relative;
color: var(--app-text-color);
box-sizing: border-box;
.back-bottom-button {
position: absolute;
right: 14px;
top: -30px;
}
&__content {
padding-top: 0;
box-sizing: border-box;
@ -16,7 +22,6 @@
}
.content {
:deep(ol) {
margin-left: 16px !important;
}

View File

@ -66,35 +66,39 @@
</TransitionContent>
</div>
</el-scrollbar>
<ChatInputOperate
:app-id="appId"
:application-details="applicationDetails"
:is-mobile="isMobile"
:type="type"
:send-message="sendMessage"
:open-chat-id="openChatId"
:validate="validate"
:chat-management="ChatManagement"
v-model:chat-id="chartOpenId"
v-model:loading="loading"
v-model:show-user-input="showUserInput"
v-if="type !== 'log'"
>
<template #userInput>
<el-button
v-if="isUserInput || isAPIInput"
class="user-input-button mb-8"
@click="toggleUserInput"
>
<AppIcon iconName="app-edit" :size="16" class="mr-4"></AppIcon>
<span class="ellipsis">
{{ userInputTitle || $t('chat.userInput') }}
</span>
</el-button>
</template>
</ChatInputOperate>
<div style="position: relative">
<!-- 置底按钮 -->
<el-button v-if="isBottom" circle class="back-bottom-button" @click="setScrollBottom">
<el-icon><ArrowDownBold /></el-icon>
</el-button>
<ChatInputOperate
:app-id="appId"
:application-details="applicationDetails"
:is-mobile="isMobile"
:type="type"
:send-message="sendMessage"
:open-chat-id="openChatId"
:validate="validate"
:chat-management="ChatManagement"
v-model:chat-id="chartOpenId"
v-model:loading="loading"
v-model:show-user-input="showUserInput"
v-if="type !== 'log'"
>
<template #userInput>
<el-button
v-if="isUserInput || isAPIInput"
class="user-input-button mb-8"
@click="toggleUserInput"
>
<AppIcon iconName="app-edit" :size="16" class="mr-4"></AppIcon>
<span class="ellipsis">
{{ userInputTitle || $t('chat.userInput') }}
</span>
</el-button>
</template>
</ChatInputOperate>
</div>
<Control></Control>
</template>
</div>
@ -603,10 +607,12 @@ function chatMessage(chat?: any, problem?: string, re_chat?: boolean, other_para
const scrollTop = ref(0)
const scorll = ref(true)
const isBottom = ref(false)
const getMaxHeight = () => {
return dialogScrollbar.value!.scrollHeight
}
/**
* 滚动滚动条到最上面
* @param $event
@ -621,6 +627,8 @@ const handleScrollTop = ($event: any) => {
} else {
scorll.value = false
}
isBottom.value =
scrollTop.value + scrollDiv.value.wrapRef.offsetHeight < dialogScrollbar.value!.scrollHeight
emit('scroll', { ...$event, dialogScrollbar: dialogScrollbar.value, scrollDiv: scrollDiv.value })
}
/**

View File

@ -36,7 +36,7 @@
<el-pagination
v-model:current-page="paginationConfig.current_page"
v-model:page-size="paginationConfig.page_size"
:page-sizes="pageSizes"
:page-sizes="paginationConfig.page_sizes|| pageSizes"
:total="paginationConfig.total"
layout="total, prev, pager, next, sizes"
@size-change="handleSizeChange"

View File

@ -290,6 +290,11 @@
height: 20px;
font-weight: 400;
}
.el-tag--plain.el-tag--info {
color: var(--app-text-color-secondary);
font-weight: 500;
border-color: var(--app-text-color-light-1);
}
// el-select
.el-select__selected-item {

View File

@ -207,6 +207,7 @@ const paginationConfig = reactive({
current_page: 1,
page_size: 10,
total: 0,
page_sizes: [10, 20, 50, 100, 1000],
})
const filterText = ref('')

View File

@ -179,7 +179,7 @@
<template #title>
<div>
{{ item.name }}
<el-tag v-if="item.version" class="ml-4">
<el-tag v-if="item.version" class="ml-4" type="info" effect="plain">
{{ item.version }}
</el-tag>
</div>

View File

@ -1,10 +1,29 @@
<template>
<CardBox :title="props.tool.name" :description="props.tool.desc" class="cursor">
<template #icon>
<el-avatar v-if="isAppIcon(props.tool?.icon)" shape="square" :size="32" style="background: none">
<el-avatar
v-if="isAppIcon(props.tool?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="resetUrl(props.tool?.icon)" alt="" />
</el-avatar>
<el-avatar v-else-if="props.tool?.name" :name="props.tool?.name" pinyinColor shape="square" :size="32" />
<el-avatar
v-else-if="props.tool?.name"
:name="props.tool?.name"
pinyinColor
shape="square"
:size="32"
/>
</template>
<template #title>
<div>
{{ props.tool?.name }}
<el-tag v-if="props.tool?.version" class="ml-4" type="info" effect="plain">
{{ props.tool?.version }}
</el-tag>
</div>
</template>
<template #subTitle>
<el-text class="color-secondary" size="small">
@ -29,24 +48,24 @@
<script setup lang="ts">
import { ref } from 'vue'
import {isAppIcon, resetUrl} from '@/utils/common'
import { isAppIcon, resetUrl } from '@/utils/common'
const props = defineProps<{
tool: any,
tool: any
getSubTitle: (v: any) => string
addLoading: boolean
}>()
const emit = defineEmits<{
(e: 'handleAdd'): void;
(e: 'handleDetail'): void;
}>();
(e: 'handleAdd'): void
(e: 'handleDetail'): void
}>()
</script>
<style lang="scss" scoped>
.el-card {
:deep(.card-footer) {
&>div:first-of-type {
& > div:first-of-type {
flex: 1;
}

View File

@ -1,19 +1,34 @@
<template>
<el-dialog v-model="dialogVisible" width="1000" append-to-body class="tool-store-dialog" align-center
:close-on-click-modal="false" :close-on-press-escape="false">
<el-dialog
v-model="dialogVisible"
width="1000"
append-to-body
class="tool-store-dialog"
align-center
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<template #header="{ titleId }">
<div class="dialog-header flex-between mb-8">
<h4 :id="titleId" class="medium">
{{ $t('views.tool.toolStore.title') }}
</h4>
<el-radio-group v-model="toolType" @change="radioChange" class="app-radio-button-group">
<el-radio-button value="INTERNAL">{{ $t('views.tool.toolStore.internal') }}</el-radio-button>
<el-radio-button value="INTERNAL">{{
$t('views.tool.toolStore.internal')
}}</el-radio-button>
<el-radio-button value="APPSTORE">{{ $t('views.tool.toolStore.title') }}</el-radio-button>
</el-radio-group>
<div class="flex align-center" style="margin-right: 28px;">
<el-input v-model="searchValue" :placeholder="$t('common.search')" prefix-icon="Search" class="w-240 mr-8"
clearable @change="getList" />
<div class="flex align-center" style="margin-right: 28px">
<el-input
v-model="searchValue"
:placeholder="$t('common.search')"
prefix-icon="Search"
class="w-240 mr-8"
clearable
@change="getList"
/>
<el-divider direction="vertical" />
</div>
</div>
@ -21,23 +36,40 @@
<LayoutContainer v-loading="loading">
<template #left>
<el-anchor direction="vertical" :offset="130" type="default" container=".category-scrollbar"
@click="handleClick">
<el-anchor-link v-for="category in categories" :key="category.id" :href="`#category-${category.id}`"
:title="category.title" />
<el-anchor
direction="vertical"
:offset="130"
type="default"
container=".category-scrollbar"
@click="handleClick"
>
<el-anchor-link
v-for="category in categories"
:key="category.id"
:href="`#category-${category.id}`"
:title="category.title"
/>
</el-anchor>
</template>
<el-scrollbar class="layout-bg" wrap-class="p-16-24 category-scrollbar">
<template v-if="filterList === null">
<div v-for="category in categories" :key="category.id">
<h4 class="title-decoration-1 mb-16 mt-8 color-text-primary" :id="`category-${category.id}`">
<h4
class="title-decoration-1 mb-16 mt-8 color-text-primary"
:id="`category-${category.id}`"
>
{{ category.title }}
</h4>
<el-row :gutter="16">
<el-col v-for="tool in category.tools" :key="tool.id" :span="12" class="mb-16">
<ToolCard :tool="tool" :addLoading="addLoading" :get-sub-title="getSubTitle"
@handleAdd="handleOpenAdd(tool)" @handleDetail="handleDetail(tool)" />
<ToolCard
:tool="tool"
:addLoading="addLoading"
:get-sub-title="getSubTitle"
@handleAdd="handleOpenAdd(tool)"
@handleDetail="handleDetail(tool)"
/>
</el-col>
</el-row>
</div>
@ -49,8 +81,13 @@
</h4>
<el-row :gutter="16" v-if="filterList.length">
<el-col v-for="tool in filterList" :key="tool.id" :span="12" class="mb-16">
<ToolCard :tool="tool" :addLoading="addLoading" :get-sub-title="getSubTitle"
@handleAdd="handleOpenAdd(tool)" @handleDetail="handleDetail(tool)" />
<ToolCard
:tool="tool"
:addLoading="addLoading"
:get-sub-title="getSubTitle"
@handleAdd="handleOpenAdd(tool)"
@handleDetail="handleDetail(tool)"
/>
</el-col>
</el-row>
<el-empty v-else :description="$t('common.noData')" />
@ -70,7 +107,7 @@ import ToolCard from './ToolCard.vue'
import { MsgSuccess } from '@/utils/message'
import InternalDescDrawer from './InternalDescDrawer.vue'
import AddInternalToolDialog from './AddInternalToolDialog.vue'
import { loadSharedApi } from "@/utils/dynamics-api/shared-api.ts";
import { loadSharedApi } from '@/utils/dynamics-api/shared-api.ts'
import useStore from '@/stores'
const { user } = useStore()
interface ToolCategory {
@ -81,7 +118,7 @@ interface ToolCategory {
const props = defineProps({
apiType: {
type: String as () => 'workspace' | 'systemShare' | 'systemManage',
default: 'workspace'
default: 'workspace',
},
})
const emit = defineEmits(['refresh'])
@ -102,12 +139,12 @@ const categories = ref<ToolCategory[]>([
{
id: 'web_search',
title: t('views.tool.toolStore.webSearch'),
tools: []
tools: [],
},
{
id: 'database_search',
title: t('views.tool.toolStore.databaseQuery'),
tools: []
tools: [],
},
// {
// id: 'image',
@ -128,7 +165,7 @@ const categories = ref<ToolCategory[]>([
const filterList = ref<any>(null)
function getSubTitle(tool: any) {
return categories.value.find(i => i.id === tool.label)?.title ?? ''
return categories.value.find((i) => i.id === tool.label)?.title ?? ''
}
function open(id: string) {
@ -156,7 +193,7 @@ async function getInternalToolList() {
filterList.value = res.data
} else {
filterList.value = null
categories.value.forEach(category => {
categories.value.forEach((category) => {
// if (category.id === 'recommend') {
// category.tools = res.data
// } else {
@ -183,7 +220,7 @@ async function getStoreToolList() {
categories.value = tags.map((tag: any) => ({
id: tag.key,
title: tag.name, //
tools: storeTools.filter((tool: any) => tool.label === tag.key)
tools: storeTools.filter((tool: any) => tool.label === tag.key),
}))
} catch (error) {
console.error(error)
@ -244,7 +281,7 @@ async function handleStoreAdd(tool: any) {
download_callback_url: tool.downloadCallbackUrl,
icon: tool.icon,
versions: tool.versions,
label: tool.label
label: tool.label,
}
await loadSharedApi({ type: 'tool', systemType: props.apiType })
.addStoreTool(tool.id, obj, addLoading)
@ -268,6 +305,9 @@ defineExpose({ open })
<style lang="scss">
.tool-store-dialog {
padding: 0;
.el-dialog__headerbtn {
top: 7px;
}
.el-dialog__header {
padding: 12px 20px 4px 24px;
@ -314,7 +354,6 @@ defineExpose({ open })
}
}
}
}
.category-scrollbar {