
使用vue-element-plus-x
中文文档地址vue-element-plus-x (element-plus-x.com/)
ts
// 下载
npm install vue-element-plus-x --save
// 中文文档地址
对话组件
这里使用的都是模拟的静态数据,调用SSE接口也可查看element-puls-x文档中的工具useXStream,这里主要是试用,目前所有功能基本ok,后期优化后会更下面的组件
js
<template>
<div class="element_plus_x">
<div class="empty message_main_container" v-if="messageList.length == 0">
<Welcome
icon="https://camo.githubusercontent.com/4ea7fdaabf101c16965c0bd3ead816c9d7726a59b06f0800eb7c9a30212d5a6a/68747470733a2f2f63646e2e656c656d656e742d706c75732d782e636f6d2f656c656d656e742d706c75732d782e706e67"
title="欢迎使用 Element Plus X 💖"
description="这是描述信息 ~"
/>
</div>
<div class="message_main_container" ref="reasoningRef" v-else>
<!-- <div class="message_main_container" ref="reasoningRef"> -->
<BubbleList :list="messageList" max-height="100%">
<!-- 自定义气泡内容 -->
<template #content="{ item }">
<XMarkdown :markdown="item.content" :code-x-render="selfCodeXRender" />
</template>
</BubbleList>
</div>
<Sender
ref="senderRef"
v-model="senderValue"
:loading="senderLoading"
:auto-size="{ minRows: 3, maxRows: 5 }"
clearable
@submit="handleSubmit"
@cancel="handleCancel"
class="ai_dialog_sender"
:inputStyle="inputStyle"
/>
</div>
</template>
<script setup lang="ts">
import { toRefs, reactive, ref, computed, onMounted, watch, onUnmounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Top, Search, Paperclip, Refresh } from '@element-plus/icons-vue';
import { BubbleList, Sender, EditorSender, XMarkdown, Typewriter, Bubble, Welcome, FilesCard } from 'vue-element-plus-x';
// npm install vue-element-plus-x --save
import type { BubbleListItemProps, BubbleListProps } from 'vue-element-plus-x/types/BubbleList';
import AiItem from './iaItem.vue';
interface IProps {
aiDialogVisible: boolean;
}
const searchFormRef = ref();
const emits = defineEmits(['updateModelValue']);
const props = withDefaults(defineProps<IProps>(), {
aiDialogVisible: false,
});
interface IMessage {
role: 'user' | 'assistant';
content: string;
content1?: string;
isStreaming?: boolean;
isError?: boolean;
}
const state = reactive({
messages: [] as IMessage[],
inputText: '',
checked1: false,
checked2: false,
isStreaming: false,
isFullscreen: false, // 是否全屏
});
const { messages, inputText, checked1, checked2, isStreaming, isFullscreen } = toRefs(state);
const closeHandle = () => {
emits('updateModelValue', false);
};
// 全屏切换
const fullscreenHandle = () => {
isFullscreen.value = !isFullscreen.value;
};
type listType = BubbleListItemProps & {
key: number;
role: 'user' | 'ai';
};
// 示例调用
const messageList: BubbleListProps<listType>['list'] = ref([]);
function generateFakeItems(count: number): listType[] {
const messages: listType[] = [];
for (let i = 0; i < count; i++) {
const role = i % 2 === 0 ? 'ai' : 'user';
const placement = role === 'ai' ? 'start' : 'end';
const key = i + 1;
const content = role === 'ai' ? '💖 感谢使用 Element Plus X ! 你的支持,是我们开源的最强动力 ~'.repeat(5) : `哈哈哈,让我试试`;
const loading = false;
const shape = 'corner';
const variant = role === 'ai' ? 'filled' : 'outlined';
// const variant = role === 'ai' ? 'filled' : 'filled';
const isMarkdown = false;
const typing = role === 'ai' ? i === count - 1 : false;
const avatar =
role === 'ai' ? 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png' : 'https://avatars.githubusercontent.com/u/76239030?v=4';
messages.push({
key, // 唯一标识
role, // user | ai 自行更据模型定义
placement, // start | end 气泡位置
content, // 消息内容 流式接受的时候,只需要改这个值即可
loading, // 当前气泡的加载状态
shape, // 气泡的形状
variant, // 气泡的样式
isMarkdown, // 是否渲染为 markdown
typing, // 是否开启打字器效果 该属性不会和流式接受冲突
isFog: role === 'ai', // 是否开启打字雾化效果,该效果 v1.1.6 新增,且在 typing 为 true 时生效,该效果会覆盖 typing 的 suffix 属性
avatar,
avatarSize: '24px', // 头像占位大小
avatarGap: '12px', // 头像与气泡之间的距离
});
}
messages.push({
key: messages.length + 1,
role: 'user',
placement: 'start',
content: markdown2,
loading: false,
shape: 'corner',
variant: 'filled',
isMarkdown: false,
typing: false,
isFog: false,
avatar: 'https://avatars.githubusercontent.com/u/76239030?v=4',
avatarSize: '24px',
avatarGap: '12px',
});
messages.push({
key: messages.length + 1,
role: 'user',
placement: 'start',
content: markdown,
loading: false,
shape: 'corner',
variant: 'filled',
isMarkdown: false,
typing: false,
isFog: false,
avatar: 'https://avatars.githubusercontent.com/u/76239030?v=4',
avatarSize: '24px',
avatarGap: '12px',
});
return messages;
}
const senderRef = ref();
const timeValue = ref<NodeJS.Timeout | null>(null);
const senderValue = ref('');
const senderLoading = ref(false);
const isSelect = ref(false);
const submitBtnDisabled = ref(true);
function handleSubmit(value: string) {
ElMessage.info(`发送中`);
senderLoading.value = true;
messageList.value = generateFakeItems(10);
timeValue.value = setTimeout(() => {
// 可以在控制台 查看打印结果
console.log('submit-> value:', value);
console.log('submit-> senderValue', senderValue.value);
senderLoading.value = false;
ElMessage.success(`发送成功`);
}, 3500);
}
function handleCancel() {
senderLoading.value = false;
if (timeValue.value) clearTimeout(timeValue.value);
timeValue.value = null;
ElMessage.info(`取消发送`);
}
import { h } from 'vue';
import Echarts from './echarts.vue';
const markdown2 = `
# Hello\n## Hi\n> hello world
\`\`\`javascript
const a = 1;
import Echarts from './echarts.vue';
\`\`\`
\`\`\`echarts
option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line'
}
]
};
\`\`\`
`;
const markdown = `
\`\`\`html
<div class="product-card">
<div class="badge">新品</div>
<img src="https://picsum.photos/300/200?product" alt="产品图片">
<div class="content">
<h3>无线蓝牙耳机 Pro</h3>
<p class="description">主动降噪技术,30小时续航,IPX5防水等级</p>
<div class="rating">
<span>★★★★☆</span>
<span class="reviews">(124条评价)</span>
</div>
<div class="price-container">
<span class="price">¥499</span>
<span class="original-price">¥699</span>
<span class="discount">7折</span>
</div>
<div class="actions">
<button class="cart-btn" onclick="addToCart(this)">加入购物车</button>
<button class="fav-btn">❤️</button>
</div>
<div class="meta">
<span>✓ 次日达</span>
<span>✓ 7天无理由</span>
</div>
</div>
</div>
\<script\>
function addToCart(button) {
// 防止重复点击
if (button.disabled) return;
// 禁用按钮
button.disabled = true;
button.innerHTML = '加入中...';
// 模拟添加到购物车的API请求
setTimeout(() => {
// 显示成功消息
button.innerHTML = '✓ 已加入';
button.classList.add('success');
// 3秒后恢复按钮状态
setTimeout(() => {
button.innerHTML = '加入购物车';
button.disabled = false;
button.classList.remove('success');
}, 3000);
}, 800);
};
\<\/script\>
<style>
.product-card {
width: 280px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: white;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.badge {
position: absolute;
top: 12px;
left: 12px;
background: #ff6b6b;
color: white;
padding: 4px 10px;
border-radius: 4px;
font-weight: bold;
font-size: 12px;
z-index: 2;
}
img {
width: 100%;
height: 180px;
object-fit: cover;
display: block;
}
.content {
padding: 16px;
}
h3 {
margin: 8px 0;
font-size: 18px;
color: #333;
}
.description {
color: #666;
font-size: 14px;
margin: 8px 0 12px;
line-height: 1.4;
}
.rating {
display: flex;
align-items: center;
margin: 10px 0;
color: #ffb300;
}
.reviews {
font-size: 13px;
color: #888;
margin-left: 8px;
}
.price-container {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 0;
}
.price {
font-size: 22px;
font-weight: bold;
color: #ff4757;
}
.original-price {
font-size: 14px;
color: #999;
text-decoration: line-through;
}
.discount {
background: #fff200;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.actions {
display: flex;
gap: 8px;
margin: 16px 0 12px;
}
.cart-btn {
flex: 1;
background: #5352ed;
color: white;
border: none;
padding: 10px;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.cart-btn:hover {
background: #3742fa;
}
.cart-btn.disabled {
background: #a4b0be;
cursor: not-allowed;
}
.cart-btn.success {
background: #2ed573;
}
.fav-btn {
width: 42px;
background: white;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 18px;
cursor: pointer;
transition: all 0.2s;
}
.fav-btn:hover {
border-color: #ff6b6b;
color: #ff6b6b;
}
.meta {
display: flex;
gap: 15px;
font-size: 13px;
color: #2ed573;
margin-top: 8px;
}
</style>
\`\`\`
`;
const selfCodeXRender = {
// 渲染自定义代码块标识符 javascript, 返回一个组件
javascript: (props: { raw: any }) => {
return h('pre', { class: 'language-javascript' }, h('code', { class: 'language-javascript' }, props.raw.content));
},
// 渲染自定义代码块标识符 echarts, Echarts 是自己封装的Vue组件
echarts: (props: { raw: any }) => h(Echarts, { code: props.raw.content, width: '500px' }),
};
const inputStyle = {
color: '#fff !important',
};
</script>
<style lang="scss" scoped>
.element_plus_x {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.message_main_container {
width: 100%;
flex: 1;
height: 0;
margin: 20px auto;
// padding: 0 5%;
padding: 0 20px;
}
.ai_dialog_sender {
margin-bottom: 10px;
}
}
</style>
- Echarts组件
js
<!-- echarts.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue';
import * as echarts from 'echarts';
// 保留原有props.code逻辑,同时添加可选配置
const props = defineProps<{
code: string; // 原始JSON字符串配置
width?: string; // 可选:图表宽度
height?: string; // 可选:图表高度
theme?: string; // 可选:图表主题
}>();
const refEle = ref<HTMLElement>();
let myChart: echarts.ECharts | null = null; // 图表实例引用
function parseEChartsOption(str: string): any {
try {
let cleanedStr = str.replace(/^option\s*=\s*/, '').replace(/;\s*$/, '');
cleanedStr = cleanedStr.replace(/'/g, '"');
cleanedStr = cleanedStr.replace(/(\w+)\s*:/g, '"$1":');
return JSON.parse(cleanedStr);
} catch (error) {
console.error('Failed to parse ECharts option:', error);
return null;
}
}
// 核心渲染逻辑(保留原始解析流程)
function renderChart() {
if (!refEle.value) return;
try {
// 解析JSON配置(保留原有逻辑)
const cleanedStr = parseEChartsOption(props.code);
// 初始化/更新图表
if (!myChart) {
myChart = echarts.init(refEle.value, props.theme);
}
myChart.setOption(cleanedStr);
} catch (error) {
console.error('图表配置解析失败:', error);
}
}
// 窗口resize处理
function handleResize() {
myChart?.resize();
}
// 销毁逻辑
function destroyChart() {
if (myChart) {
myChart.dispose(); // 释放ECharts实例
myChart = null;
}
window.removeEventListener('resize', handleResize);
}
// 初始化渲染
onMounted(() => {
renderChart();
window.addEventListener('resize', handleResize); // 添加resize监听
});
// 监听code变化自动更新(关键优化)
watch(
() => props.code,
() => {
renderChart(); // 配置变化时重新渲染
}
);
// 卸载时清理资源
onUnmounted(() => {
destroyChart();
});
</script>
<template>
<div class="echarts-wrap">
<div
ref="refEle"
:style="{
height: height || '400px', // 可选高度,默认400px
width: width || '100%', // 可选宽度,默认100%
}"
/>
</div>
</template>
<style scoped lang="scss">
.echarts-wrap {
position: relative;
color: #fff;
.echarts-titlt {
position: absolute;
width: fit-content;
margin-left: 20px;
color: #fff;
font-size: 20px;
font-weight: bold;
}
}
</style>

- 引入使用
js
<template>
<ElementPlusX />
</template>
import ElementPlusX from './elementPlusX.vue';
