|
@@ -0,0 +1,500 @@
|
|
|
+<template>
|
|
|
+ <div class="ai-chat-container">
|
|
|
+ <div class="chat-body" ref="chatBody">
|
|
|
+ <div class="message ai-message">
|
|
|
+ <div class="message-header">
|
|
|
+ <div class="message-header-img">
|
|
|
+ <avatar :user="{
|
|
|
+ portrait:'https://file-node.oss-cn-shanghai.aliyuncs.com/youji/f9617c7f80da485cb3cc72b6accc62ed'
|
|
|
+ }"></avatar>
|
|
|
+ </div>
|
|
|
+ <div class="message-header-label">AI网站生成助手</div>
|
|
|
+ </div>
|
|
|
+ <div class="message-content">
|
|
|
+ <p>您好!我是AI网站生成助手,请点击按钮生成企业官网。</p>
|
|
|
+ <el-button size="mini" @click="visible = true" style="margin-top: 10px;">
|
|
|
+ 点击生成企业官网
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="message-time">
|
|
|
+ {{ formatTime(messageList.length === 0? new Date():messageList[0].date) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-for="(message, index) in messageList" :key="index">
|
|
|
+ <div class="message user-message">
|
|
|
+ <div class="message-header">
|
|
|
+ <div class="message-header-img">
|
|
|
+ <avatar :user="user"></avatar>
|
|
|
+ </div>
|
|
|
+ <div class="message-header-label">{{user.userName}}</div>
|
|
|
+ </div>
|
|
|
+ <div class="message-content" v-html="message.userInput"></div>
|
|
|
+ <div class="message-time">
|
|
|
+ {{ formatTime(message.date) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="message ai-message" v-if="!loading || message.status !== 'running'"
|
|
|
+ style="margin-top: 15px;">
|
|
|
+ <div class="message-header">
|
|
|
+ <div class="message-header-img">
|
|
|
+ <avatar :user="{
|
|
|
+ portrait:'https://file-node.oss-cn-shanghai.aliyuncs.com/youji/f9617c7f80da485cb3cc72b6accc62ed'
|
|
|
+ }"></avatar>
|
|
|
+ </div>
|
|
|
+ <div class="message-header-label">AI网站生成助手</div>
|
|
|
+ </div>
|
|
|
+ <div class="message-content">
|
|
|
+ <div v-if="message.status === 'failed'">
|
|
|
+ <p>服务器繁忙,请稍后重试</p>
|
|
|
+ <el-button size="mini" @click="reload(message)" :disabled="index !== messageList.length - 1"
|
|
|
+ style="margin-top: 10px;">
|
|
|
+ 点击重试
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="ai-website-boxs" v-else>
|
|
|
+ <div class="ai-website-box" v-html="message.AIInput"></div>
|
|
|
+ <div class="ai-website-mask">
|
|
|
+ <el-button size="mini" type="primary"
|
|
|
+ @click="$emit('previewWebSite', message.AIInput,message.simpleUUID)">
|
|
|
+ 预览
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="message-time">
|
|
|
+ {{ formatTime(message.endDate) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="loading" class="typing-indicator">
|
|
|
+ <div class="typing-dot"></div>
|
|
|
+ <div class="typing-dot"></div>
|
|
|
+ <div class="typing-dot"></div>
|
|
|
+ <span style="margin-left: 10px;">思考中...</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="input-container">
|
|
|
+ <el-input type="textarea" v-model="newMessage" placeholder="给AI发送消息" resize="none" rows="2"
|
|
|
+ :autosize="{ minRows: 2, maxRows: 6 }" :disabled="!htmlCode">
|
|
|
+ </el-input>
|
|
|
+ <div class="input-button">
|
|
|
+ <el-button size="mini" @click="selectProduct" :disabled="loading || !htmlCode">
|
|
|
+ 添加商品
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" size="mini" @click="sendMessage"
|
|
|
+ :disabled="!newMessage.trim() || loading || !htmlCode">
|
|
|
+ 发送
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-dialog :close-on-click-modal="false" title="生成网站" :visible.sync="visible" width="900px"
|
|
|
+ :append-to-body="true">
|
|
|
+ <website-form v-if="visible" @callback="callback"></website-form>
|
|
|
+ </el-dialog>
|
|
|
+ <el-dialog :close-on-click-modal="false" title="选择商品" :visible.sync="visibles" width="900px"
|
|
|
+ :append-to-body="true">
|
|
|
+ <div class="hui-flex hui-content hui-dialog">
|
|
|
+ <select-product ref="selectProduct" v-if="visibles"></select-product>
|
|
|
+ <div class="hui-dialog-submit">
|
|
|
+ <el-button size="small" @click="visibles = false">取 消</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="sureProduct" :loading="productLoading">
|
|
|
+ 确定
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import websiteForm from '@/components/work/common/websiteForm.vue'
|
|
|
+ const selectProduct = () => import('@/components/work/common/selectProduct');
|
|
|
+ import {
|
|
|
+ getAIFlowDataStatus,
|
|
|
+ getAIFlowDataList,
|
|
|
+ createAIFlowData
|
|
|
+ } from '@/api/ai'
|
|
|
+ import Crypto from '@/uitls/crypto'
|
|
|
+ export default {
|
|
|
+ components: {
|
|
|
+ websiteForm,
|
|
|
+ selectProduct
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ messageList: [],
|
|
|
+ newMessage: '',
|
|
|
+ loading: false,
|
|
|
+ darkMode: false,
|
|
|
+ visible: false,
|
|
|
+ timer: null,
|
|
|
+ formData: {
|
|
|
+ company_name: '企业名称',
|
|
|
+ logo_url: '企业logo',
|
|
|
+ company_sub_name: '企业简称',
|
|
|
+ business_scope: '核心业务',
|
|
|
+ key_features: '核心优势',
|
|
|
+ contact_info: '联系方式',
|
|
|
+ industry_sector: '行业领域',
|
|
|
+ create_time: '成立时间',
|
|
|
+ create_place: '成立地点',
|
|
|
+ all_style: '整体风格',
|
|
|
+ model: '主要模块',
|
|
|
+ color_scheme: '主色调',
|
|
|
+ websites_url: '参考网站'
|
|
|
+ },
|
|
|
+ user: {},
|
|
|
+ htmlCode: '',
|
|
|
+ simpleUUID: '',
|
|
|
+ visibles: false,
|
|
|
+ productLoading: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.user = this.$store.getters.user;
|
|
|
+ this.scrollToBottom();
|
|
|
+ this.init()
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ messageList: {
|
|
|
+ deep: true,
|
|
|
+ handler() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.scrollToBottom();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.clearTimer();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ init() {
|
|
|
+ getAIFlowDataList({
|
|
|
+ userId: this.$store.getters.user.userId
|
|
|
+ }).then(res => {
|
|
|
+ if (res.state) {
|
|
|
+ this.messageList = res.data.map(node => {
|
|
|
+ node['userInput'] = this.returnUserInputs(node);
|
|
|
+ let AIInput = ''
|
|
|
+ if (node.outputs) AIInput = this.extractContentBetween(JSON.parse(node.outputs)
|
|
|
+ .text,
|
|
|
+ node.simpleUUID)
|
|
|
+ node['AIInput'] = AIInput;
|
|
|
+ return node;
|
|
|
+ });
|
|
|
+ if (this.messageList.length > 0) {
|
|
|
+ let data = this.messageList[this.messageList.length - 1];
|
|
|
+ if (data.status === 'running') this.initStaus(data.simpleUUID);
|
|
|
+ if (data.status === 'succeeded') this.$emit('previewWebSite', this.htmlCode, this
|
|
|
+ .simpleUUID);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ initStaus(simpleUUlD) {
|
|
|
+ this.loading = true;
|
|
|
+ this.timer = setInterval(() => {
|
|
|
+ getAIFlowDataStatus(simpleUUlD).then(res => {
|
|
|
+ if (res.state) {
|
|
|
+ let data = res.data;
|
|
|
+ if (!data) return;
|
|
|
+ if (data.status === 'succeeded' || data.status === 'failed') {
|
|
|
+ this.clearTimer();
|
|
|
+ this.loading = false;
|
|
|
+ data['userInput'] = this.returnUserInputs(data);
|
|
|
+ let AIInput = ''
|
|
|
+ if (data.outputs) AIInput = this.extractContentBetween(JSON.parse(data
|
|
|
+ .outputs).text, data.simpleUUID)
|
|
|
+ node['AIInput'] = AIInput;
|
|
|
+ this.messageList[this.messageList.length - 1] = data;
|
|
|
+ if (data.status === 'succeeded') this.$emit('previewWebSite', this
|
|
|
+ .htmlCode, this.simpleUUID);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.clearTimer();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }, 20000)
|
|
|
+ },
|
|
|
+ returnUserInputs(data) {
|
|
|
+ let obj = data.inputs;
|
|
|
+ if (!obj) return '';
|
|
|
+ let html = '';
|
|
|
+ let objs = JSON.parse(obj);
|
|
|
+ return objs.query;
|
|
|
+ },
|
|
|
+ clearTimer() {
|
|
|
+ if (!this.timer) return;
|
|
|
+ clearInterval(this.timer);
|
|
|
+ this.timer = null;
|
|
|
+ },
|
|
|
+ formatTime(date) {
|
|
|
+ return new Date(date).toLocaleTimeString([], {
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit'
|
|
|
+ });
|
|
|
+ },
|
|
|
+ scrollToBottom() {
|
|
|
+ const container = this.$refs.chatBody;
|
|
|
+ if (container) {
|
|
|
+ container.scrollTop = container.scrollHeight;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ callback(type) {
|
|
|
+ this.visible = false;
|
|
|
+ if (type === 'init') this.init();
|
|
|
+ },
|
|
|
+ sendMessage() {
|
|
|
+ createAIFlowData(5, {
|
|
|
+ type: 1,
|
|
|
+ query: this.newMessage,
|
|
|
+ html_code: this.htmlCode
|
|
|
+ }).then(this.successFunc);
|
|
|
+ },
|
|
|
+ reload(message) {
|
|
|
+ let data = JSON.parse(message.inputs);
|
|
|
+ createAIFlowData(message.difyTypeId, {
|
|
|
+ type: data.type,
|
|
|
+ query: data.query,
|
|
|
+ html_code: data.html_code
|
|
|
+ }).then(this.successFunc);
|
|
|
+ },
|
|
|
+ successFunc(res) {
|
|
|
+ if (res.state) {
|
|
|
+ this.visibles = false;
|
|
|
+ this.newMessage = '';
|
|
|
+ this.$message.success('操作成功');
|
|
|
+ this.init();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ extractContentBetween(html, simpleUUID) {
|
|
|
+ if (!html) return '';
|
|
|
+ // 使用正则表达式匹配 ```html 和 ``` 之间的内容
|
|
|
+ let regex = /```html([\s\S]*?)```/;
|
|
|
+ let match = html.match(regex);
|
|
|
+ let htmls = match && match[1] ? match[1].trim() : '';
|
|
|
+ this.htmlCode = htmls;
|
|
|
+ this.simpleUUID = simpleUUID;
|
|
|
+ return htmls;
|
|
|
+ },
|
|
|
+ selectProduct() {
|
|
|
+ this.visibles = true;
|
|
|
+ },
|
|
|
+ sureProduct() {
|
|
|
+ let form = this.$refs.selectProduct.form;
|
|
|
+ let currentRow = this.$refs.selectProduct.currentRow.map(node => {
|
|
|
+ let id = Crypto.AES.encrypt(String(node.id), 'bosshand');
|
|
|
+ return {
|
|
|
+ id: encodeURIComponent(id),
|
|
|
+ name: node.name,
|
|
|
+ subtitme: node.subtitle,
|
|
|
+ img: this.imageUrl(node.listDisplayImage),
|
|
|
+ unit: '次'
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (currentRow.length === 0) return this.$message.warning('请至少选择一个商品');
|
|
|
+ createAIData(4, {
|
|
|
+ detailed_html: this.htmlCode,
|
|
|
+ model_name: form.modelName,
|
|
|
+ json_data: JSON.stringify(currentRow)
|
|
|
+ }).then(this.successFunc);
|
|
|
+ },
|
|
|
+ imageUrl(data) {
|
|
|
+ if (data && JSON.parse(data)[0]) return JSON.parse(data)[0].url;
|
|
|
+ return 'https://assets.api.uizard.io/api/cdn/stream/c05650d2-192b-4a56-ae97-05638f53804c.png';
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }
|
|
|
+</script>
|
|
|
+
|
|
|
+<style>
|
|
|
+ .message-image {
|
|
|
+ width: 30px;
|
|
|
+ }
|
|
|
+</style>
|
|
|
+<style scoped lang="scss">
|
|
|
+ .ai-chat-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: white;
|
|
|
+ border-radius: 16px;
|
|
|
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai-website-boxs {
|
|
|
+ width: 185px;
|
|
|
+ height: 108px;
|
|
|
+ overflow: hidden;
|
|
|
+ cursor: pointer;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &:hover .ai-website-mask {
|
|
|
+ display: flex;
|
|
|
+ }
|
|
|
+
|
|
|
+ img {
|
|
|
+ height: revert-layer;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai-website-mask {
|
|
|
+ background: rgba(0, 0, 0, 0.1);
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ display: none;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai-website-box {
|
|
|
+ width: 1850px;
|
|
|
+ height: 1080px;
|
|
|
+ overflow: hidden;
|
|
|
+ transform: scale(0.1);
|
|
|
+ transform-origin: 0 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 聊天内容区域 */
|
|
|
+ .chat-body {
|
|
|
+ flex: 1;
|
|
|
+ padding: 20px;
|
|
|
+ overflow-y: auto;
|
|
|
+ background: #f8fafc;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 15px;
|
|
|
+ overflow-x: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message {
|
|
|
+ max-width: 90%;
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-radius: 18px;
|
|
|
+ position: relative;
|
|
|
+ animation: fadeIn 0.3s ease;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-message {
|
|
|
+ background: #7a72cf;
|
|
|
+ color: white;
|
|
|
+ margin-left: auto;
|
|
|
+ border-bottom-right-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai-message {
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px solid #e9ecef;
|
|
|
+ margin-right: auto;
|
|
|
+ border-bottom-left-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-header-img {
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ border-radius: 30px;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-right: 5px;
|
|
|
+ background-color: #fff;
|
|
|
+ border: 2px solid #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-header-label {
|
|
|
+ flex: 1;
|
|
|
+ width: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .message-time {
|
|
|
+ font-size: 0.7rem;
|
|
|
+ opacity: 0.7;
|
|
|
+ margin-top: 5px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 加载指示器 */
|
|
|
+ .typing-indicator {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 16px;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #e9ecef;
|
|
|
+ border-radius: 18px;
|
|
|
+ width: fit-content;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ border-bottom-left-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .typing-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ background: #6c757d;
|
|
|
+ border-radius: 50%;
|
|
|
+ margin: 0 3px;
|
|
|
+ animation: typing 1.4s infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .typing-dot:nth-child(1) {
|
|
|
+ animation-delay: 0s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .typing-dot:nth-child(2) {
|
|
|
+ animation-delay: 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .typing-dot:nth-child(3) {
|
|
|
+ animation-delay: 0.4s;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 输入区域 */
|
|
|
+ .input-container {
|
|
|
+ padding: 10px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-top: 1px solid #e9ecef;
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-button {
|
|
|
+ margin-top: 10px;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 动画 */
|
|
|
+ @keyframes fadeIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(10px);
|
|
|
+ }
|
|
|
+
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes typing {
|
|
|
+
|
|
|
+ 0%,
|
|
|
+ 60%,
|
|
|
+ 100% {
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ 30% {
|
|
|
+ transform: translateY(-5px);
|
|
|
+ }
|
|
|
+ }
|
|
|
+</style>
|