|
|
|
|
@ -0,0 +1,766 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div v-if="state.isShowSearch" class="ai-float-window ai-float-fullscreen">
|
|
|
|
|
<div class="ai-float-main">
|
|
|
|
|
<div class="ai-float-left">
|
|
|
|
|
<iframe
|
|
|
|
|
ref="iframeRef"
|
|
|
|
|
:src="iframeSrc"
|
|
|
|
|
class="ai-left-iframe"
|
|
|
|
|
@load="onIframeLoad"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-float-right" ref="rightPaneRef">
|
|
|
|
|
<div class="ai-float-header" @mousedown="onDragStart">
|
|
|
|
|
<span>AI智能填报助手</span>
|
|
|
|
|
<el-icon class="ai-float-close" @click="close"><Close /></el-icon>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-float-form-list ai-float-scroll">
|
|
|
|
|
<div class="ai-float-welcome">👋 欢迎使用AI智能填报助手</div>
|
|
|
|
|
<div class="form-list-title">请选择需要填报的表单:</div>
|
|
|
|
|
<!-- 加载状态 -->
|
|
|
|
|
<div v-if="formsLoading" class="loading-container">
|
|
|
|
|
<el-icon class="is-loading"><Loading /></el-icon>
|
|
|
|
|
<span>加载表单中...</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 表单列表 -->
|
|
|
|
|
<div v-else class="form-list">
|
|
|
|
|
<el-tag
|
|
|
|
|
v-for="aiFormSetting in aiFormSettingList"
|
|
|
|
|
:key="aiFormSetting.formSettingId"
|
|
|
|
|
:type="aiFormSetting.formSettingId === selectedFormSettingId ? 'success' : 'info'"
|
|
|
|
|
class="form-tag"
|
|
|
|
|
@click="selectForm(aiFormSetting)"
|
|
|
|
|
effect="dark"
|
|
|
|
|
>
|
|
|
|
|
{{ aiFormSetting.formName }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
<el-button v-if="aiFormSettingList.length > 5" type="text" class="more-btn" @click="showMore = !showMore">更多</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-dialog v-model="showMore" title="选择其他表单" width="400px" @close="moreFilterText = ''">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="moreFilterText"
|
|
|
|
|
placeholder="输入表单名称过滤"
|
|
|
|
|
clearable
|
|
|
|
|
style="margin-bottom: 12px;"
|
|
|
|
|
/>
|
|
|
|
|
<el-scrollbar style="max-height:300px;">
|
|
|
|
|
<el-tag
|
|
|
|
|
v-for="form in filteredForms"
|
|
|
|
|
:key="form.formSettingId"
|
|
|
|
|
:type="form.formSettingId === selectedFormSettingId ? 'success' : 'info'"
|
|
|
|
|
class="form-tag"
|
|
|
|
|
style="margin-bottom: 8px;"
|
|
|
|
|
@click="selectForm(form); showMore = false"
|
|
|
|
|
effect="dark"
|
|
|
|
|
>
|
|
|
|
|
{{ form.formName }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</el-scrollbar>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 聊天区始终渲染,内容为空时不显示消息 -->
|
|
|
|
|
<div class="ai-float-chat-list" ref="chatListRef">
|
|
|
|
|
<div v-for="(msg, idx) in chatList" :key="idx" :class="['ai-float-chat-item', msg.from === 'ai' ? 'ai-chat-ai' : 'ai-chat-user']">
|
|
|
|
|
<template v-if="msg.from === 'ai'">
|
|
|
|
|
<div class="ai-avatar">🤖</div>
|
|
|
|
|
<div class="ai-float-chat-bubble ai-float-chat-bubble-ai">
|
|
|
|
|
<div v-if="msg.type === 'select' && msg.selectLists">
|
|
|
|
|
<div class="ai-select-title" style="font-weight:bold; margin-bottom:8px;">{{ msg.text }}</div>
|
|
|
|
|
<div class="ai-select-lists">
|
|
|
|
|
<div v-for="(list, lidx) in msg.selectLists" :key="lidx" class="ai-select-list-block">
|
|
|
|
|
<div class="ai-select-list-title">{{ list.title }}</div>
|
|
|
|
|
<div class="ai-select-options">
|
|
|
|
|
<el-button v-for="opt in list.options.slice(0,4)" :key="opt" size="small" @click="() => handleSelectOption(opt)" :type="selectedOption === opt ? 'primary' : 'default'">{{ opt }}</el-button>
|
|
|
|
|
<el-button v-if="list.options.length > 4" size="small" type="primary" link @click="() => openSelectDialog(list)">更多</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else v-html="formatMsg(msg.text)"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else>
|
|
|
|
|
<div class="ai-float-chat-bubble-user-wrap">
|
|
|
|
|
<div class="ai-float-chat-bubble" v-html="formatMsg(msg.text)"></div>
|
|
|
|
|
<div class="ai-avatar ai-avatar-user">👤</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-dialog v-model="selectDialog" title="更多选项" width="400px" @close="selectDialogFilterText = ''">
|
|
|
|
|
<div v-if="selectDialogList">
|
|
|
|
|
<div class="ai-select-title">{{ selectDialogList.title }}</div>
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="selectDialogFilterText"
|
|
|
|
|
placeholder="输入关键字过滤"
|
|
|
|
|
clearable
|
|
|
|
|
style="margin-bottom: 12px;"
|
|
|
|
|
/>
|
|
|
|
|
<div class="ai-select-options">
|
|
|
|
|
<el-button v-for="opt in filteredSelectDialogOptions" :key="opt" size="small" @click="() => handleSelectOption(opt)" :type="selectedOption === opt ? 'primary' : 'default'">{{ opt }}</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
<div class="ai-float-input-area">
|
|
|
|
|
<div v-if="selectedFormSettingId" class="ai-float-form-info">
|
|
|
|
|
<div class="form-info-title">当前表单:{{ aiFormSettingList.find(f => f.formSettingId === selectedFormSettingId)?.formName }}</div>
|
|
|
|
|
<div class="form-info-tip">请根据下方示例填写内容,AI将自动为您智能填报。</div>
|
|
|
|
|
<div class="form-info-example">示例:如"申请10个物料A用于生产线1,计划交付日期为2024-07-01"</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-float-input-row">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="inputValue"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="2"
|
|
|
|
|
:autosize="{ minRows: 2, maxRows: 6 }"
|
|
|
|
|
:maxlength="300"
|
|
|
|
|
:disabled="!selectedFormSettingId || aiThinking"
|
|
|
|
|
placeholder="请输入内容(最多300字符)"
|
|
|
|
|
@keydown="onInputKeydown"
|
|
|
|
|
class="ai-float-input"
|
|
|
|
|
show-word-limit
|
|
|
|
|
clearable
|
|
|
|
|
/>
|
|
|
|
|
<el-button
|
|
|
|
|
class="ai-float-send-btn"
|
|
|
|
|
:disabled="!canSend || aiThinking"
|
|
|
|
|
icon="el-icon-s-promotion"
|
|
|
|
|
@click="() => handleSend()"
|
|
|
|
|
circle
|
|
|
|
|
type="primary"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick, shallowRef, watch, getCurrentInstance } from 'vue'
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
import { Close, Loading } from '@element-plus/icons-vue'
|
|
|
|
|
import type { ComponentInternalInstance } from 'vue'
|
|
|
|
|
import router from "@/router"
|
|
|
|
|
|
|
|
|
|
import {getAiFormSettingList} from '@/api/ai/base/aiFormSetting';
|
|
|
|
|
import {AiFormSettingVO} from '@/api/ai/base/aiFormSetting/types';
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
|
|
|
|
|
// 用reactive对象直接定义state
|
|
|
|
|
const state = reactive({
|
|
|
|
|
isShowSearch: false,
|
|
|
|
|
menuQuery: '',
|
|
|
|
|
menuList: []
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const aiFormSettingList = ref<AiFormSettingVO[]>([])
|
|
|
|
|
const selectedFormSettingId = ref<number | null>(null)
|
|
|
|
|
const formsLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
// 搜索弹窗打开
|
|
|
|
|
const openSearch = () => {
|
|
|
|
|
state.menuQuery = '';
|
|
|
|
|
state.isShowSearch = true;
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
// layoutMenuAutocompleteRef.value.focus();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
// 搜索弹窗关闭
|
|
|
|
|
const closeSearch = () => {
|
|
|
|
|
state.isShowSearch = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 暴露变量
|
|
|
|
|
defineExpose({
|
|
|
|
|
openSearch
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 动态生成的表单页面映射
|
|
|
|
|
const formPageMap = ref<Record<number, { loader: () => Promise<any>, props: any }>>({})
|
|
|
|
|
|
|
|
|
|
// 加载表单数据并初始化formPageMap
|
|
|
|
|
const loadForms = async () => {
|
|
|
|
|
try {
|
|
|
|
|
formsLoading.value = true
|
|
|
|
|
const res = await getAiFormSettingList({})
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
aiFormSettingList.value = res.data || []
|
|
|
|
|
// 动态生成formPageMap
|
|
|
|
|
// initFormPageMap()
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载表单数据失败:', error)
|
|
|
|
|
ElMessage.error('加载表单数据失败')
|
|
|
|
|
} finally {
|
|
|
|
|
formsLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 动态生成formPageMap
|
|
|
|
|
const initFormPageMap = () => {
|
|
|
|
|
const newFormPageMap: Record<number, { loader: () => Promise<any>, props: any }> = {}
|
|
|
|
|
|
|
|
|
|
aiFormSettingList.value.forEach(form => {
|
|
|
|
|
// 根据表单的formPath字段动态生成组件路径
|
|
|
|
|
const componentPath = `../../../views/${form.formPath}.vue`
|
|
|
|
|
// 创建动态加载器
|
|
|
|
|
const loader = () => {
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
|
|
// const dd = new URL(`@/views/system/user/index.vue`, import.meta.url).href;
|
|
|
|
|
// alert("--"+componentPath.replace('@/', '../../../'))
|
|
|
|
|
// return import(`${componentPathUrl}`);
|
|
|
|
|
// localStorage.setItem("previousRoute", "ai");
|
|
|
|
|
return import(/* @vite-ignore */ componentPath)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`加载组件失败: ${componentPath}`, error)
|
|
|
|
|
// 如果加载失败,返回默认组件
|
|
|
|
|
// return import('@/views/ai/skill/aiForm/index.vue')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 根据表单类型设置不同的props
|
|
|
|
|
const props = getFormProps(form)
|
|
|
|
|
|
|
|
|
|
newFormPageMap[form.formSettingId] = { loader, props }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
formPageMap.value = newFormPageMap
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectForm(aiFormSetting: AiFormSettingVO) {
|
|
|
|
|
selectedFormSettingId.value = aiFormSetting.formSettingId
|
|
|
|
|
console.log('选择表单:', aiFormSetting.formPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 根据表单信息生成props
|
|
|
|
|
const getFormProps = (form: AiFormSettingVO) => {
|
|
|
|
|
const baseProps = {
|
|
|
|
|
formSettingId: form.formSettingId,
|
|
|
|
|
formName: form.formName,
|
|
|
|
|
formType: form.formType,
|
|
|
|
|
userInput: ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 根据表单类型添加特定props
|
|
|
|
|
switch (form.formType) {
|
|
|
|
|
case 'system':
|
|
|
|
|
return { ...baseProps, pageNum: 1, pageSize: 10 }
|
|
|
|
|
case 'ai':
|
|
|
|
|
return { ...baseProps, aiType: 'form', mode: 'edit' }
|
|
|
|
|
default:
|
|
|
|
|
return baseProps
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const showMore = ref(false)
|
|
|
|
|
const inputValue = ref('')
|
|
|
|
|
const canSend = computed(() => selectedFormSettingId.value && inputValue.value.trim().length > 0)
|
|
|
|
|
|
|
|
|
|
// 新增:更多弹窗过滤输入
|
|
|
|
|
const moreFilterText = ref('')
|
|
|
|
|
const filteredForms = computed(() =>
|
|
|
|
|
aiFormSettingList.value.filter(form => form.formName.includes(moreFilterText.value))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ChatSelectList = { title: string, options: string[] }
|
|
|
|
|
type ChatMsg = { text: string, from: 'user' | 'ai', type?: 'text' | 'select', selectLists?: ChatSelectList[] }
|
|
|
|
|
const chatList = ref<ChatMsg[]>([])
|
|
|
|
|
const selectDialog = ref(false)
|
|
|
|
|
const selectDialogList = ref<ChatSelectList|null>(null)
|
|
|
|
|
const aiThinking = ref(false)
|
|
|
|
|
const chatListRef = ref<HTMLElement | null>(null)
|
|
|
|
|
const rightPaneRef = ref<HTMLElement | null>(null)
|
|
|
|
|
const selectedOption = ref('')
|
|
|
|
|
const iframeRef = ref<HTMLIFrameElement | null>(null)
|
|
|
|
|
const iframeSrc = ref<string>('')
|
|
|
|
|
|
|
|
|
|
// 新增:AI更多弹窗过滤输入
|
|
|
|
|
const selectDialogFilterText = ref('')
|
|
|
|
|
const filteredSelectDialogOptions = computed(() =>
|
|
|
|
|
selectDialogList.value
|
|
|
|
|
? selectDialogList.value.options.filter(opt => opt.includes(selectDialogFilterText.value))
|
|
|
|
|
: []
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 悬浮窗拖拽
|
|
|
|
|
const floatStyle = reactive({
|
|
|
|
|
left: '',
|
|
|
|
|
top: '0px',
|
|
|
|
|
width: '440px',
|
|
|
|
|
height: '100vh',
|
|
|
|
|
zIndex: 9999,
|
|
|
|
|
position: 'fixed' as const,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 默认显示在最右侧
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
// 放宽对话框焦点陷阱以允许右侧输入
|
|
|
|
|
document.addEventListener('focusin', focusTrapBypass, true);
|
|
|
|
|
floatStyle.left = (window.innerWidth - 440) + 'px';
|
|
|
|
|
floatStyle.top = '0px';
|
|
|
|
|
// 加载表单数据
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let drag = false
|
|
|
|
|
let offsetX = 0
|
|
|
|
|
let offsetY = 0
|
|
|
|
|
function onDragStart(e: MouseEvent) {
|
|
|
|
|
drag = true
|
|
|
|
|
offsetX = e.clientX - parseInt(floatStyle.left)
|
|
|
|
|
offsetY = e.clientY - parseInt(floatStyle.top)
|
|
|
|
|
document.addEventListener('mousemove', onDragging)
|
|
|
|
|
document.addEventListener('mouseup', onDragEnd)
|
|
|
|
|
}
|
|
|
|
|
function onDragging(e: MouseEvent) {
|
|
|
|
|
if (!drag) return
|
|
|
|
|
let newLeft = e.clientX - offsetX
|
|
|
|
|
let newTop = e.clientY - offsetY
|
|
|
|
|
// 限制在屏幕内
|
|
|
|
|
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - 440))
|
|
|
|
|
newTop = Math.max(0, Math.min(newTop, window.innerHeight - 100))
|
|
|
|
|
floatStyle.left = newLeft + 'px'
|
|
|
|
|
floatStyle.top = newTop + 'px'
|
|
|
|
|
}
|
|
|
|
|
function onDragEnd() {
|
|
|
|
|
drag = false
|
|
|
|
|
document.removeEventListener('mousemove', onDragging)
|
|
|
|
|
document.removeEventListener('mouseup', onDragEnd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function focusTrapBypass(e: FocusEvent) {
|
|
|
|
|
const overlay = document.querySelector('.el-overlay') as HTMLElement | null
|
|
|
|
|
const dialog = overlay?.querySelector('.el-dialog') as HTMLElement | null
|
|
|
|
|
if (!overlay || !dialog) return
|
|
|
|
|
const rightPane = rightPaneRef.value
|
|
|
|
|
if (!rightPane) return
|
|
|
|
|
// 如果焦点转移到右侧面板内,允许通过
|
|
|
|
|
if (rightPane.contains(e.target as Node)) {
|
|
|
|
|
// 取消Element Plus 可能的焦点回滚
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
document.removeEventListener('mousemove', onDragging)
|
|
|
|
|
document.removeEventListener('mouseup', onDragEnd)
|
|
|
|
|
document.removeEventListener('focusin', focusTrapBypass, true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function scrollToBottom() {
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
if (chatListRef.value) {
|
|
|
|
|
chatListRef.value.scrollTop = chatListRef.value.scrollHeight
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onIframeLoad() {
|
|
|
|
|
// 载入完成后同步一次参数
|
|
|
|
|
const form = aiFormSettingList.value.find(f => f.formSettingId === selectedFormSettingId.value)
|
|
|
|
|
if (form) {
|
|
|
|
|
alert(233333)
|
|
|
|
|
console.log('iframe 加载完成,开始传参');
|
|
|
|
|
sendToIframe({ type: 'init', payload:` getFormProps(form)` })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sendToIframe(message: any) {
|
|
|
|
|
const iframeEl = iframeRef.value
|
|
|
|
|
try {
|
|
|
|
|
iframeEl?.contentWindow?.postMessage(message, '*')
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 忽略跨域异常
|
|
|
|
|
console.error('传参失败:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSend(selectedText?: string) {
|
|
|
|
|
if ((!canSend.value || aiThinking.value) && !selectedText) return
|
|
|
|
|
const userMsg = selectedText || inputValue.value
|
|
|
|
|
chatList.value.push({ text: userMsg, from: 'user', type: 'text' })
|
|
|
|
|
// 同步用户输入到 iframe 页面
|
|
|
|
|
sendToIframe({ type: 'userInput', payload: userMsg })
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
aiThinking.value = true
|
|
|
|
|
inputValue.value = 'AI思考中....'
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
inputValue.value = ''
|
|
|
|
|
aiThinking.value = false
|
|
|
|
|
// 固定AI回复+多个选择列表
|
|
|
|
|
chatList.value.push({
|
|
|
|
|
text: '操作成功,请选择相关信息:',
|
|
|
|
|
from: 'ai',
|
|
|
|
|
type: 'select',
|
|
|
|
|
selectLists: [
|
|
|
|
|
{ title: '物料信息', options: ['物料A', '物料B', '物料C', '物料D', '物料E', '物料F'] },
|
|
|
|
|
{ title: 'BOM信息', options: ['BOM-001', 'BOM-002', 'BOM-003', 'BOM-004', 'BOM-005'] }
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
}, 2000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保证所有方法直接在<script setup>中定义
|
|
|
|
|
function handleSelectOption(opt: string) {
|
|
|
|
|
selectedOption.value = opt
|
|
|
|
|
}
|
|
|
|
|
function openSelectDialog(list: ChatSelectList) {
|
|
|
|
|
selectDialogList.value = list
|
|
|
|
|
selectDialog.value = true
|
|
|
|
|
}
|
|
|
|
|
function formatMsg(text: string) {
|
|
|
|
|
// 转换换行符为<br>,并做简单转义防XSS
|
|
|
|
|
return text
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
.replace(/\n/g, '<br>')
|
|
|
|
|
}
|
|
|
|
|
function onInputKeydown(e: KeyboardEvent) {
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleSend();
|
|
|
|
|
}
|
|
|
|
|
// shift+enter 默认换行,无需处理
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function close() {
|
|
|
|
|
state.isShowSearch = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const currentComponent = shallowRef(null)
|
|
|
|
|
const currentProps = ref({})
|
|
|
|
|
|
|
|
|
|
function buildIframeSrc(form: AiFormSettingVO) {
|
|
|
|
|
try {
|
|
|
|
|
const resolved = router.resolve({ path: `/${form.formPath}` })
|
|
|
|
|
resolved.href += "?ai=true"
|
|
|
|
|
return resolved.href
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return `/${form.formPath}`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(selectedFormSettingId, async (formSettingId) => {
|
|
|
|
|
const form = aiFormSettingList.value.find(f => f.formSettingId === formSettingId)
|
|
|
|
|
if (form) {
|
|
|
|
|
iframeSrc.value = buildIframeSrc(form)
|
|
|
|
|
// alert(iframeSrc.value)
|
|
|
|
|
// 同时准备将初始参数 postMessage 给 iframe
|
|
|
|
|
// 使用 nextTick 确保 iframe 已更新
|
|
|
|
|
// nextTick(() => {
|
|
|
|
|
// sendToIframe({ type: 'init', payload: 'getFormProps(form)' })
|
|
|
|
|
// alert(12)
|
|
|
|
|
// })
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch(state, (newVal) => {
|
|
|
|
|
if (newVal.isShowSearch) {
|
|
|
|
|
loadForms()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.ai-float-fullscreen {
|
|
|
|
|
position: fixed;
|
|
|
|
|
left: 0;
|
|
|
|
|
top: 0;
|
|
|
|
|
width: 100vw;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
z-index: 2005;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
/* 让全局弹窗显示在浮窗之上 */
|
|
|
|
|
:deep(.el-overlay) {
|
|
|
|
|
z-index: 3000 !important;
|
|
|
|
|
}
|
|
|
|
|
:deep(.el-overlay .el-dialog) {
|
|
|
|
|
z-index: 3001 !important;
|
|
|
|
|
}
|
|
|
|
|
/* 允许遮罩不拦截右侧输入:只有遮罩变为不拦截,弹窗可交互 */
|
|
|
|
|
:deep(.el-overlay) {
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
:deep(.el-overlay .el-dialog),
|
|
|
|
|
:deep(.el-overlay .el-message-box),
|
|
|
|
|
:deep(.el-overlay .el-drawer),
|
|
|
|
|
:deep(.el-overlay .el-popup) {
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-window {
|
|
|
|
|
box-shadow: 0 4px 24px rgba(0,0,0,0.18);
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 0 0 16px 0;
|
|
|
|
|
min-height: 220px;
|
|
|
|
|
user-select: none;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
width: 90vw;
|
|
|
|
|
right: 0;
|
|
|
|
|
left: auto;
|
|
|
|
|
top: 0;
|
|
|
|
|
position: fixed;
|
|
|
|
|
z-index: 2005;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-main {
|
|
|
|
|
display: flex;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-left {
|
|
|
|
|
flex: 7 7 0;
|
|
|
|
|
min-width: 400px;
|
|
|
|
|
background: #f7f8fa;
|
|
|
|
|
border-right: 1px solid #e0e7ef;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
.ai-left-iframe {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border: 0;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-right {
|
|
|
|
|
flex: 3 3 0;
|
|
|
|
|
min-width: 260px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
padding: 16px 24px 8px 24px;
|
|
|
|
|
cursor: move;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-welcome {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
color: #409eff;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-close {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
color: #888;
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-close:hover {
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-form-list {
|
|
|
|
|
padding: 16px 24px 0 24px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
max-height: 220px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
.form-list-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
.form-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.form-tag {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
.more-btn {
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
}
|
|
|
|
|
.loading-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
color: #666;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-input-area {
|
|
|
|
|
padding: 6px 24px 0 24px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
height: 140px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-form-info {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
background: #f6f8fa;
|
|
|
|
|
border-radius: 1px;
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
}
|
|
|
|
|
.form-info-title {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #333;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
.form-info-tip {
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
.form-info-example {
|
|
|
|
|
color: #999;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-input-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-input {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-send-btn {
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-chat-list {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
margin: 0 24px;
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
margin-bottom:20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
max-height: calc(100vh - 170px - 140px - 32px);
|
|
|
|
|
}
|
|
|
|
|
.ai-float-chat-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
}
|
|
|
|
|
.ai-chat-ai {
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
}
|
|
|
|
|
.ai-avatar {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
background: linear-gradient(135deg, #e0e7ef 0%, #f6f8fa 100%);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(64,158,255,0.08);
|
|
|
|
|
}
|
|
|
|
|
.ai-float-chat-bubble {
|
|
|
|
|
background: linear-gradient(135deg, #409eff 0%, #6ec1ff 100%);
|
|
|
|
|
color: #fff;
|
|
|
|
|
border-radius: 20px 20px 4px 24px;
|
|
|
|
|
padding: 12px 22px;
|
|
|
|
|
max-width: 80%;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
box-shadow: 0 4px 16px rgba(64,158,255,0.13);
|
|
|
|
|
line-height: 1.85;
|
|
|
|
|
min-height: 36px;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
text-shadow: 0 1px 2px rgba(0,0,0,0.08);
|
|
|
|
|
}
|
|
|
|
|
.ai-float-chat-bubble-ai {
|
|
|
|
|
background: linear-gradient(135deg, #f4f4f4 0%, #e9f0fb 100%);
|
|
|
|
|
color: #333;
|
|
|
|
|
border-radius: 16px 16px 16px 6px;
|
|
|
|
|
box-shadow: 0 2px 12px rgba(64,158,255,0.10);
|
|
|
|
|
border: 1px solid #e0e7ef;
|
|
|
|
|
}
|
|
|
|
|
.ai-select-lists {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
}
|
|
|
|
|
.ai-select-list-block {
|
|
|
|
|
width: 100%;
|
|
|
|
|
text-align: left;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
.ai-select-list-title {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #333;
|
|
|
|
|
text-align: left;
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
.ai-select-options {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
text-align: left;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
.ai-select-options .el-button {
|
|
|
|
|
width: 100%;
|
|
|
|
|
text-align: left;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding-left: 0 !important;
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
}
|
|
|
|
|
.el-dialog__body .ai-select-title {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
.el-dialog__body .ai-select-options {
|
|
|
|
|
width: 100%;
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.ai-float-chat-bubble-user-wrap {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.ai-avatar-user {
|
|
|
|
|
background: linear-gradient(135deg, #e0e7ef 0%, #d0eaff 100%);
|
|
|
|
|
color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|