Compare commits
10 Commits
bdb685b4ec
...
feature/zo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
011c28d945 | ||
|
|
ff5355855f | ||
|
|
aff9233ce2 | ||
| d566344eb8 | |||
| 236abba8d0 | |||
| 47a361e78b | |||
| 8714478587 | |||
| 14d213c4a8 | |||
| 34ad0ed8e3 | |||
| aa8c1737ad |
64
src/renderer/api/ConversationApi.ts
Normal file
64
src/renderer/api/ConversationApi.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
import { getRequest, postRequest, ResponseModel } from '@utils/request'
|
||||||
|
|
||||||
|
/** 创建会话 创建会话创建会话 GET /agent/assistant/createConversation */
|
||||||
|
export interface CreateConversationResponse {
|
||||||
|
conversationId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createConversation = async () => {
|
||||||
|
const res: ResponseModel = await getRequest('/agent/assistant/createConversation')
|
||||||
|
return res.data as CreateConversationResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取会话列表 获取会话列表获取会话列表 POST /agent/assistant/conversationList */
|
||||||
|
export interface ConversationListRequest {
|
||||||
|
pageSize: number
|
||||||
|
pageNum: number
|
||||||
|
conversationId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversationListResponse {
|
||||||
|
records: Array<ConversationListRecords>
|
||||||
|
total: number
|
||||||
|
size: number
|
||||||
|
current: number
|
||||||
|
optimizeCountSql: boolean
|
||||||
|
searchCount: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversationListRecords {
|
||||||
|
conversationTitle: string
|
||||||
|
conversationId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversationMessageListResponse {
|
||||||
|
records: Array<ConversationMessageListRecords>
|
||||||
|
total: number
|
||||||
|
size: number
|
||||||
|
current: number
|
||||||
|
optimizeCountSql: boolean
|
||||||
|
searchCount: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversationMessageListRecords {
|
||||||
|
messageId: string
|
||||||
|
conversationId: string
|
||||||
|
messageType: string
|
||||||
|
messageContent: string
|
||||||
|
messageDisplay: string
|
||||||
|
messageSenderId: string
|
||||||
|
messageSenderRole: string
|
||||||
|
messageTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getConversationList = async (params: ConversationListRequest) => {
|
||||||
|
const res: ResponseModel = await postRequest('/agent/assistant/conversationList', params)
|
||||||
|
return res.data as ConversationListResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export const conversationMessageList = async (params: ConversationListRequest) => {
|
||||||
|
const res: ResponseModel = await postRequest('/agent/assistant/conversationMessageList', params)
|
||||||
|
return res.data as ConversationMessageListResponse
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="title-bar flex items-start justify-between h-[40px]">
|
<header class="flex items-start justify-between h-[40px]">
|
||||||
<div class="title-bar-main flex-auto">
|
<div class="title-bar-main flex-auto">
|
||||||
<slot>{{ title ?? '' }}</slot>
|
<slot>{{ title ?? '' }}</slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,8 +69,4 @@ function handleClose() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.title-bar {
|
|
||||||
background-color: rgba(239, 246, 255, 0.8);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg h-screen flex flex-col">
|
<div class="bg-color h-screen flex flex-col">
|
||||||
<header-bar>
|
<header-bar>
|
||||||
<drag-region class="w-full" />
|
<drag-region class="w-full" />
|
||||||
</header-bar>
|
</header-bar>
|
||||||
|
|
||||||
<main class="bg-[#f7f9fc] box-border w-full h-[calc(100vh-40px)] flex pt-[8px] pb-[8px] pl-[8px] ">
|
<main class="box-border w-full h-[calc(100vh-40px)] flex pt-[8px] pb-[8px] pl-[8px] ">
|
||||||
<div class="flex-1 flex">
|
<div class="flex-1 flex">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@@ -17,3 +17,8 @@
|
|||||||
<script setup lang="ts" name="Layout">
|
<script setup lang="ts" name="Layout">
|
||||||
import SideMenus from '@renderer/components/SideMenus/index.vue'
|
import SideMenus from '@renderer/components/SideMenus/index.vue'
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.bg-color {
|
||||||
|
background: linear-gradient(180deg, #EFF6FF 0%, #F5F7FA 40%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import cache from '@utils/cache'
|
import cache from '@utils/cache'
|
||||||
import errorCode from '@constant/errorCode'
|
import errorCode from '@constant/errorCode'
|
||||||
import { ElNotification , ElMessageBox, ElMessage } from 'element-plus'
|
import { ElNotification, ElMessageBox, ElMessage } from 'element-plus'
|
||||||
import { Session } from '@renderer/utils/storage'
|
import { Session } from '@renderer/utils/storage'
|
||||||
import { tansParams } from '@utils/tansParams'
|
import { tansParams } from '@utils/tansParams'
|
||||||
|
|
||||||
@@ -87,12 +87,13 @@ instance.interceptors.request.use(
|
|||||||
// 添加响应拦截器
|
// 添加响应拦截器
|
||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(res) => {
|
(res) => {
|
||||||
|
console.log(`🚀 ~ response: \n url:${res.config.url} \n params:${JSON.stringify(res.config.params)} \n data:\n ${JSON.stringify(res.data)}`)
|
||||||
// 未设置状态码则默认成功状态
|
// 未设置状态码则默认成功状态
|
||||||
const code = res.data.code || 200
|
const code = res.data.code || 200
|
||||||
// 获取错误信息
|
// 获取错误信息
|
||||||
const msg = (errorCode as any)[code] || res.data.msg || errorCode['default']
|
const msg = (errorCode as any)[code] || res.data.msg || errorCode['default']
|
||||||
// 二进制数据则直接返回
|
// 二进制数据则直接返回
|
||||||
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
|
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
if (code === 401) {
|
if (code === 401) {
|
||||||
@@ -103,10 +104,10 @@ instance.interceptors.response.use(
|
|||||||
// useUserStore().logOut().then(() => {
|
// useUserStore().logOut().then(() => {
|
||||||
// location.href = '/index'
|
// location.href = '/index'
|
||||||
// })
|
// })
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
isRelogin.show = false
|
isRelogin.show = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
|
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
|
||||||
} else if (code === 500) {
|
} else if (code === 500) {
|
||||||
ElMessage({ message: msg, type: 'error' })
|
ElMessage({ message: msg, type: 'error' })
|
||||||
@@ -118,7 +119,7 @@ instance.interceptors.response.use(
|
|||||||
ElNotification.error({ title: msg })
|
ElNotification.error({ title: msg })
|
||||||
return Promise.reject('error')
|
return Promise.reject('error')
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(res.data)
|
return Promise.resolve(res.data)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -138,4 +139,34 @@ instance.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 封装基于 request 的 POST 请求(
|
||||||
|
export const postRequest = <ResponseModel>(url: string, data?: any, options?: any): Promise<ResponseModel> => {
|
||||||
|
return instance.request({
|
||||||
|
url,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
...(options || {}),
|
||||||
|
}) as Promise<ResponseModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 封装基于 request 的 GET 请求
|
||||||
|
export const getRequest = <ResponseModel>(url: string, params?: any, options?: any): Promise<ResponseModel> => {
|
||||||
|
return instance.request({
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
...(options || {}),
|
||||||
|
}) as Promise<ResponseModel>
|
||||||
|
}
|
||||||
|
|
||||||
export default instance
|
export default instance
|
||||||
|
|
||||||
|
/// 响应模型
|
||||||
|
export interface ResponseModel {
|
||||||
|
code: number;
|
||||||
|
msg: string | null;
|
||||||
|
data: any | null;
|
||||||
|
};
|
||||||
@@ -1,72 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 页面根 -->
|
<!-- 页面根 -->
|
||||||
<div class="h-full overflow-hidden flex flex-col">
|
<div class="flex flex-col h-full py-6 px-6" :class="isGuidePage ? 'overflow-auto' : 'overflow-hidden'">
|
||||||
|
|
||||||
<!-- 消息列表(唯一滚动区) -->
|
<!-- 引导页顶部 welcome(仅在引导页显示) -->
|
||||||
<div ref="listRef" class="flex-1 overflow-y-auto px-6 py-6 space-y-6">
|
<div v-if="isGuidePage" class="border-box pt-30">
|
||||||
|
<h1 class="text-[28px] font-bold mb-7 leading-tight">
|
||||||
|
你好,<br />
|
||||||
|
我今天能帮你什么?
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主体滚动区:聊天或引导页内容 -->
|
||||||
|
<div v-if="!isGuidePage" ref="listRef" class="flex-1 overflow-y-auto py-6 space-y-6">
|
||||||
|
<!-- 聊天消息列表 -->
|
||||||
<div v-for="msg in chatMsgList" :key="msg.messageId" class="flex items-start gap-3"
|
<div v-for="msg in chatMsgList" :key="msg.messageId" class="flex items-start gap-3"
|
||||||
:class="msg.messageRole === MessageRole.ME ? 'justify-end' : 'justify-start'">
|
:class="msg.messageRole === MessageRole.ME ? 'justify-end' : 'justify-start'">
|
||||||
|
|
||||||
<!-- AI avatar -->
|
<!-- AI avatar -->
|
||||||
<ChatAvatar v-if="msg.messageRole === MessageRole.AI" :src="aiAvatar" />
|
<ChatAvatar v-if="msg.messageRole === MessageRole.AI" :src="aiAvatar" />
|
||||||
|
|
||||||
<!-- 消息气泡 -->
|
<!-- 自己 发的消息 -->
|
||||||
<div class="max-w-[70%]">
|
<ChatRoleMe v-if="msg.messageRole === MessageRole.ME" :msg="msg">
|
||||||
<!-- 名字和时间 -->
|
<template #header>
|
||||||
<ChatNameTime :showReverse="msg.messageRole === MessageRole.ME" />
|
<!-- 名字和时间 -->
|
||||||
|
<ChatNameTime :showReverse="true" />
|
||||||
|
</template>
|
||||||
|
</ChatRoleMe>
|
||||||
|
|
||||||
<!-- AI 发的消息 -->
|
<!-- AI 发的消息 -->
|
||||||
<ChatRoleAI v-if="msg.messageRole === MessageRole.AI" :msg="msg" />
|
<ChatRoleAI v-if="msg.messageRole === MessageRole.AI" :msg="msg">
|
||||||
|
<template #header>
|
||||||
|
<!-- 名字和时间 -->
|
||||||
|
<ChatNameTime :showReverse="false" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 自己 发的消息 -->
|
<template #footer>
|
||||||
<ChatRoleMe v-if="msg.messageRole === MessageRole.ME" :msg="msg" />
|
<!-- 问题标签 -->
|
||||||
|
<ChatAttach v-if="msg.question && msg.question.length > 0" :question="msg.question" @select="onTagSelect" />
|
||||||
|
|
||||||
<!-- AI 标识 -->
|
<!-- AI 标识 -->
|
||||||
<ChatAIMark v-if="msg.messageRole === MessageRole.AI && msg.finished" />
|
<ChatAIMark v-if="msg.finished" />
|
||||||
|
|
||||||
<!-- AI 操作按钮 -->
|
<!-- AI 操作按钮 -->
|
||||||
<ChatOperation v-if="msg.messageRole === MessageRole.AI && msg.finished" :msg="msg" />
|
<ChatOperation v-if="msg.finished" :msg="msg" />
|
||||||
</div>
|
</template>
|
||||||
|
</ChatRoleAI>
|
||||||
|
|
||||||
<!-- User avatar -->
|
<!-- User avatar -->
|
||||||
<ChatAvatar v-if="msg.messageRole === MessageRole.ME" :src="userAvatar" />
|
<ChatAvatar v-if="msg.messageRole === MessageRole.ME" :src="userAvatar" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入区(固定底部,不滚) -->
|
<!-- 输入区 -->
|
||||||
<div class="shrink-0 px-6 py-4 gap-3">
|
<div class="flex flex-col gap-3" :class="isGuidePage ? 'mt-16' : 'mt-4'">
|
||||||
<div class="inline-flex items-center justify-center w-[108px]
|
<div class="inline-flex items-center justify-center w-[108px]
|
||||||
px-3 py-1.5 rounded-2xl border border-[#E5E8EE]
|
px-3 py-1.5 rounded-2xl border border-[#E5E8EE]
|
||||||
text-[13px] text-[#333]">
|
text-[13px] text-[#333]">
|
||||||
智能问数
|
智能问数
|
||||||
</div>
|
</div>
|
||||||
|
<ChatInputArea v-model="inputMessage" :isSendingMessage="isSendingMessage" @send="onGuideSend"
|
||||||
<div class="h-[174px] bg-white rounded-lg border border-[#eef2f6]
|
@attach="addAttachmentAction" />
|
||||||
shadow-[0_1px_0_rgba(0,0,0,0.03)]
|
|
||||||
p-[18px] mt-[8px] flex flex-col justify-between">
|
|
||||||
<textarea rows="2" placeholder="给我发布或者布置任务" class="flex-1 resize-none outline-none text-sm"
|
|
||||||
v-model="inputMessage" @keyup.enter="sendMessageAction" />
|
|
||||||
|
|
||||||
<div class="flex justify-between items-end">
|
|
||||||
<button @click="addAttachmentAction()">
|
|
||||||
<RiLink />
|
|
||||||
</button>
|
|
||||||
<button class="w-[48px] h-[48px] bg-[#F5F7FA] px-2.5 py-1.5 rounded-md flex items-center justify-center"
|
|
||||||
@click="sendMessageAction()">
|
|
||||||
<RiStopFill v-if="isSendingMessage" />
|
|
||||||
<RiSendPlaneFill v-else />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务中心(仅在引导页显示) -->
|
||||||
|
<TaskCenter v-if="isGuidePage" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, defineProps, defineEmits, watch, nextTick } from 'vue'
|
||||||
import { RiLink, RiSendPlaneFill, RiStopFill } from '@remixicon/vue'
|
import { onMounted, onUnmounted } from "vue";
|
||||||
import { onMounted, nextTick, onUnmounted } from "vue";
|
|
||||||
import { WebSocketManager } from "@common/WebSocketManager";
|
import { WebSocketManager } from "@common/WebSocketManager";
|
||||||
import { MessageRole, ChatMessage } from "./model/ChatModel";
|
import { MessageRole, ChatMessage } from "./model/ChatModel";
|
||||||
import { IdUtils } from "@common/index";
|
import { IdUtils } from "@common/index";
|
||||||
@@ -76,15 +80,43 @@ import ChatRoleAI from './components/ChatRoleAI.vue';
|
|||||||
import ChatRoleMe from './components/ChatRoleMe.vue';
|
import ChatRoleMe from './components/ChatRoleMe.vue';
|
||||||
import ChatAIMark from './components/ChatAIMark.vue';
|
import ChatAIMark from './components/ChatAIMark.vue';
|
||||||
import ChatNameTime from './components/ChatNameTime.vue';
|
import ChatNameTime from './components/ChatNameTime.vue';
|
||||||
|
import ChatAttach from './components/ChatAttach.vue';
|
||||||
|
import ChatInputArea from './components/ChatInputArea.vue';
|
||||||
|
import TaskCenter from './TaskCenter.vue';
|
||||||
|
|
||||||
import { Session } from '../../utils/storage';
|
import { Session } from '../../utils/storage';
|
||||||
|
|
||||||
import userAvatar from '@assets/images/login/user_icon.png';
|
import userAvatar from '@assets/images/login/user_icon.png';
|
||||||
import aiAvatar from '@assets/images/login/blue_logo.png';
|
import aiAvatar from '@assets/images/login/blue_logo.png';
|
||||||
|
import { createConversation, conversationMessageList } from '../../api/ConversationApi';
|
||||||
|
import { ElMessage, ElLoading } from 'element-plus'
|
||||||
|
|
||||||
|
// 支持外部通过 prop 控制是否为引导页
|
||||||
|
const props = defineProps({
|
||||||
|
guide: { type: Boolean, default: true },
|
||||||
|
conversationId: { type: String, default: '' }
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['update:guide']);
|
||||||
|
|
||||||
///(控制滚动位置)
|
/// 是否是引导页(内部响应式)
|
||||||
const scrollTop = ref(99999);
|
const isGuidePage = ref(props.guide);
|
||||||
|
|
||||||
|
// 同步外部变化到内部
|
||||||
|
watch(() => props.guide, (v) => {
|
||||||
|
isGuidePage.value = v;
|
||||||
|
});
|
||||||
|
// 将内部变化通知父组件
|
||||||
|
watch(isGuidePage, (v) => {
|
||||||
|
emit('update:guide', v);
|
||||||
|
if (v) {
|
||||||
|
// 当切换到引导页时,重置/清理会话状态
|
||||||
|
resetConversation();
|
||||||
|
createConversationRequest();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 列表滚动容器引用
|
||||||
|
const listRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
/// 会话列表
|
/// 会话列表
|
||||||
const chatMsgList = ref<ChatMessage[]>([]);
|
const chatMsgList = ref<ChatMessage[]>([]);
|
||||||
@@ -93,10 +125,19 @@ const inputMessage = ref("");
|
|||||||
/// 发送消息中标志
|
/// 发送消息中标志
|
||||||
const isSendingMessage = ref(false);
|
const isSendingMessage = ref(false);
|
||||||
|
|
||||||
/// agentId 首页接口中获取
|
/// agentId 首页接口中获取 1953462165250859011
|
||||||
const agentId = ref("1953462165250859010");
|
const agentId = ref("1953462165250859011");
|
||||||
/// 会话ID 历史数据接口中获取
|
/// 会话ID 历史数据接口中获取
|
||||||
const conversationId = ref("");
|
const conversationId = ref(props.conversationId);
|
||||||
|
|
||||||
|
// 监听 conversationId prop 变化,只有当有值时(选择历史消息)才请求消息列表
|
||||||
|
watch(() => props.conversationId, (newId) => {
|
||||||
|
if (newId) {
|
||||||
|
conversationId.value = newId;
|
||||||
|
loadConversationMessages(newId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 会话进行中标志
|
// 会话进行中标志
|
||||||
const isSessionActive = ref(false);
|
const isSessionActive = ref(false);
|
||||||
/// 指令通用消息类型
|
/// 指令通用消息类型
|
||||||
@@ -123,15 +164,39 @@ const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
|||||||
// 当前会话的消息ID,用于保持发送和终止的messageId一致
|
// 当前会话的消息ID,用于保持发送和终止的messageId一致
|
||||||
let currentSessionMessageId: string | null = null;
|
let currentSessionMessageId: string | null = null;
|
||||||
|
|
||||||
// 滚动到底部 - 优化版本,确保打字机效果始终可见
|
// 滚动到底部 - 精确计算 last 元素位置并使用双 rAF 保证布局稳定
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = (smooth = false) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 使用更大的值确保滚动到真正的底部
|
const el = listRef.value;
|
||||||
scrollTop.value = 99999;
|
if (!el) return;
|
||||||
// 强制触发滚动更新,增加延迟确保DOM更新完成
|
|
||||||
setTimeout(() => {
|
const doScroll = () => {
|
||||||
scrollTop.value = scrollTop.value + Math.random();
|
const last = el.lastElementChild as HTMLElement | null;
|
||||||
}, 10);
|
// 计算容器 style padding-bottom,保证滚动到真正可视底部
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
const paddingBottom = parseFloat(style.paddingBottom || '0') || 0;
|
||||||
|
|
||||||
|
if (last) {
|
||||||
|
const lastOffset = last.offsetTop + last.offsetHeight;
|
||||||
|
const target = lastOffset + paddingBottom - el.clientHeight;
|
||||||
|
const top = Math.max(0, Math.ceil(target));
|
||||||
|
if (smooth && typeof el.scrollTo === 'function') {
|
||||||
|
el.scrollTo({ top, behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
el.scrollTop = top;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兜底:滚到底
|
||||||
|
if (smooth && typeof el.scrollTo === 'function') {
|
||||||
|
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 使用两次 requestAnimationFrame 增强在复杂渲染/打字机更新场景的可靠性
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(doScroll));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,7 +214,6 @@ const handleReplyText = (text: string) => {
|
|||||||
// 是发送指令消息
|
// 是发送指令消息
|
||||||
const handleReplyInstruct = async (message: string, type: string) => {
|
const handleReplyInstruct = async (message: string, type: string) => {
|
||||||
// await checkToken();
|
// await checkToken();
|
||||||
|
|
||||||
commonTypeMessage = type;
|
commonTypeMessage = type;
|
||||||
// 重置消息状态,准备接收新的AI回复
|
// 重置消息状态,准备接收新的AI回复
|
||||||
resetMessageState();
|
resetMessageState();
|
||||||
@@ -157,6 +221,18 @@ const handleReplyInstruct = async (message: string, type: string) => {
|
|||||||
setTimeoutScrollToBottom();
|
setTimeoutScrollToBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 选择标签事件:切换到聊天页并发送
|
||||||
|
const onTagSelect = (text: string) => {
|
||||||
|
isGuidePage.value = false;
|
||||||
|
nextTick(() => handleReplyText(text));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在引导页中按发送:切换到聊天页再发送
|
||||||
|
const onGuideSend = () => {
|
||||||
|
isGuidePage.value = false;
|
||||||
|
nextTick(() => sendMessageAction());
|
||||||
|
};
|
||||||
|
|
||||||
/// 添加附件按钮事件
|
/// 添加附件按钮事件
|
||||||
const addAttachmentAction = () => {
|
const addAttachmentAction = () => {
|
||||||
console.log("添加附件");
|
console.log("添加附件");
|
||||||
@@ -169,11 +245,14 @@ const sendMessageAction = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("输入消息:", inputMessage.value);
|
const raw = inputMessage.value || '';
|
||||||
if (!inputMessage.value.trim()) return;
|
// 去除尾部多余的换行,保留中间换行(例如 Shift+Enter)
|
||||||
|
const sendText = raw.replace(/\n+$/, '');
|
||||||
|
console.log("输入消息:", sendText);
|
||||||
|
if (!sendText.trim()) return;
|
||||||
// 重置消息状态,准备接收新的AI回复
|
// 重置消息状态,准备接收新的AI回复
|
||||||
resetMessageState();
|
resetMessageState();
|
||||||
sendMessage(inputMessage.value);
|
sendMessage(sendText);
|
||||||
setTimeoutScrollToBottom();
|
setTimeoutScrollToBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -195,11 +274,12 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// token存在,初始化数据
|
// token存在,初始化数据
|
||||||
const initHandler = () => {
|
const initHandler = async () => {
|
||||||
console.log("initHandler");
|
console.log("initHandler");
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
initWebSocket();
|
await createConversationRequest();
|
||||||
|
await initWebSocket();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAccessToken = () => {
|
const getAccessToken = () => {
|
||||||
@@ -214,6 +294,38 @@ const checkToken = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 调用接口创建新会话
|
||||||
|
const createConversationRequest = async (): Promise<string | null> => {
|
||||||
|
const res = await createConversation();
|
||||||
|
if (res && res.conversationId) {
|
||||||
|
conversationId.value = res.conversationId;
|
||||||
|
console.log("创建新会话,ID:", conversationId.value);
|
||||||
|
return res.conversationId;
|
||||||
|
} else {
|
||||||
|
console.log("创建会话失败,接口返回异常");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载历史会话消息
|
||||||
|
const loadConversationMessages = async (convId: string) => {
|
||||||
|
try {
|
||||||
|
const res = await conversationMessageList({ conversationId: convId, pageSize: 50, pageNum: 1 });
|
||||||
|
// 将消息转换为 ChatMessage 格式
|
||||||
|
chatMsgList.value = res.records.map((msg: any) => ({
|
||||||
|
messageId: msg.messageId,
|
||||||
|
messageRole: msg.messageSenderRole === 'user' ? MessageRole.ME : MessageRole.AI,
|
||||||
|
messageContent: msg.messageContent,
|
||||||
|
finished: true, // 历史消息已完成
|
||||||
|
}));
|
||||||
|
console.log("加载历史消息:", chatMsgList.value);
|
||||||
|
// 加载历史消息后滚动到底部
|
||||||
|
nextTick(() => scrollToBottom());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载历史消息失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// =============对话↓================
|
/// =============对话↓================
|
||||||
// 初始化WebSocket
|
// 初始化WebSocket
|
||||||
const initWebSocket = async () => {
|
const initWebSocket = async () => {
|
||||||
@@ -369,6 +481,7 @@ const handleWebSocketMessage = (data: any) => {
|
|||||||
|
|
||||||
// 处理question
|
// 处理question
|
||||||
if (data.question && data.question.length > 0) {
|
if (data.question && data.question.length > 0) {
|
||||||
|
console.log("收到问题标签:", data.question);
|
||||||
chatMsgList.value[aiMsgIndex].question = data.question;
|
chatMsgList.value[aiMsgIndex].question = data.question;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +501,7 @@ const handleWebSocketMessage = (data: any) => {
|
|||||||
isSessionActive.value = false;
|
isSessionActive.value = false;
|
||||||
// 清理当前会话的 messageId,避免保留陈旧 id
|
// 清理当前会话的 messageId,避免保留陈旧 id
|
||||||
resetMessageState();
|
resetMessageState();
|
||||||
|
setTimeoutScrollToBottom();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -407,10 +521,7 @@ const sendMessage = async (message: string, isInstruct: boolean = false) => {
|
|||||||
if (!isWsConnected()) {
|
if (!isWsConnected()) {
|
||||||
console.log("WebSocket未连接,尝试重新连接...");
|
console.log("WebSocket未连接,尝试重新连接...");
|
||||||
// 显示加载提示
|
// 显示加载提示
|
||||||
// uni.showLoading({
|
const loadingInstance = ElLoading.service({ fullscreen: true, text: '正在连接服务器...' });
|
||||||
// title: "正在连接服务器...",
|
|
||||||
// });
|
|
||||||
|
|
||||||
// 尝试重新初始化WebSocket连接
|
// 尝试重新初始化WebSocket连接
|
||||||
try {
|
try {
|
||||||
await initWebSocket();
|
await initWebSocket();
|
||||||
@@ -419,31 +530,22 @@ const sendMessage = async (message: string, isInstruct: boolean = false) => {
|
|||||||
|
|
||||||
// 检查连接是否成功建立
|
// 检查连接是否成功建立
|
||||||
if (!isWsConnected()) {
|
if (!isWsConnected()) {
|
||||||
// uni.hideLoading();
|
loadingInstance.close();
|
||||||
// uni.showToast({
|
ElMessage({ message: '连接服务器失败,请稍后重试', type: 'error' })
|
||||||
// title: "连接服务器失败,请稍后重试",
|
|
||||||
// icon: "none",
|
|
||||||
// });
|
|
||||||
console.error("重新连接WebSocket后仍未连接成功");
|
console.error("重新连接WebSocket后仍未连接成功");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// uni.hideLoading();
|
loadingInstance.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
loadingInstance.close();
|
||||||
console.error("重新连接WebSocket失败:", error);
|
console.error("重新连接WebSocket失败:", error);
|
||||||
// uni.hideLoading();
|
ElMessage({ message: '连接服务器失败,请稍后重试', type: 'error' })
|
||||||
// uni.showToast({
|
|
||||||
// title: "连接服务器失败,请稍后重试",
|
|
||||||
// icon: "none",
|
|
||||||
// });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSessionActive.value) {
|
if (isSessionActive.value) {
|
||||||
// uni.showToast({
|
ElMessage({ message: '当前会话正在进行中,请等待回复完成', type: 'warning' })
|
||||||
// title: "请等待当前回复完成",
|
|
||||||
// icon: "none",
|
|
||||||
// });
|
|
||||||
console.warn("当前会话正在进行中,请等待回复完成");
|
console.warn("当前会话正在进行中,请等待回复完成");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -705,5 +807,32 @@ const resetConfig = () => {
|
|||||||
pendingMap.clear();
|
pendingMap.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 清空会话并停止相关活动(保留 websocket 连接以便继续使用)
|
||||||
|
const resetConversation = () => {
|
||||||
|
try {
|
||||||
|
// 如果正在发送,尝试发送停止请求
|
||||||
|
try {
|
||||||
|
if (isSendingMessage.value) sendStopAction();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 pendingTimeouts
|
||||||
|
for (const t of pendingTimeouts.values()) {
|
||||||
|
clearTimeout(t);
|
||||||
|
}
|
||||||
|
pendingTimeouts.clear();
|
||||||
|
pendingMap.clear();
|
||||||
|
|
||||||
|
// 清理消息与状态
|
||||||
|
chatMsgList.value = [];
|
||||||
|
inputMessage.value = '';
|
||||||
|
isSendingMessage.value = false;
|
||||||
|
isSessionActive.value = false;
|
||||||
|
currentSessionMessageId = null;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('resetConversation failed', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- 唯一滚动容器 -->
|
|
||||||
<div class="h-full overflow-y-auto">
|
|
||||||
|
|
||||||
<!-- Hero:吸顶 -->
|
|
||||||
<div class="bg-white border-box px-12 pt-10 pb-6 max-[800px]:px-5">
|
|
||||||
<h1 class="text-[28px] font-bold mb-7 leading-tight">
|
|
||||||
你好,<br />
|
|
||||||
我今天能帮你什么?
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- input -->
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<div class="inline-flex items-center justify-center w-[108px]
|
|
||||||
px-3 py-1.5 rounded-2xl border border-[#E5E8EE]
|
|
||||||
text-[13px] text-[#333]">
|
|
||||||
智能问数
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="h-[174px] bg-white rounded-lg border border-[#eef2f6]
|
|
||||||
shadow-[0_1px_0_rgba(0,0,0,0.03)]
|
|
||||||
p-[18px] flex flex-col justify-between">
|
|
||||||
<div class="text-[#bfc9d4]">
|
|
||||||
给我发布或者布置任务
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<button class="text-[#9fb0c4]">🔗</button>
|
|
||||||
<button class="bg-[#f1f6fb] px-2.5 py-1.5 rounded-md">
|
|
||||||
✈
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- header -->
|
|
||||||
<div class="flex justify-between items-center mt-4">
|
|
||||||
<h3 class="text-base font-semibold">任务中心</h3>
|
|
||||||
<a class="text-[#3b82f6] text-[13px] cursor-pointer">
|
|
||||||
编辑
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 内容区 -->
|
|
||||||
<div class="flex-1 px-12 pb-10 pt-4 max-[800px]:px-5">
|
|
||||||
<div class="grid grid-cols-2 gap-4 max-[800px]:grid-cols-1">
|
|
||||||
<div v-for="n in 14" :key="n" class="flex gap-3 items-start p-3.5
|
|
||||||
rounded-[10px] border border-[#dfeaf6] bg-white">
|
|
||||||
<div class="w-11 h-11 bg-[#EFF6FF] rounded-lg
|
|
||||||
border border-dashed border-[#9fc0e8]
|
|
||||||
flex items-center justify-center
|
|
||||||
text-[#3b82f6] text-[23px]">
|
|
||||||
销
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="font-semibold">
|
|
||||||
每日销售数据
|
|
||||||
</div>
|
|
||||||
<div class="text-[#9aa5b1] text-[13px] mt-1.5">
|
|
||||||
分析用于销售渠道每日数据汇总及简要展示
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
</script>
|
|
||||||
@@ -5,127 +5,79 @@
|
|||||||
<div class="font-bold text-gray-80">YINIAN</div>
|
<div class="font-bold text-gray-80">YINIAN</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center m-2 bg-white rounded-lg p-2.5 border-[##E5E8EE] shadow-sm text-center" @click="addNewChat">
|
<div class="flex justify-center m-2 bg-white rounded-lg p-2.5 border-[#E5E8EE] shadow-sm text-center"
|
||||||
|
@click="addNewChat">
|
||||||
<RiAddLine /> 新对话
|
<RiAddLine /> 新对话
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-y-auto p-2 ">
|
<div class="overflow-y-auto p-2 ">
|
||||||
<section v-for="(group, index) in groups" :key="group.title" class="mb-3">
|
<ul class="list-none">
|
||||||
<div class="flex items-center justify-between text-sm text-gray-500" @click="selectGroupKey(index)">
|
<li v-for="item in groups" :key="item.conversationId" @click="selectedHistoryMessage(item.conversationId)"
|
||||||
<span>{{ group.title }}</span>
|
:class="[
|
||||||
<RiArrowDownSLine v-show="group.selected" color="rgba(153,160,174,1)" />
|
|
||||||
<RiArrowRightSLine v-show="!group.selected" color="rgba(153,160,174,1)" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="list-none mt-1.5" v-if="group.selected">
|
|
||||||
<li v-for="item in group.items" :key="item.id" @click="selectedHistoryMessage(item.id)" :class="[
|
|
||||||
'flex items-center gap-2 p-2 text-gray-600 rounded-lg cursor-pointer transition-colors',
|
'flex items-center gap-2 p-2 text-gray-600 rounded-lg cursor-pointer transition-colors',
|
||||||
item.id === selectedId ? 'bg-white shadow-sm border-[##E5E8EE]' : 'hover:bg-gray-50'
|
item.conversationId === selectedConversationId ? 'bg-white shadow-sm border-[#E5E8EE] py-1.5 relative z-10' : 'hover:bg-gray-200'
|
||||||
]">
|
]">
|
||||||
<span class="w-2 h-2 rounded-full bg-[#BEDBFF] flex-none"></span>
|
<span class="w-2 h-2 rounded-full bg-[#BEDBFF] flex-none"></span>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="truncate text-sm">{{ item.title }}</div>
|
<div class="truncate text-sm">{{ item.conversationId }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="item.id === selectedId"
|
<button v-if="item.conversationId === selectedConversationId"
|
||||||
class="bg-transparent border-0 text-gray-500 text-lg px-1 py-0">…</button>
|
class="bg-transparent border-0 text-gray-500 text-lg px-1 py-0">…</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted, defineEmits } from 'vue'
|
||||||
import { RiAddLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/vue'
|
import { RiAddLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/vue'
|
||||||
|
import { getConversationList } from '../../api/ConversationApi';
|
||||||
|
|
||||||
|
interface HistoryMessage {
|
||||||
|
conversationId: string;
|
||||||
|
conversationTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
/// 记录选择的历史消息ID
|
/// 记录选择的历史消息ID
|
||||||
const selectedId = ref<number | null>(2)
|
const selectedConversationId = ref<string>('')
|
||||||
|
|
||||||
/// 历史消息分组数据
|
/// 历史消息分组数据
|
||||||
const groups = ref([
|
const groups = ref<Array<HistoryMessage>>([])
|
||||||
{
|
|
||||||
title: '近3天',
|
|
||||||
selected: false,
|
|
||||||
items: [
|
|
||||||
{ id: 1, title: '这是一段对话' },
|
|
||||||
{ id: 2, title: '这是一段对话' },
|
|
||||||
{ id: 3, title: '这是一段对话这是一段对话这是一段对话' },
|
|
||||||
{ id: 4, title: '这是一段对话这是一段对话' },
|
|
||||||
{ id: 5, title: '这是一段对话这是一段对话' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '近7天',
|
|
||||||
selected: false,
|
|
||||||
items: [
|
|
||||||
{ id: 6, title: '这是一段对话' },
|
|
||||||
{ id: 7, title: '这是一段对话' },
|
|
||||||
{ id: 8, title: '这是一段对话这是一段对话' },
|
|
||||||
{ id: 9, title: '这是一段对话这是一段对话' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '近15天',
|
|
||||||
selected: false,
|
|
||||||
items: [
|
|
||||||
{ id: 10, title: '这是一段对话' },
|
|
||||||
{ id: 11, title: '这是一段对话' },
|
|
||||||
{ id: 12, title: '这是一段对话这是一段对话这是一段对话' },
|
|
||||||
{ id: 13, title: '这是一段对话这是一段对话' },
|
|
||||||
{ id: 14, title: '这是一段对话这是一段对话' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '近30天',
|
|
||||||
selected: false,
|
|
||||||
items: [
|
|
||||||
{ id: 15, title: '这是一段对话' },
|
|
||||||
{ id: 16, title: '这是一段对话' },
|
|
||||||
{ id: 17, title: '这是一段对话这是一段对话' },
|
|
||||||
{ id: 18, title: '这是一段对话这是一段对话' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '近60天',
|
|
||||||
selected: false,
|
|
||||||
items: [
|
|
||||||
{ id: 19, title: '这是一段对话' },
|
|
||||||
{ id: 20, title: '这是一段对话' },
|
|
||||||
{ id: 21, title: '这是一段对话这是一段对话这是一段对话' },
|
|
||||||
{ id: 22, title: '这是一段对话这是一段对话' },
|
|
||||||
{ id: 23, title: '这是一段对话这是一段对话' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '近90天',
|
|
||||||
selected: false,
|
|
||||||
items: [
|
|
||||||
{ id: 24, title: '这是一段对话' },
|
|
||||||
{ id: 25, title: '这是一段对话' },
|
|
||||||
{ id: 26, title: '这是一段对话这是一段对话' },
|
|
||||||
{ id: 27, title: '这是一段对话这是一段对话' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
/// 选择历史消息
|
/// 定义事件
|
||||||
const selectedHistoryMessage = (id: number) => {
|
const emit = defineEmits(['new-chat', 'select-chat'])
|
||||||
selectedId.value = id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 选择分组展开/收起
|
/// 添加新对话
|
||||||
const selectGroupKey = (index: number) => {
|
|
||||||
groups.value.forEach((group, i) => {
|
|
||||||
if (i === index) {
|
|
||||||
group.selected = !group.selected
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TODO: 添加新对话
|
|
||||||
const addNewChat = () => {
|
const addNewChat = () => {
|
||||||
console.log('add new chat')
|
console.log('add new chat')
|
||||||
|
// 触发新对话事件
|
||||||
|
emit('new-chat')
|
||||||
|
// 清空选择的历史消息ID
|
||||||
|
selectedConversationId.value = ''
|
||||||
|
// 获取最新的历史会话列表
|
||||||
|
getHistoryConversationList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 选择历史消息
|
||||||
|
const selectedHistoryMessage = (conversationId: string) => {
|
||||||
|
selectedConversationId.value = conversationId
|
||||||
|
emit('select-chat', conversationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 页面加载时获取历史会话列表
|
||||||
|
onMounted(() => {
|
||||||
|
getHistoryConversationList()
|
||||||
|
})
|
||||||
|
|
||||||
|
/// 获取历史会话列表
|
||||||
|
const getHistoryConversationList = async () => {
|
||||||
|
const list = await getConversationList({ pageSize: 20, pageNum: 1 })
|
||||||
|
if (!list || !list.records) return;
|
||||||
|
groups.value.push(...list.records.map((item: any) => ({
|
||||||
|
conversationId: item.conversationId,
|
||||||
|
conversationTitle: item.conversationTitle
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
31
src/renderer/views/home/TaskCenter.vue
Normal file
31
src/renderer/views/home/TaskCenter.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-1 pb-6">
|
||||||
|
<div class="flex justify-between items-center py-4">
|
||||||
|
<h3 class="text-base font-semibold">任务中心</h3>
|
||||||
|
<a class="text-[#3b82f6] text-[13px] cursor-pointer">
|
||||||
|
编辑
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4 max-[800px]:grid-cols-1">
|
||||||
|
<div v-for="n in 14" :key="n" class="flex gap-3 items-start p-3.5
|
||||||
|
rounded-[10px] border border-[#dfeaf6] bg-white">
|
||||||
|
<div class="w-11 h-11 bg-[#EFF6FF] rounded-lg
|
||||||
|
border border-dashed border-[#9fc0e8]
|
||||||
|
flex items-center justify-center
|
||||||
|
text-[#3b82f6] text-[23px]">
|
||||||
|
销
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold">
|
||||||
|
每日销售数据
|
||||||
|
</div>
|
||||||
|
<div class="text-[#9aa5b1] text-[13px] mt-1.5">
|
||||||
|
分析用于销售渠道每日数据汇总及简要展示
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
34
src/renderer/views/home/components/ChatAttach.vue
Normal file
34
src/renderer/views/home/components/ChatAttach.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tag-flex flex-wrap pt-3">
|
||||||
|
<div class="inline-flex items-center justify-center box-border border border-[#E5E8EE] rounded-lg py-0.5 px-2.5 mr-2 mb-2"
|
||||||
|
v-for="(item, index) in questionList" :key="index" @click="handleClick(item)">
|
||||||
|
<span class="tag-text-[#2d91ff] text-[10px]">{{ item }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
question: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const questionList = ref<string[]>([]);
|
||||||
|
|
||||||
|
// 定义 emit 事件,向父组件发送选中的 tag
|
||||||
|
const emit = defineEmits<{ (e: 'select', tag: string): void }>();
|
||||||
|
|
||||||
|
const handleClick = (item: string) => {
|
||||||
|
emit('select', item);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
questionList.value = props.question.split(/[&|;]/).filter((tag) => tag.trim() !== "");
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
48
src/renderer/views/home/components/ChatInputArea.vue
Normal file
48
src/renderer/views/home/components/ChatInputArea.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-[174px] bg-white rounded-lg border border-[#eef2f6] shadow-[0_1px_0_rgba(0,0,0,0.03)] p-4 mt-2 flex flex-col justify-between">
|
||||||
|
<textarea
|
||||||
|
rows="2"
|
||||||
|
placeholder="给我发布或者布置任务"
|
||||||
|
class="flex-1 resize-none outline-none text-sm"
|
||||||
|
:value="modelValue"
|
||||||
|
@input="onInput"
|
||||||
|
@keydown.enter="onKeydownEnter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-end">
|
||||||
|
<button @click="onAttach">
|
||||||
|
<RiLink />
|
||||||
|
</button>
|
||||||
|
<button class="w-12 h-12 bg-[#F5F7FA] px-2.5 py-1.5 rounded-md flex items-center justify-center" @click="onSend">
|
||||||
|
<RiStopFill v-if="isSendingMessage" />
|
||||||
|
<RiSendPlaneFill v-else />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, defineEmits } from 'vue'
|
||||||
|
import { RiLink, RiSendPlaneFill, RiStopFill } from '@remixicon/vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: String, default: '' },
|
||||||
|
isSendingMessage: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'send', 'attach'])
|
||||||
|
|
||||||
|
const onInput = (e: Event) => {
|
||||||
|
const v = (e.target as HTMLTextAreaElement).value
|
||||||
|
emit('update:modelValue', v)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeydownEnter = (e: KeyboardEvent) => {
|
||||||
|
if ((e as KeyboardEvent).shiftKey) return
|
||||||
|
e.preventDefault()
|
||||||
|
emit('send')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAttach = () => emit('attach')
|
||||||
|
const onSend = () => emit('send')
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-sm text-gray-700 flex flex-row">
|
<div class="max-w-[75%] flex flex-col">
|
||||||
<div v-html="compiledMarkdown"></div>
|
<slot name="header"></slot>
|
||||||
<ChatLoading v-if="msg.isLoading" />
|
<div class="text-sm text-gray-700 flex flex-row">
|
||||||
|
<div v-html="compiledMarkdown"></div>
|
||||||
|
<ChatLoading v-if="msg.isLoading" />
|
||||||
|
</div>
|
||||||
|
<slot name="footer"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-sm text-gray-700 bg-[#f7f9fc] rounded-md px-2 py-2">
|
<div class="max-w-[75%]">
|
||||||
{{ msg.messageContent }}
|
<slot name="header"></slot>
|
||||||
|
<div class="text-sm text-gray-700 bg-[#f7f9fc] rounded-md px-2 py-2">
|
||||||
|
{{ msg.messageContent }}
|
||||||
|
</div>
|
||||||
|
<slot name="footer"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<layout>
|
<layout>
|
||||||
<div class="flex h-full w-full flex-col md:flex-row ">
|
<div class="flex h-full w-full flex-col md:flex-row">
|
||||||
<chat-history class="flex-none w-64" />
|
<ChatHistory class="flex-none w-50" @new-chat="guide = true" @select-chat="handleSelectChat" />
|
||||||
<div class="flex-1 mr-2 overflow-hidden bg-white rounded-xl">
|
<div class="flex-1 mr-2 overflow-hidden bg-white rounded-xl">
|
||||||
<!-- <chat-guide /> -->
|
<ChatBox v-model:guide="guide" :conversationId="selectedConversationId" />
|
||||||
<chat-box />
|
|
||||||
</div>
|
</div>
|
||||||
<TaskList />
|
<TaskList />
|
||||||
</div>
|
</div>
|
||||||
@@ -14,6 +13,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TaskList from '@renderer/components/TaskList/index.vue'
|
import TaskList from '@renderer/components/TaskList/index.vue'
|
||||||
import ChatHistory from './ChatHistory.vue'
|
import ChatHistory from './ChatHistory.vue'
|
||||||
import ChatGuide from './ChatGuide.vue'
|
|
||||||
import ChatBox from './ChatBox.vue'
|
import ChatBox from './ChatBox.vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
/// 是否显示引导页
|
||||||
|
const guide = ref(true)
|
||||||
|
/// 选择的历史会话ID
|
||||||
|
const selectedConversationId = ref('')
|
||||||
|
|
||||||
|
/// 选择历史会话
|
||||||
|
const handleSelectChat = (conversationId: string) => {
|
||||||
|
guide.value = false;
|
||||||
|
selectedConversationId.value = conversationId;
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class ChatMessage {
|
|||||||
// 工具调用信息
|
// 工具调用信息
|
||||||
toolCall?: any;
|
toolCall?: any;
|
||||||
// 问题信息
|
// 问题信息
|
||||||
question?: any;
|
question?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
class="bg-sky-50 rounded-[8px] text-[14px] text-sky-600 px-[12px] py-[6px] focus-visible:outline-none cursor-pointer">注册</button> -->
|
class="bg-sky-50 rounded-[8px] text-[14px] text-sky-600 px-[12px] py-[6px] focus-visible:outline-none cursor-pointer">注册</button> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-center justify-center mb-[24px] box-border pt-[108px]">
|
<div class="flex flex-col items-center justify-center mb-[24px] box-border pt-[40px]">
|
||||||
<img class="w-[80px] h-[80px] mb-[12px]" src="@assets/images/login/user_icon.png" />
|
<img class="w-[80px] h-[80px] mb-[12px]" src="@assets/images/login/user_icon.png" />
|
||||||
<div class="text-[24px] font-500 text-gray-800 line-height-[32px] mb-[4px]">登录</div>
|
<div class="text-[24px] font-500 text-gray-800 line-height-[32px] mb-[4px]">登录</div>
|
||||||
<div class="text-[16px] text-gray-500 line-height-[24px]">24小时在岗,从不打烊的数字员工</div>
|
<div class="text-[16px] text-gray-500 line-height-[24px]">24小时在岗,从不打烊的数字员工</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white box-border w-full h-full flex">
|
<layout>
|
||||||
<SystemConfig @change=onChange />
|
<div class="bg-white box-border w-full h-full flex">
|
||||||
|
<SystemConfig @change=onChange />
|
||||||
<component :is="currentComponent" />
|
<component :is="currentComponent" />
|
||||||
</div>
|
</div>
|
||||||
|
</layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
Reference in New Issue
Block a user