Files
lingji-work-fe/src/views/home/components/conversation-detail/index.vue
2025-08-21 16:26:57 +08:00

231 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="tsx">
import { message, Tooltip } from 'ant-design-vue';
import { BubbleList } from '@/components/xt-chat/xt-bubble';
import SenderInput from '../sender-input/index.vue';
import { Typography } from 'ant-design-vue';
import RightView from './rightView.vue';
import { useRoute } from 'vue-router';
import markdownit from 'markdown-it';
import { useClipboard } from '@vueuse/core';
import { genRandomId } from '@/utils/tools';
import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types';
const QUESTION_ROLE = 'question';
const ANSWER_ROLE = 'text';
export default {
setup(props, { emit, expose }) {
const route = useRoute();
const { copy } = useClipboard();
const senderRef = ref(null);
const rightViewRef = ref(null);
const bubbleListRef = ref<any>(null);
const searchValue = ref('');
const generateLoading = ref(false);
const conversationList = ref([]);
const currentAnswerId = ref<string | null>(null);
const showRightView = ref(false);
const rightViewContent = ref('');
const md = markdownit({
html: true,
breaks: true,
linkify: true,
typographer: true,
});
const conversationId = computed(() => {
return route.params.conversationId;
});
const onCopy = (content: string) => {
copy(content);
message.success('复制成功!');
};
const onRefresh = (tempId: string, tempIndex: number) => {
generateLoading.value = true;
conversationList.value.splice(tempIndex, 1, {
id: tempId,
loading: true,
role: ANSWER_ROLE,
});
};
const handleSubmit = () => {
if (generateLoading.value) {
message.warning('停止生成后可发送');
return;
}
generateLoading.value = true;
conversationList.value.push({
role: QUESTION_ROLE,
content: searchValue.value,
});
const tempId = genRandomId();
const tempIndex = conversationList.value.length;
conversationList.value.push({
id: tempId,
loading: true,
role: ANSWER_ROLE,
});
currentAnswerId.value = tempId;
setTimeout(() => {
const content = `# 测试数据表格
## 用户信息表
| 表头1 | 表头2 |
|---|---|
| 内容1 | 内容2 |
## 项目统计表
| 项目名称 | 负责人 | 进度 | 状态 | 预算(万元) | 完成度 |
|----------|--------|------|------|------------|--------|
| 电商平台重构 | 张三 | 75% | 进行中 | 50 | 🟡 |
| 移动端APP | 李四 | 90% | 测试中 | 30 | 🟢 |
| 数据分析系统 | 王五 | 45% | 开发中 | 80 | 🟡 |
| 客户管理系统 | 赵六 | 100% | 已完成 | 25 | 🟢 |
| 营销自动化 | 钱七 | 30% | 规划中 | 60 | 🔴 |
## 销售数据
| 月份 | 销售额(万) | 订单数 | 客户数 | 增长率 | 备注 |
|------|------------|--------|--------|--------|------|
| 1月 | 120.5 | 156 | 89 | +12% | 春节促销 |
| 2月 | 98.3 | 134 | 76 | -18% | 淡季 |
| 3月 | 145.2 | 189 | 102 | +48% | 新品上市 |
| 4月 | 167.8 | 203 | 115 | +16% | 稳定增长 |
| 5月 | 189.6 | 234 | 128 | +13% | 持续增长 |
> 以上数据仅供参考,实际数据请以系统为准。
**注意事项:**
- 所有数据均为测试数据
- 表格支持Markdown格式渲染
- 可以包含表情符号和特殊字符`;
showRightView.value = true;
rightViewContent.value = '';
nextTick(() => {
rightViewContent.value = content;
});
searchValue.value = '';
conversationList.value.splice(tempIndex, 1, {
id: tempId,
loading: false,
role: ANSWER_ROLE,
content,
messageRender: (content) => (
<Typography>
<div v-html={md.render(content)}></div>
</Typography>
),
footer: ({ item }) => {
// 判断当前元素是否是最后一个非QUESTION_ROLE的元素
const nonQuestionElements = conversationList.value.filter((item) => item.role !== QUESTION_ROLE);
const isLastAnswer = nonQuestionElements[nonQuestionElements.length - 1]?.id === item.id;
return (
<div class="flex items-center">
<Tooltip title="复制" onClick={() => onCopy(content)}>
<icon-copy size={16} class="mr-12px color-#737478 cursor-pointer" />
</Tooltip>
{isLastAnswer && (
<Tooltip title="重新生成" onClick={() => onRefresh(tempId, tempIndex)}>
<icon-refresh size={16} class="color-#737478 cursor-pointer" />
</Tooltip>
)}
</div>
);
},
});
}, 1000);
};
const handleCancel = () => {
generateLoading.value = false;
// 中止当前正在输出的回答
if (currentAnswerId.value && bubbleListRef.value?.abortTypingByKey) {
bubbleListRef.value.abortTypingByKey(currentAnswerId.value);
}
if (showRightView.value) {
rightViewRef.value?.abortTyping?.();
}
message.info('取消生成');
};
const roles: BubbleListProps['roles'] = {
[ANSWER_ROLE]: {
placement: 'start',
variant: 'borderless',
typing: { step: 2, interval: 100 },
onTypingComplete: () => {
console.log('onTypingComplete');
generateLoading.value = false;
currentAnswerId.value = null;
},
style: {
width: '600px',
margin: '0 auto',
},
},
[QUESTION_ROLE]: {
placement: 'end',
shape: 'round',
style: {
width: '600px',
margin: '0 auto',
},
},
};
return () => (
<div class="conversation-detail-wrap w-full h-full flex">
<section class="flex-1 flex flex-col pt-20px justify-center relative px-16px">
{/* <div class="w-full h-full flex "> */}
<div class="flex-1 overflow-hidden pb-20px">
<BubbleList ref={bubbleListRef} roles={roles} items={conversationList.value} />
</div>
<div class="w-full flex flex-col justify-center items-center">
<SenderInput
class="w-600px"
ref={senderRef}
placeholder="继续追问..."
loading={generateLoading.value}
v-model={searchValue.value}
onSubmit={handleSubmit}
onCancel={handleCancel}
data-ne='123'
/>
<p class="cts !color-#939499 text-12px !lh-20px my-4px">内容由AI生成仅供参考</p>
</div>
{/* </div> */}
</section>
{/* 右侧展示区域 */}
{showRightView.value && (
<RightView
ref={rightViewRef}
rightViewContent={rightViewContent.value}
showRightView={showRightView.value}
onClose={() => (showRightView.value = false)}
/>
)}
</div>
);
},
};
</script>
<style lang="scss" scoped>
@import './style.scss';
</style>