Compare commits
3 Commits
8714478587
...
feature/ds
| Author | SHA1 | Date | |
|---|---|---|---|
| d566344eb8 | |||
| 236abba8d0 | |||
| 47a361e78b |
@@ -1,9 +1,18 @@
|
|||||||
<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'">
|
||||||
|
|
||||||
@@ -42,40 +51,26 @@
|
|||||||
</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" @keydown.enter="handleKeydownEnter" />
|
|
||||||
|
|
||||||
<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";
|
||||||
@@ -86,12 +81,36 @@ 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 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';
|
||||||
|
|
||||||
|
// 支持外部通过 prop 控制是否为引导页
|
||||||
|
const props = defineProps({
|
||||||
|
guide: { type: Boolean, default: true }
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['update:guide']);
|
||||||
|
|
||||||
|
/// 是否是引导页(内部响应式)
|
||||||
|
const isGuidePage = ref(props.guide);
|
||||||
|
|
||||||
|
// 同步外部变化到内部
|
||||||
|
watch(() => props.guide, (v) => {
|
||||||
|
isGuidePage.value = v;
|
||||||
|
});
|
||||||
|
// 将内部变化通知父组件
|
||||||
|
watch(isGuidePage, (v) => {
|
||||||
|
emit('update:guide', v);
|
||||||
|
if (v) {
|
||||||
|
// 当切换到引导页时,重置/清理会话状态
|
||||||
|
resetConversation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 列表滚动容器引用
|
// 列表滚动容器引用
|
||||||
const listRef = ref<HTMLElement | null>(null);
|
const listRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
@@ -189,9 +208,16 @@ const handleReplyInstruct = async (message: string, type: string) => {
|
|||||||
setTimeoutScrollToBottom();
|
setTimeoutScrollToBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 选择标签事件
|
/// 选择标签事件:切换到聊天页并发送
|
||||||
const onTagSelect = (text: string) => {
|
const onTagSelect = (text: string) => {
|
||||||
handleReplyText(text);
|
isGuidePage.value = false;
|
||||||
|
nextTick(() => handleReplyText(text));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在引导页中按发送:切换到聊天页再发送
|
||||||
|
const onGuideSend = () => {
|
||||||
|
isGuidePage.value = false;
|
||||||
|
nextTick(() => sendMessageAction());
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 添加附件按钮事件
|
/// 添加附件按钮事件
|
||||||
@@ -217,16 +243,6 @@ const sendMessageAction = () => {
|
|||||||
setTimeoutScrollToBottom();
|
setTimeoutScrollToBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理在 textarea 中按 Enter:Shift+Enter 保留换行,单按 Enter 发送并阻止默认换行行为
|
|
||||||
const handleKeydownEnter = (e: KeyboardEvent) => {
|
|
||||||
if ((e as KeyboardEvent).shiftKey) {
|
|
||||||
// 允许插入换行
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
sendMessageAction();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 停止发送消息事件
|
// 停止发送消息事件
|
||||||
const sendStopAction = () => {
|
const sendStopAction = () => {
|
||||||
console.log("停止发送消息");
|
console.log("停止发送消息");
|
||||||
@@ -757,5 +773,33 @@ 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>
|
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, defineEmits } from 'vue'
|
||||||
import { RiAddLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/vue'
|
import { RiAddLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/vue'
|
||||||
|
|
||||||
/// 记录选择的历史消息ID
|
/// 记录选择的历史消息ID
|
||||||
@@ -124,8 +124,11 @@ const selectGroupKey = (index: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// TODO: 添加新对话
|
/// TODO: 添加新对话
|
||||||
|
const emit = defineEmits(['new-chat'])
|
||||||
|
|
||||||
const addNewChat = () => {
|
const addNewChat = () => {
|
||||||
console.log('add new chat')
|
console.log('add new chat')
|
||||||
|
emit('new-chat')
|
||||||
}
|
}
|
||||||
|
|
||||||
</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>
|
||||||
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>
|
||||||
@@ -16,7 +16,6 @@ import MarkdownIt from 'markdown-it'
|
|||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
import 'highlight.js/styles/github.css'
|
import 'highlight.js/styles/github.css'
|
||||||
import ChatLoading from './ChatLoading.vue';
|
import ChatLoading from './ChatLoading.vue';
|
||||||
import { sl } from 'element-plus/es/locale/index.mjs';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
msg: ChatMessage
|
msg: ChatMessage
|
||||||
|
|||||||
@@ -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" />
|
||||||
<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" />
|
||||||
<chat-box />
|
|
||||||
</div>
|
</div>
|
||||||
<TaskList />
|
<TaskList />
|
||||||
</div>
|
</div>
|
||||||
@@ -14,6 +13,8 @@
|
|||||||
<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)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user