diff --git a/src/App.vue b/src/App.vue index 88135c2..cb29009 100644 --- a/src/App.vue +++ b/src/App.vue @@ -44,6 +44,8 @@ const openAiAssistantMenu = () => { right: 10px; width: 24px; height: 24px; + cursor: pointer; + z-index:7000; } .el-message { diff --git a/src/api/ai/base/aiFormSetting/types.ts b/src/api/ai/base/aiFormSetting/types.ts index 0994366..f3dfa14 100644 --- a/src/api/ai/base/aiFormSetting/types.ts +++ b/src/api/ai/base/aiFormSetting/types.ts @@ -93,6 +93,11 @@ export interface AiFormSettingForm extends BaseEntity { */ formPath?: string; + /** + * 表单加载类型(1表单(直接加载表单),2菜单(打开菜单弹窗)) + */ + formLoadType?: string; + /** * 弹窗可见变量名称 */ diff --git a/src/api/monitorData.ts b/src/api/monitorData.ts new file mode 100644 index 0000000..73f4888 --- /dev/null +++ b/src/api/monitorData.ts @@ -0,0 +1,33 @@ +import { defineStore } from 'pinia' + +export const useSharedDataStore = defineStore('sharedData', { + state: () => ({ + // 定义需要共享的数据 + dynamicValue: null, + // 可以定义多个共享状态 + otherData: '', + // 可以添加时间戳以便监听更新 + lastUpdated: null + }), + + actions: { + // 更新数据的方法 + updateDynamicValue(value) { + this.dynamicValue = value + this.lastUpdated = new Date().toISOString() + }, + + // 清空数据 + clearData() { + this.dynamicValue = null + this.otherData = '' + } + }, + + getters: { + // 可以添加计算属性 + formattedValue() { + return this.dynamicValue ? JSON.stringify(this.dynamicValue) : '无数据' + } + } +}) diff --git a/src/api/objectUtil.ts b/src/api/objectUtil.ts new file mode 100644 index 0000000..34a41d2 --- /dev/null +++ b/src/api/objectUtil.ts @@ -0,0 +1,115 @@ +/** + * 过滤对象中的空值属性 + * @param sourceObject 源对象 + * @returns 只包含非空值属性的新对象 + * + * @description 该函数会遍历源对象的所有属性,只保留非空值的属性。 + * 空值定义为:null、undefined、空字符串('') + * 支持嵌套对象的递归处理 + */ +export function filterEmptyValues(sourceObject: T): Partial { + const result: Partial = {}; + + // 遍历源对象的所有属性 + Object.keys(sourceObject).forEach(key => { + const value = (sourceObject as any)[key]; + + // 检查值是否不为空 + if (value !== null && value !== undefined && value !== '') { + // 如果是对象且不是数组,递归处理 + if (typeof value === 'object' && !Array.isArray(value)) { + // 递归过滤嵌套对象 + const filteredNestedObject = filterEmptyValues(value); + // 只有当过滤后的嵌套对象有属性时才添加 + if (Object.keys(filteredNestedObject).length > 0) { + (result as any)[key] = filteredNestedObject; + } + } else { + // 对于非对象类型或数组,直接添加 + (result as any)[key] = value; + } + } + }); + + return result; +} + +/** + * 增强版过滤对象空值属性 + * @param sourceObject 源对象 + * @param options 过滤选项 + * @returns 过滤后的新对象 + * + * @description 提供更灵活的空值过滤配置,可自定义空值判断逻辑 + */ +export function filterEmptyValuesAdvanced( + sourceObject: T, + options: { + // 是否将空数组视为空值 + treatEmptyArrayAsEmpty?: boolean; + // 是否将空对象视为空值 + treatEmptyObjectAsEmpty?: boolean; + // 是否将0视为空值 + treatZeroAsEmpty?: boolean; + // 是否将false视为空值 + treatFalseAsEmpty?: boolean; + } = {} +): Partial { + const { + treatEmptyArrayAsEmpty = false, + treatEmptyObjectAsEmpty = false, + treatZeroAsEmpty = false, + treatFalseAsEmpty = false + } = options; + + const result: Partial = {}; + + Object.keys(sourceObject).forEach(key => { + const value = (sourceObject as any)[key]; + + // 基本空值检查 + let isEmpty = value === null || value === undefined || value === ''; + + // 根据选项进行额外的空值检查 + if (!isEmpty && treatZeroAsEmpty && value === 0) { + isEmpty = true; + } + + if (!isEmpty && treatFalseAsEmpty && value === false) { + isEmpty = true; + } + + if (!isEmpty && treatEmptyArrayAsEmpty && Array.isArray(value) && value.length === 0) { + isEmpty = true; + } + + if (!isEmpty && treatEmptyObjectAsEmpty && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) { + isEmpty = true; + } + + // 如果值不为空,处理并添加到结果中 + if (!isEmpty) { + if (typeof value === 'object' && !Array.isArray(value)) { + const filteredNestedObject = filterEmptyValuesAdvanced(value, options); + if (Object.keys(filteredNestedObject).length > 0) { + (result as any)[key] = filteredNestedObject; + } + } else { + (result as any)[key] = value; + } + } + }); + + return result; +} + +/** + * 从源对象复制非空值到目标对象 + * @param source 源对象 + * @param target 目标对象 + * @returns 更新后的目标对象 + */ +export function copyNonEmptyValues(source: T, target: U): U & Partial { + const filteredValues = filterEmptyValues(source); + return Object.assign(target, filteredValues) as U & Partial; +} diff --git a/src/layout/components/TopBar/aiAssistant.vue b/src/layout/components/TopBar/aiAssistant.vue index 9734170..1f6e807 100644 --- a/src/layout/components/TopBar/aiAssistant.vue +++ b/src/layout/components/TopBar/aiAssistant.vue @@ -1,15 +1,32 @@ @@ -164,15 +192,25 @@ import {getAiFormSettingList, getAiFormSettingDetailList} from '@/api/ai/base/ai import {AiFormSettingVO, AiFormSettingDetailVO} from '@/api/ai/base/aiFormSetting/types'; import {aiFillForm, recognizeSpeechByUrl, uploadFile} from '@/api/ai/skill/aiAssistant'; import router from "@/router" +import {useSharedDataStore} from '@/api/monitorData' + +const sharedStore = useSharedDataStore() const {proxy} = getCurrentInstance() as ComponentInternalInstance const aiFormSettingList = ref([]) const aiFormSettingDetailListMap = ref>({}); const selectedFormSettingId = ref(null) +const selectedForm = ref(); const formsLoading = ref(false) const isShowLeft = ref(false); const aiLoading = ref(false); +const currentSize = ref("menu"); + +const currentSizeClass = computed(() => + `size-${currentSize.value}` +); + // 用reactive对象直接定义state const state = reactive({ @@ -272,9 +310,12 @@ const getFormProps = (form: AiFormSettingVO) => { } const selectForm = async (aiFormSetting: AiFormSettingVO) => { + if( selectedFormSettingId.value === aiFormSetting.formSettingId) return; selectedFormSettingId.value = aiFormSetting.formSettingId + selectedForm.value = aiFormSetting; aiLoading.value = true try { + resetAiAssistant(); if (!aiFormSettingDetailListMap.value[aiFormSetting.formSettingId]) { const res = await getAiFormSettingDetailList( {formSettingId: aiFormSetting.formSettingId, settingFlag: '1'}) @@ -288,6 +329,10 @@ const selectForm = async (aiFormSetting: AiFormSettingVO) => { console.log(aiFormSettingDetailListMap.value[aiFormSetting.formSettingId]) } +const resetAiAssistant = () =>{ + chatList.value = []; +} + // function selectForm(aiFormSetting: AiFormSettingVO) { // selectedFormSettingId.value = aiFormSetting.formSettingId // if(!aiFormSettingDetailListMap[aiFormSetting.formSettingId]){ @@ -306,11 +351,14 @@ watch(selectedFormSettingId, async (formSettingId) => { try { if (formPageMap.value[formSettingId].props && formPageMap.value[formSettingId].props.dialogVisibleVariable && formPageMap.value[formSettingId].props.dialogVisibleVariable !== '') { isShowLeft.value = false - router.push("/" + formPageMap.value[formSettingId].props.formPath) + currentSize.value = "menu"; + router.push({path: "/" + formPageMap.value[formSettingId].props.formPath, query: {from: "ai"}}) + // proxy.$store.commit('aiFormSettingId', formSettingId) // proxy.$store.commit('aiFormSetting', formPageMap.value[formSettingId]) } else { isShowLeft.value = true + currentSize.value = "form"; const mod = await formPageMap.value[formSettingId].loader() console.log(formPageMap.value[formSettingId].props) currentComponent.value = mod.default @@ -330,6 +378,7 @@ watch(selectedFormSettingId, async (formSettingId) => { } }) + // 暴露变量 defineExpose({ openSearch @@ -342,7 +391,50 @@ watch(state, (newVal) => { } }); +const selectLists = ref([]); +// 存储fieldType为1的元素及其位置信息,显示使用 +const type1Elements = ref([]); +// 存储fieldType为2的元素及其位置信息,赋值使用 +const type2Elements = ref([]); + +// 存储选中后对应的fieldType为2的元素 +const selectedType2Elements = ref([]); + +// 处理数据,找出所有fieldType为1的元素 +const processData = (data) => { + type1Elements.value = []; + type2Elements.value = []; + let title = ""; + data.forEach((subArray, subArrayIndex) => { + if (Array.isArray(subArray)) { + subArray.forEach(item => { + if (item.fieldType === '3' || item.fieldType === 3) {//请选择关联表的表名title信息 + if (title == '') title = item.fieldValue; + } + + if (item.fieldType === '1' || item.fieldType === 1) {//赋值 + type1Elements.value.push({ + ...item, + subArrayIndex + }); + } + + if (item.fieldType === '2' || item.fieldType === 2) {//显示的名称 + type2Elements.value.push({ + ...item, + subArrayIndex + }); + } + }); + + } + }); + selectLists.value.push({title: title, options: type2Elements.value, formOptions: type1Elements.value}); +}; + + +const sendForm = ref({}); const handleSend = async (selectedText?: string) => { if ((!canSend.value || aiThinking.value) && !selectedText) return const userMsg = selectedText || inputValue.value @@ -361,38 +453,144 @@ const handleSend = async (selectedText?: string) => { platformId: 1 } - // currentProps.value = {completeAmount:10,productionTime:16.6}; + // chatList.value.push({ + // text: '操作成功,请选择相关信息:', + // from: 'ai', + // type: 'select', + // selectLists: selectLists.value + // }) + // + // sharedStore.updateDynamicValue({ + // message: {deptId:100}, + // timestamp: new Date().toLocaleString() + // }) // return; const response = await aiFillForm(params) - currentProps.value = response; + sendForm.value = response; inputValue.value = '' aiThinking.value = false + traverseJSON(); + + if (selectedForm.value && selectedForm.value.dialogVisibleVariable && selectedForm.value.dialogVisibleVariable !== '') { + //打开菜单时赋值使用 + sharedStore.updateDynamicValue({ + message: sendForm.value, + timestamp: new Date().toLocaleString() + }) + } else { + //直接打开表单赋值使用 + currentProps.value = sendForm.value; + } + + // 固定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']} - ] - }) + // 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']} + // ] + // }) + + + if (selectLists.value.length > 0) { + chatList.value.push({ + text: '操作成功,请选择相关信息:', + from: 'ai', + type: 'select', + selectLists: selectLists.value + }) + } else { + chatList.value.push({ + text: '操作成功', + from: 'ai', + }) + } + + scrollToBottom() console.log(1) console.log(response) - console.log(2) } catch (error) { proxy?.$modal.msgError(error); console.log("Error:" + error) - + chatList.value.push({ + text: '请重试:'+error, + from: 'ai', + }) } finally { + aiThinking.value = false + inputValue.value = '' aiLoading.value = false } } + +// 递归遍历JSON对象的函数 +function traverseJSON() { + + + // 判断是否为数组 + if (Array.isArray(sendForm.value)) { + console.log('这是一个数组,包含 ' + sendForm.value.length + ' 个元素:'); + // 遍历数组中的每个元素 + // obj.forEach((item, index) => { + // console.log(' '.repeat(indent) + '索引 ' + index + ':'); + // }); + } else { + // 遍历对象的所有key值 + for (const key in sendForm.value) { + // 确保key是对象自身的属性(非继承) + if (sendForm.value.hasOwnProperty(key)) { + const value = sendForm.value[key]; + + // 判断value是否为数组 + if (Array.isArray(value)) { + // selectLists: [ + // {title: '物料信息', options: ['物料A', '物料B', '物料C', '物料D', '物料E', '物料F']}, + // {title: 'BOM信息', options: ['BOM-001', 'BOM-002', 'BOM-003', 'BOM-004', 'BOM-005']} + // ] + sendForm.value[key] = ''; + if (value.length == 1) { + + const firstValue = value[0]; + firstValue.forEach(item => { + if(item.fieldType ==='1' || item.fieldType===1){ + sendForm.value[key] = item.fieldValue + } + }); + + // let selectList = {title: value[0].relateTableDesc, options: value} + // selectLists.value.push(selectList); + }else if (value.length > 1) { + processData(value) + } + + // 遍历数组中的每个元素 + // value.forEach((item, index) => { + // console.log(' '.repeat(indent + 1) + '数组元素 ' + index + ':'); + // }); + } + // 如果value是对象但不是数组,递归遍历 + // else if (value !== null && typeof value === 'object') { + // console.log(' '.repeat(indent) + '值是一个对象:'); + // traverseJSON(value, indent + 1); + // } + // 基本类型值直接输出 + // else { + // console.log(' '.repeat(indent) + '值:', value); + // } + } + } + } +} + + // 录音相关对象 const isRecording = ref(false); let mediaRecorder = null @@ -418,8 +616,6 @@ const toggleRecording = () => { // 开始录音 async function startRecording() { - alert('您的浏览器不支持录音功能1') - proxy.$modal.msgError('您的浏览器不支持录音功能') try { // 检查浏览器支持 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { @@ -529,8 +725,8 @@ function stopRecording() { console.error('处理录音文件失败:', error) proxy.$modal.msgError('处理录音文件失败,请重试', error) // addLog(`处理录音文件失败: ${error.message}`) - }finally{ - + } finally { + audioAsrLoading.value = false; } } } @@ -699,13 +895,11 @@ async function uploadAudioFile(blob, fileName) { if (response.code === 200) { // addLog('音频上传成功,正在识别') - // alert(response.data.url) const response1 = await recognizeSpeechByUrl(response.data.url); if (response1.code === 200) { console.log("--" + response1); inputValue.value += response1.data.text // recognitionResult.value = response1.data.text || '识别成功,但未返回文本结果' - // alert(recognitionResult.value) // addLog('识别成功,结果为: ' + recognitionResult.value) } } else { @@ -767,6 +961,7 @@ type ChatMsg = { text: string, from: 'user' | 'ai', type?: 'text' | 'select', se const chatList = ref([]) const selectDialog = ref(false) const selectDialogList = ref(null) +const selectDialogListIndex = ref(); const aiThinking = ref(false) const chatListRef = ref(null) const selectedOption = ref('') @@ -776,61 +971,48 @@ const iframeRef = ref(null) const selectDialogFilterText = ref('') const filteredSelectDialogOptions = computed(() => selectDialogList.value - ? selectDialogList.value.options.filter(opt => opt.includes(selectDialogFilterText.value)) + ? selectDialogList.value.options.filter(opt => { + // 检查opt是否为对象,如果是则访问其字符串属性 + // 这里假设对象有label或value属性,根据实际情况修改 + const fieldValue = typeof opt === 'string' ? opt : + opt.fieldValue + return fieldValue.toLowerCase().includes(selectDialogFilterText.value.toLowerCase()); + }) : [] ) -// 悬浮窗拖拽 -const floatStyle = reactive({ - left: '', - top: '0px', - width: '440px', - height: '100vh', - zIndex: 9999, - position: 'fixed' as const, -}) + // 默认显示在最右侧 onMounted(() => { - floatStyle.left = (window.innerWidth - 440) + 'px'; - floatStyle.top = '0px'; + // 加载表单数据 // loadForms() - + // + // const testData = [[{"fieldKey":"relateTableDesc","fieldValue":"部门1","fieldType":"3"}, + // {"fieldKey":"deptName","fieldValue":"测试部门1","fieldType":"2"}, + // {"fieldKey":"deptId","fieldValue":105,"fieldType":"1"}], + // [{"fieldKey":"relateTableDesc","fieldValue":null,"fieldType":"3"}, + // {"fieldKey":"deptName","fieldValue":"测试部门2","fieldType":"2"}, + // {"fieldKey":"deptId","fieldValue":"1952572898270183426","fieldType":"1"}] + // ] + // + // + // processData(testData); + // + // const testData1 = [ + // [{"fieldKey":"relateTableDesc","fieldValue":"部门2","fieldType":"3"}, + // {"fieldKey":"deptName","fieldValue":"测试部门11","fieldType":"2"}, + // {"fieldKey":"deptId","fieldValue":1051,"fieldType":"1"}], + // [{"fieldKey":"relateTableDesc","fieldValue":null,"fieldType":"3"}, + // {"fieldKey":"deptName","fieldValue":"测试部门22","fieldType":"2"}, + // {"fieldKey":"deptId","fieldValue":"19525728982701834261","fieldType":"1"}] + // ] + // processData(testData1); }) -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) -} - onBeforeUnmount(() => { - document.removeEventListener('mousemove', onDragging) - document.removeEventListener('mouseup', onDragEnd) + }) // 组件卸载时清理资源 @@ -865,22 +1047,40 @@ function scrollToBottom() { // 保证所有方法直接在 diff --git a/src/views/ai/base/aiFormSetting/index.vue b/src/views/ai/base/aiFormSetting/index.vue index 2a1f58e..3e7ed82 100644 --- a/src/views/ai/base/aiFormSetting/index.vue +++ b/src/views/ai/base/aiFormSetting/index.vue @@ -52,7 +52,11 @@ - + + + + + + + + + + + + + + + + + +