import { ssIcon, commonIcon } from "./icon-config.js"; import * as IndexComponents from "./ss-index-components.js"; import * as EchartComponents from "./ss-echarts-compnents.js"; import { isNum, toStyleStr } from "./tools.js"; import { EVEN_VAR } from "./EventBus.js"; // import * as elements from "../lib/element-plus.js"; (function () { const { createApp, ref, reactive, watch, onMounted, onBeforeUnmount, h, computed, resolveComponent, watchEffect, nextTick, onVnodeMounted, Teleport, inject, provide, } = Vue; // 弹窗默认遮罩z-index let currentZIndex = 100; // 目前已存在的弹窗 const topWindow = window.top; topWindow.dialogInstances = topWindow.dialogInstances || []; // 新建弹窗 function createSsDialogInstance(setting, callbackEvent) { currentZIndex += 10; // 动态提升 z-index const container = document.createElement("div"); document.body.appendChild(container); const app = Vue.createApp({ render() { return h(SsDialog, { ...setting, zIndex: currentZIndex, onClose() { document.body.removeChild(container); // 仅移除弹窗容器 const index = topWindow.dialogInstances.indexOf(app); if (index > -1) { topWindow.dialogInstances.splice(index, 1); // 移除实例 } // 关闭后的回调 if (callbackEvent && typeof callbackEvent === "function") { callbackEvent(); } app.unmount(); // 仅卸载弹窗实例 if (container.parentNode) { container.parentNode.removeChild(container); // 确保移除容器 } }, }); }, }); topWindow.dialogInstances.push({ app, callbackEvent, container }); app.component("ss-mark", SsMark); // 注册 ss-mark 组件 app.component("ss-icon", SsIcon); app.component("ss-full-style-header", SsFullStyleHeader); // 注册 ss-full-style-header 组件 app.mount(container); } // ss-breadcrumb 一级菜单页面面包屑 const SsBreadcrumb = { name: "SsBreadcrumb", props: { level: { type: Object, default: null, }, }, setup(props) { const currentMenu = ref(null); const folderPath = ref([]); const eventBus = window.parent.sharedEventBus; // 监听页面变化 onMounted(() => { // 获取初始页面 currentMenu.value = eventBus.getState(EVEN_VAR.currentPage); folderPath.value = eventBus.getState("folderPath") || []; // 订阅页面变化 eventBus.subscribe(EVEN_VAR.currentPage, (page) => { currentMenu.value = page; }); eventBus.subscribe("folderPath", (path) => { folderPath.value = path || []; }); }); // 修改点击处理函数 const handlePathClick = (index) => { if (props.level?.onBack) { // 截取到点击的位置,后面的路径会被销毁 const newPath = folderPath.value.slice(0, index + 1); eventBus.publish("folderPath", newPath); // 返回到对应层级 const targetFolder = newPath[newPath.length - 1]?.folder || null; props.level.onBack(targetFolder); } }; const SsCommonIcon = resolveComponent("SsCommonIcon"); return () => h("div", { class: "bread-crumb" }, [ currentMenu.value && h( "div", { onClick: () => { if (props.level?.onBack) { eventBus.publish("folderPath", []); props.level.onBack(null); // 返回到根目录 } else { eventBus.publish(EVEN_VAR.currentPage, currentMenu.value); } }, }, currentMenu.value.label || currentMenu.value.name ), ...(folderPath.value || []) .map((folder, index) => [ h(SsCommonIcon, { class: "common-icon-arrow-right" }), h( "div", { class: "bread-crumb-item", onClick: () => handlePathClick(index), style: { cursor: "pointer" }, }, folder.title ), ]) .flat(), ]); }, }; // ss-input form表单的输入 const SsInput = { name: "SsInput", inheritAttrs: false, // 不直接继承属性到组件根元素 props: { name: { type: String, required: true, default: "", }, // 接收 v-model 绑定的值 errTip: { type: String, }, required: { type: Boolean, default: false, }, placeholder: { type: String, default: "请输入", }, defaultValue: [String, Number], modelValue: [String, Number], // 新增:附件配置 fj: { type: Object, default: null, }, // 新增:param 配置(用于附件功能) param: { type: Object, default: null, }, // 新增:高度配置 height: { type: String, default: "", }, // 新增:是否允许回车换行,默认false禁止换行 by xu 20251212 multiline: { type: Boolean, default: false, }, }, emits: ["update:modelValue", "input", "blur", "change"], // 允许更新 v-model 绑定的值 setup(props, { emit }) { const errMsg = ref(""); const inputRef = ref(null); const textareaRef = ref(null); const inputValue = ref(props.modelValue || props.defaultValue || ""); const contentFloatingDiv = ref(false); // 控制浮动 DIV 的显示 const floatingDivPosition = ref("bottom"); // 'bottom' 或 'top' const isFocused = ref(false); // 跟踪焦点状态 // 附件相关变量(仅在传入 param 时初始化) let fjid = ref(null); let fjName = null; let mode = null; if (props.param && props.param.button) { fjid = ref(props.param.button.val); fjName = props.param.button.desc; mode = props.param.mode; } const showRequired = computed(() => { // 检查是否有验证规则(通过 window.ssVm 判断) const hasValidationRule = window.ssVm?.validations?.has(props.name); if (!hasValidationRule) return false; if (errMsg.value) return true; if (!inputValue.value) return true; return false; }); // 计算floatdiv应该向上还是向下展开 const calculateFloatingDivPosition = () => { nextTick(() => { const textarea = inputRef.value; if (!textarea) return; const rect = textarea.getBoundingClientRect(); const viewportHeight = window.innerHeight; // 预估floatdiv的高度(最多5行 * 20px + 上下padding + border) const estimatedFloatDivHeight = 20 * 5 + 10 + 2; // 5行 + padding + border = 112px // 检查下方空间 const spaceBelow = viewportHeight - rect.bottom; // 如果下方空间不足,且上方空间足够,则向上展开 if (spaceBelow < estimatedFloatDivHeight && rect.top > estimatedFloatDivHeight) { floatingDivPosition.value = "top"; } else { floatingDivPosition.value = "bottom"; } }); }; // 计算floatdiv的top偏移量 const getFloatingDivTop = computed(() => { if (props.height) { // 有height时,padding是5px return '5px'; } else { // 没有height时是单行居中,需要计算居中位置 // 假设input-container高度是32px(或者从CSS读取),单行20px // 居中偏移 = (容器高度 - 行高) / 2 = (32 - 20) / 2 = 6px return '6px'; } }); const validate = () => { if (window.ssVm) { const result = window.ssVm.validateField(props.name); console.log(result); errMsg.value = result.valid ? "" : result.message; } }; // 使用 watch 监听 props.errTip 和 props.modelValue 的变化 watch( () => props.errTip, (newVal) => { errMsg.value = newVal; }, { immediate: true } ); watch( () => props.modelValue, (newVal) => { inputValue.value = newVal; } ); // 挂载时的逻辑 onMounted(() => { errMsg.value = props.errTip; inputValue.value = props.modelValue || props.defaultValue || ""; }); // 计算并调整textarea的高度 const adjustHeight = () => { nextTick(() => { const textarea = textareaRef.value; if (!textarea) return; // floatDiv的textarea始终自动计算高度,不受props.height影响 // 重置高度以获得正确的scrollHeight textarea.style.height = "auto"; // 计算新高度 - 统一限制为5行 const lineHeight = parseInt(getComputedStyle(textarea).lineHeight, 10); const maxHeight = lineHeight * 5; // 统一为5行 const newHeight = Math.min(textarea.scrollHeight, maxHeight); textarea.style.height = `${newHeight}px`; }); }; // 检查是否应该显示浮动窗口(需要同时满足:有焦点 + 内容超出) // 修复新增页面点击就出现floatdiv的问题 by xu 20251212 const checkShouldShowFloatingDiv = () => { const textarea = inputRef.value; if (!textarea) return false; // 首先检查是否有内容,没有内容时不显示floatdiv by xu 20251212 if (!inputValue.value || inputValue.value.toString().trim() === '') { console.log('[floatdiv] 内容为空,不显示floatdiv'); return false; } // 判断内容是否超出 by xu 20251212 // 同时检查横向和纵向溢出,任一方向溢出都应显示floatdiv // 纵向溢出需要加容差值,避免padding/border导致的误判 by xu 20251212 const verticalTolerance = 5; // 容差值5px const isHorizontalOverflow = textarea.scrollWidth > textarea.clientWidth; const isVerticalOverflow = textarea.scrollHeight > textarea.clientHeight + verticalTolerance; const isOverflow = isHorizontalOverflow || isVerticalOverflow; console.log('[floatdiv] 溢出检测 - scrollWidth:', textarea.scrollWidth, 'clientWidth:', textarea.clientWidth, 'horizontalOverflow:', isHorizontalOverflow); console.log('[floatdiv] 溢出检测 - scrollHeight:', textarea.scrollHeight, 'clientHeight:', textarea.clientHeight, 'tolerance:', verticalTolerance, 'verticalOverflow:', isVerticalOverflow); const shouldShow = isFocused.value && isOverflow; console.log('[floatdiv] 最终判断 - isFocused:', isFocused.value, 'isOverflow:', isOverflow, 'shouldShow:', shouldShow); // 需要同时满足:有焦点 + 内容超出 return shouldShow; }; // 定义事件处理函数 const onInput = (event) => { const newValue = event.target.value; inputValue.value = newValue; emit("update:modelValue", newValue); validate(); // 输入时验证 nextTick(() => { // 检查是否需要显示浮动div contentFloatingDiv.value = checkShouldShowFloatingDiv(); // 如果需要显示floatdiv,计算其位置 if (contentFloatingDiv.value) { calculateFloatingDivPosition(); } }); adjustHeight(); }; const onFocus = (event) => { // 设置焦点状态为true isFocused.value = true; adjustHeight(); // 检查是否应该显示浮动窗口 nextTick(() => { contentFloatingDiv.value = checkShouldShowFloatingDiv(); if (contentFloatingDiv.value) { calculateFloatingDivPosition(); } }); }; // 失去焦点时进行验证 const onBlur = (event) => { emit("blur", event.target); validate(); // 失焦时验证 nextTick(() => { // 如果焦点不在 textarea 上,则隐藏浮动 div if (!document.activeElement.classList.contains("input-control")) { isFocused.value = false; contentFloatingDiv.value = false; } }); }; const onChange = (event) => { inputValue.value = event.target.value || ""; emit("change", inputValue.value); }; const onMouseover = (event) => { nextTick(() => { // setTimeout(contentFloatingDiv.value = true, 500) }); }; const onMouseleave = (event) => { // contentFloatingDiv.value = false }; // 处理键盘按下事件,禁止回车换行 by xu 20251212 const onKeydown = (event) => { // 如果不允许多行且按下的是回车键,阻止默认行为 if (!props.multiline && event.key === 'Enter') { event.preventDefault(); } }; // 附件按钮点击处理(从 SsEditor 搬运) const onAttachmentClick = (e) => { e.preventDefault(); if (!props.param || !props.param.button) { console.warn("未配置 param 参数"); return; } console.log("附件点击了"); console.log("param", props.param); console.log("cmsAddUrl", props.param.button.cmsAddUrl); // 如果 fjid 为空,先调用 cmsAddUrl 创建 if (fjid.value == null || fjid.value == "") { $.ajax({ type: "post", url: props.param.button.cmsAddUrl, async: false, data: { name: "fjid", ssNrObjName: "sh", ssNrObjId: "", }, success: function (_fjid) { console.log("cmsAddUrl success", _fjid); fjid.value = _fjid; }, }); } // 构建参数字符串 var str = "&nrid=T-" + fjid.value + "&objectId=" + fjid.value + "&objectName=" + fjName + "&callback=" + (window["fjidCallbackName"] || ""); console.log("str", str); // 打开附件编辑对话框 SS.openDialog({ src: props.param.button.cmsUpdUrl + str, headerTitle: "编辑", width: 900, high: 664, zIndex: 51, }); }; return { errMsg, inputValue, showRequired, onInput, onBlur, onChange, onMouseover, onMouseleave, onKeydown, // 新增:键盘事件处理 by xu 20251212 contentFloatingDiv, floatingDivPosition, getFloatingDivTop, inputRef, textareaRef, onFocus, onAttachmentClick, fjid, // 附件 ID,用于隐藏字段 }; }, render() { const { resolveComponent, h } = Vue; const SsIcon = resolveComponent("ss-icon"); const SsEditorIcon = resolveComponent("SsEditorIcon"); // 构建主textarea的样式 const mainTextareaStyle = {}; if (this.height) { mainTextareaStyle.height = "auto" // mainTextareaStyle.paddingTop = '5px'; // 有高度时加上padding-top // mainTextareaStyle.paddingBottom = '5px'; // 有高度时加上padding-bottom } else { // 没有指定height时,固定为单行高度 mainTextareaStyle.height = '20px'; // 行高20px mainTextareaStyle.lineHeight = '20px'; // 确保单行垂直居中 mainTextareaStyle.display = 'flex'; mainTextareaStyle.marginBottom = '5px'; } // 如果有附件按钮,为按钮留出空间 if (this.fj || this.param) { mainTextareaStyle.paddingRight = '75px'; } const mainTextareaRows = this.height ? Math.floor(parseFloat('80px') / 20) : 1 return h("div", { class: "input" }, [ h("div", { class: "input-container", }, [ h("div", { class: "input",style:"padding:5px 0" }, [ h("textarea", { ref: "inputRef", class: "input-control", name: this.name, value: this.inputValue, onInput: this.onInput, onFocus: this.onFocus, onBlur: this.onBlur, onChange: this.onChange, onKeydown: this.onKeydown, // 新增:禁止回车换行 by xu 20251212 placeholder: this.placeholder, onMouseover: this.onMouseover, // 监听鼠标悬停 onMouseleave: this.onMouseleave, // 监听鼠标离开 rows:mainTextareaRows, ...this.$attrs, style: mainTextareaStyle, autocomplete: "off", }), // 附件按钮(优先使用 param,兼容旧的 fj) this.param || this.fj ? h( "button", { type: "button", class: "fj-button", onClick: this.param ? this.onAttachmentClick : (e) => { e.preventDefault(); console.log("附件配置:", this.fj); }, }, [ h(SsEditorIcon, { class: "editor-icon-link", }), h("span", { class: "fj-button-text" }, "附件") ] ) : null, // this.showRequired ? h("div", { class: "required" }) : null, ]), this.contentFloatingDiv || "" ? h("div", { class: "floating-div", style: this.floatingDivPosition === "bottom" ? { // 向下展开: 覆盖原输入框,top对齐首行 top: this.getFloatingDivTop, bottom: 'auto', } : { // 向上展开: 同样覆盖原输入框,但从底部开始计算 top: 'auto', bottom: this.height ? '5px' : '6px', // 对齐到原textarea的底部padding位置 } }, [ h("textarea", { ref: "textareaRef", class: "input-control", value: this.inputValue, onInput: this.onInput, onBlur: this.onBlur, onFocus: this.onFocus, onKeydown: this.onKeydown, // 新增:禁止回车换行 by xu 20251212 onMouseover: this.onMouseover, // 监听鼠标悬停 onMouseleave: this.onMouseleave, // 监听鼠标离开 autocomplete: "off", onVnodeMounted: (vnode) => { vnode.el.focus(); }, }), ]) : null, // this.errMsg ? h(SsValidate, { errMsg: this.errMsg }) : null, ]), // 附件相关的隐藏字段(仅在有 param 时才渲染) this.param && [ // fjid 隐藏字段(只有当 fjid 有值时才渲染) this.fjid && this.fjid.value && h("input", { type: "hidden", name: "fjid", value: this.fjid.value, }), // 其他隐藏字段根据 name 生成 /* 去掉。文本框不需要(这是富文本才有的) Ben 20251205 h("input", { type: "hidden", name: this.name.replace(/wj$/, "") + "Edit", value: this.inputValue }), */ // h("input", { // type: "hidden", // name: this.name.replace(/wj$/, "") + "wj", // value: this.inputValue // }), /* 去掉。文本框不需要(这是富文本才有的) Ben 20251205 h("input", { type: "hidden", name: "ueditorpath", value: this.name }), */ ], ]); }, }; // ss-normal-input 登录输入 const SsLoginInput = { name: "SsLoginInput", inheritAttrs: false, props: { errTip: { type: String, }, type: { type: String, default: "text", }, required: { type: Boolean, default: false, }, placeholder: { type: String, default: "请输入", }, name: { type: String, default: "", }, defaultValue: [String, Number], modelValue: [String, Number], }, emits: ["update:modelValue", "input", "blur", "change"], // 允许更新 v-model 绑定的值 setup(props, { emit }) { const errMsg = ref(""); const inputRef = ref(null); const textareaRef = ref(null); const inputValue = ref(props.modelValue || props.defaultValue || ""); // 使用 watch 监听 props.errTip 和 props.modelValue 的变化 watch( () => props.errTip, (newVal) => { errMsg.value = newVal; }, { immediate: true } ); watch( () => props.modelValue, (newVal) => { inputValue.value = newVal; } ); // 挂载时的逻辑 onMounted(() => { errMsg.value = props.errTip; inputValue.value = props.modelValue || props.defaultValue || ""; }); // 定义事件处理函数 const onInput = (event) => { const newValue = event.target.value; inputValue.value = newValue; emit("update:modelValue", newValue); }; return { inputValue, onInput, inputRef, textareaRef }; }, render() { return h("div", { class: "input" }, [ h("div", { class: "input-container" }, [ h("div", { class: "input" }, [ h("input", { ref: "inputRef", class: "input-control", name: this.name, value: this.inputValue, onInput: this.onInput, type: this.type, placeholder: this.placeholder, required: this.required, ...this.$attrs, autocomplete: "off", }), this.required ? h("div", { class: "required" }) : null, ]), ]), ]); }, }; // ss-login-button const SsLoginButton = { name: "SsLoginButton", inheritAttrs: false, props: { class: { type: String, default: "", }, text: { type: String, default: "", }, type: { type: String, default: "button", }, }, emits: ["click"], setup(props, { emit }) { // 定义事件处理函数 const onClick = (event) => { // 发射一个 'click' 事件,你可以传递所需的参数 emit("click", event); }; return { props, onClick }; }, render() { const SsIcon = resolveComponent("ss-icon"); const SsLoginIcon = resolveComponent("ss-login-icon"); return h( "button", { class: "login-button", type: this.type, onClick: this.onClick }, [ h("span", [h(SsLoginIcon, { class: this.class })]), h("span", {}, this.text), ] ); }, }; // ss-objp 下拉选择 const SsObjp = { name: "SsObjp", inheritAttrs: false, props: { filter: { type: String, required: false, }, cb: { type: String, required: true, }, url: { type: String, required: true, }, name: { type: String, required: true, }, width: { type: String, default: "100%", }, placeholder: { type: String, default: "请选择", }, inp: { type: Boolean, default: false, }, opt: { type: Array, default: () => [], }, errTip: String, defaultValue: [String, Number], modelValue: [String, Number], direction: { type: String, default: "bottom", }, }, emits: ["update:modelValue", "input", "blur", "change"], setup(props, { emit }) { const canInput = props.inp; const errMsg = Vue.ref(props.errTip); const selectItem = Vue.ref({}); let inputText = Vue.ref(""); // 用于存储输入框的文本 const popupWinVisible = Vue.ref(false); const filteredOptions = Vue.ref(props.opt); const popupDirection = Vue.ref("bottom"); const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212 // const showRequired = Vue.computed(() => { // const hasValidationRule = window.ssVm?.validations?.has(props.name); // if (!hasValidationRule) return false; // if (errMsg.value) return true; // if (!selectItem.value?.value) return true; // return false; // }); const validate = () => { if (window.ssVm) { const result = window.ssVm.validateField(props.name); // console.log("validate", window.ssVm.validateField(props.name)); errMsg.value = result.valid ? "" : result.message; } }; //在objPicker界面,选中value对应的项 const updateSelectItem = () => { // console.log(props.opt); const item = props.opt.find((it) => it.value === props.modelValue); if (item) { selectItem.value = item; inputText.value = item.label; } else { selectItem.value = { label: "", value: "" }; inputText.value = ""; } // validate(); }; Vue.watch( () => props.errTip, (newVal) => { errMsg.value = newVal; } ); Vue.watch(() => props.modelValue, updateSelectItem, { immediate: true }); Vue.watch( () => props.opt, (newVal) => { updateSelectItem(); filteredOptions.value = [...newVal]; // console.log("filteredOptions", filteredOptions.value); } ); //初始化objPicker在页面刚打开时的默认值 async function initDefaultValue() { try { if (props.url && props.cb && props.modelValue) { let objectPickerParam; let url = props.url; //如果有定义过滤器 if (props.filter) { //包含HTML实体的JSON字符串转为JSON对象,如原字符串是{"dwid":"88"},注意key也必需用单引号包着 // const decodedString = props.filter.replace(/"/g, '"'); // 转换为: {"dwid":"88"} // objectPickerParam = JSON.parse(decodedString); // 转为json对象 const filterObj = props.filter; // 转为json对象 for (let k in filterObj) { let v = filterObj[k]; url += "&" + k + "=" + v; } objectPickerParam = props.filter; // 转为json对象 objectPickerParam["input"] = props.inp; objectPickerParam["codebook"] = props.cb; // alert(url); } else { objectPickerParam = { input: props.inp, codebook: props.cb }; } const objectPickerParamStr = JSON.stringify(objectPickerParam); const params = new URLSearchParams(); params.append("objectpickerparam", objectPickerParamStr); params.append("objectpickertype", "2"); params.append("objectpickervalue", props.modelValue); //需回显的值 // alert("1params:"+JSON.stringify(params)); axios .post(props.url, params, { headers: { "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置! }, }) .then((response) => { // alert(JSON.stringify(response.data)); if ("timeout" == response.data.statusText) { alert("网络超时!"); return; } if (response.data.result) { const keys = Object.keys(response.data.result); if (keys.length === 1) { let code = keys[0]; let desc = response.data.result[keys[0]]; if (props.opt) props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式 else { props.opt = []; } props.opt.push({ label: desc, value: code }); updateSelectItem(); // alert('props.opt:'+JSON.stringify(props.opt)); } } }); } } catch (error) { // callback(null, error.message); // 失败回调,传递错误 } } // Vue.onMounted(updateSelectItem); const doSelectItem = (item) => { emit("update:modelValue", item.value); selectItem.value = item; inputText.value = item.label; hidePopup(); nextTick(() => { console.log(item.value + "@@@props.modelValue:" + props.modelValue); validate(); if (window.ssVm) { // 遍历所有验证规则,找到依赖当前字段的规则 for (const [field, rules] of window.ssVm.validations.entries()) { for (const rule of rules) { if (rule.opt?.relField === props.name) { // console.log("Found dependent field:", field); // 调试日志 window.ssVm.validateField(field); } } } } }); }; //可录入的objPicker,更新下拉菜单选项 async function updateOptionBYInputText(inpTxt) { try { let objectPickerParam; let url = props.url; if (props.url && props.cb) { //如果有定义过滤器 if (props.filter) { const filterObj = props.filter; // 转为json对象 for (let k in filterObj) { let v = filterObj[k]; url += "&" + k + "=" + v; } //包含HTML实体的JSON字符串转为JSON对象,如原字符串是{"dwid":"88"},注意key也必需用单引号包着 // const decodedString = props.filter.replace(/"/g, '"'); // 转换为: {"dwid":"88"} // objectPickerParam = JSON.parse(decodedString); // 转为json对象 objectPickerParam = props.filter; objectPickerParam["input"] = props.inp; objectPickerParam["codebook"] = props.cb; // alert(url); } else { objectPickerParam = { input: props.inp, codebook: props.cb }; } const objectPickerParamStr = JSON.stringify(objectPickerParam); const params = new URLSearchParams(); params.append("objectpickerparam", objectPickerParamStr); params.append("objectpickertype", "1"); if (props.inp && props.inp === true) {//把"true"改为true Ben(20251209) params.append("objectpickersearchAll", 0); //只查录入的值 params.append("objectpickerinput", inpTxt); //录入的值 } else { params.append("objectpickersearchAll", 1); } axios .post(url, params, { headers: { "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置! }, }) .then((response) => { if ("timeout" == response.data.statusText) { alert("网络超时!"); return; } // 先清空选项 by xu 20251212 if (props.opt) { props.opt.length = 0; } else { props.opt = []; } if (response.data.result) { const keys = Object.keys(response.data.result); // console.log("params:"+params+"@@response.data:"+JSON.stringify(response.data)); if (keys.length > 0) { for (let k in response.data.result) { props.opt.push({ label: response.data.result[k], value: k, }); } // console.log('###inpTxt:'+inpTxt+';'); if ( props.inp && props.inp === true && //把"true"改为true Ben(20251209) inpTxt.length > 0 ) { //对于可录入的,用已录入的值作过滤 filteredOptions.value = props.opt.filter((option) => option.label .toLowerCase() .includes(inputText.value.toLowerCase()) ); // 可录入的objPicker,当搜索结果只有一项时,自动选中这一项 by xu 20251212 if (filteredOptions.value.length === 1) { const autoSelectItem = filteredOptions.value[0]; console.log("[objp] 搜索结果只有一项,自动选中:", autoSelectItem); doSelectItem(autoSelectItem); return; // 自动选中后直接返回,不需要显示popup } filteredOptions.value.unshift({ label: "", value: "" }); // console.log('###做了过滤:'+inputText.value.toLowerCase()+';'); } else { filteredOptions.value = props.opt; filteredOptions.value.unshift({ label: "", value: "" }); } console.log("props.opt11:" + JSON.stringify(props.opt)); } else { // 没有数据时,清空过滤选项 by xu 20251212 filteredOptions.value = []; console.log("[objp] 接口返回空数据"); } } else { // result不存在时,清空过滤选项 by xu 20251212 filteredOptions.value = []; console.log("[objp] 接口返回无result"); } // 无论是否有数据,都显示popup by xu 20251212 if (!popupWinVisible.value) { popupWinVisible.value = true; } }); } } catch (error) { // callback(null, error.message); // 失败回调,传递错误 } } // 计算弹出方向和最大高度的方法 by xu 20251212 // 当空间不足时限制popup高度并显示滚动条 const calculatePopupDirection = () => { // 1. 获取select容器元素 const selectEl = document .querySelector(`[name="${props.name}"]`) ?.closest(".select-container"); if (!selectEl) return; // 2. 获取位置信息 const selectRect = selectEl.getBoundingClientRect(); const viewportHeight = window.innerHeight; // 3. 计算上下可用空间 const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距 const spaceAbove = selectRect.top - 10; // 减10px留边距 // 4. popup预估高度(假设每项36px,最多显示8项 + padding) const estimatedPopupHeight = 300; const minPopupHeight = 100; // 最小高度 console.log('[popup] 空间计算 - spaceAbove:', spaceAbove, 'spaceBelow:', spaceBelow, 'estimatedHeight:', estimatedPopupHeight); // 5. 判断方向和最大高度 by xu 20251212 if (spaceBelow >= estimatedPopupHeight) { // 下方空间足够,向下展开,不限制高度 popupDirection.value = "bottom"; popupMaxHeight.value = "none"; console.log('[popup] 向下展开,空间充足'); } else if (spaceAbove >= estimatedPopupHeight) { // 上方空间足够,向上展开,不限制高度 popupDirection.value = "top"; popupMaxHeight.value = "none"; console.log('[popup] 向上展开,空间充足'); } else { // 上下空间都不足,选择空间大的方向,并限制高度出滚动条 if (spaceBelow >= spaceAbove) { popupDirection.value = "bottom"; popupMaxHeight.value = Math.max(spaceBelow, minPopupHeight) + "px"; console.log('[popup] 向下展开,空间不足,限制高度:', popupMaxHeight.value); } else { popupDirection.value = "top"; popupMaxHeight.value = Math.max(spaceAbove, minPopupHeight) + "px"; console.log('[popup] 向上展开,空间不足,限制高度:', popupMaxHeight.value); } } }; //点击下拉菜单的文本区域时,会触发的方法 function togglePopup() { //可录入的objPicker,更新下拉菜单选项 updateOptionBYInputText(inputText.value); // popupWinVisible.value = !popupWinVisible.value; Vue.nextTick(() => { calculatePopupDirection(); }); } const hidePopup = () => { popupWinVisible.value = false; }; //点击下拉菜单的三角形时,会触发的方法 // 添加toggle逻辑,点击时切换显示/隐藏 by xu 20251212 const suffixClick = () => { // 如果popup已显示,则关闭 by xu 20251212 if (popupWinVisible.value) { hidePopup(); console.log("[objp] 点三角关闭popup"); return; } //可录入的objPicker,更新下拉菜单选项 updateOptionBYInputText(""); Vue.nextTick(() => { calculatePopupDirection(); }); console.log("[objp] 点三角打开popup"); }; //可录入的objPicker,录入项变化时,会触发 async function handleInputChange(event) { inputText.value = event.target.value; if (!inputText.value) { inputText.value = ""; } //可录入的objPicker,更新下拉菜单选项 updateOptionBYInputText(inputText.value); // filteredOptions.value = props.opt.filter((option) => // option.label.toLowerCase().includes(inputText.value.toLowerCase()) // ); // if (!popupWinVisible.value) { // popupWinVisible.value = true; // 确保下拉框在输入时打开 // } } Vue.onMounted(() => { initDefaultValue(); window.addEventListener("resize", calculatePopupDirection); }); Vue.onUnmounted(() => { window.removeEventListener("resize", calculatePopupDirection); }); return { errMsg, selectItem, inputText, canInput, filteredOptions, popupWinVisible, popupDirection, popupMaxHeight, // 添加popup最大高度 by xu 20251212 suffixClick, togglePopup, hidePopup, doSelectItem, handleInputChange, }; }, template: `
`, }; // ss-hidden 隐藏字段组件 const SsHidden = { name: "SsHidden", props: { modelValue: String, name: { type: String, required: true, }, rule: { type: String, required: true, }, param: { type: String, required: true, }, url: { type: String, required: true, }, }, emits: ["update:modelValue"], setup(props, { emit }) { const errMsg = Vue.ref(""); const validate = () => { if (window.ssVm) { const result = window.ssVm.validateField(props.name); console.log("validate", window.ssVm.validateField(props.name)); errMsg.value = result.valid ? "" : result.message; } }; Vue.onMounted(() => { /** * 初始化级联菜单值初始值思路: * 1. 带隐藏字段(即带编码规则)的级联菜单 * 在隐藏字段这,可以取到要回显的值和编码规则,从而计算出各级下拉菜单要回显的值。 * 然后通过ajax取各级级联菜单的值回显。 * 2. 不带隐藏字段的级联,只能在各个下拉菜单的setup事件中通过ajax取回显值回显 */ // 当同组级联下拉菜单选中值变化时,会调用本隐藏字段下面这方法设置隐藏字段值 window.addEventListener( "cascader-setHiddenVal-" + props.name, (event) => { const { value } = event.detail; emit("update:modelValue", value); console.log(value); setTimeout(() => { validate(); }, 50); } ); // 如果有初始值,触发回显过程 if (props.modelValue) { console.log("级联隐藏字段,开始回显,初始值:", props.modelValue); triggerCascaderEcho(props.modelValue); validate(); } }); // 触发级联回显 const triggerCascaderEcho = (code) => { /** * 开始回显,初始值: 440304 * 解析后的所有值: Array(3)0: "440000"1: "440300"2: "440304"length: 3[[Prototype]]: Array(0) */ const values = parseHiddenCodeForAll(code, props.rule); console.log("解析后的所有值:", values); // 转换为 JSON 对象 // const paramObj = JSON.parse(props.param); const paramObj = props.param; let selectArr = paramObj.fieldOrd; //保存本组级联菜单项的数组,如:['hksheng','hkshi','hkxian'] if (selectArr.length != values.length) { // alert('属性'+props.name+'的值'+code+'与级联菜单中下拉菜单的数目不匹配!'); return; } // 按顺序触发回显,并增加延迟确保数据加载 /** * 通过隐藏字段的setup事件, * 循环遍历各级下拉菜单,并触发定义在下拉菜单中的'cascader-echo'事件, * 在此事件中完成每个下拉菜单回显值操作(只取当前要回显的键值对显示, * 下拉菜单所有的值,在点击下拉菜单时,才通过ajax取)。 */ values.forEach((value, index) => { if (value) { setTimeout(() => { let upperVal = undefined; if (index != 0) { upperVal = values[index - 1]; } const echoEvent = new CustomEvent( "cascader-echo-" + selectArr[index], { detail: { name: props.name, value: value, // level: index + 1, isAuto: true, // 标记为自动回显 upperVal: upperVal, }, } ); console.log(props.name + "--upperValue:" + upperVal); window.dispatchEvent(echoEvent); }, index * 500); // 每级增加500ms延迟 } }); }; // 解析所有级别的代码 const parseHiddenCodeForAll = (code, rule) => { if (!code || !rule) return []; // 获取规则中每段的长度 const segments = []; let currentChar = rule[0]; let currentLength = 1; for (let i = 1; i < rule.length; i++) { if (rule[i] === currentChar) { currentLength++; } else { segments.push(currentLength); currentChar = rule[i]; currentLength = 1; } } segments.push(currentLength); // 解析每一级的值 const values = []; let position = 0; segments.forEach((length, index) => { const value = code .substring(0, position + length) .padEnd(rule.length, "0"); values.push(value); position += length; }); return values; }; watchEffect(() => {}); return {}; }, template: ``, }; // ss-cascader 级联选择器 const SsCcp = { name: "SsCcp", inheritAttrs: false, props: { modelValue: String, name: { type: String, required: true, }, level: { type: Number, required: true, }, opt: { type: Array, default: () => [], }, placeholder: { type: String, default: "请选择", }, width: { type: String, default: "150px", }, direction: { type: String, default: "bottom", }, mode: { type: String, default: "1", }, //级联菜单配置参数,如果是数组,则代表本下拉菜单是多套级联菜单共用的第一级菜单。如果是对象,则只有一套级联菜单用此下拉菜单。 param: { type: String, required: true, }, //向后台拿数据的url url: { type: String, required: true, }, }, emits: ["update:modelValue", "change"], setup(props, { emit }) { // alert('级联菜单初始化:'+props.name+':--:'+props.modelValue); const selectItem = Vue.ref({ label: props.placeholder, value: "" }); const popupWinVisible = Vue.ref(false); const isAutoEcho = Vue.ref(false); // 用于标记是否是自动回显 const upperValue = Vue.ref(""); //上级下拉菜单当前值,在初始化下拉菜单默认值时,和上级下拉菜单的值变化时,修改此upperValue变量 const popupDirection = Vue.ref("bottom"); const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212 //有隐藏字段的下拉菜单,加载菜单项并展开事件 // 被上级下拉菜单选中值后,触发本下拉菜单刷新菜单项并弹出显示 window.addEventListener("cascader-open-" + props.name, async (event) => { const { upperVal } = event.detail; upperValue.value = upperVal; console.log( "22props.name:" + props.name + ",22props.upperValue:" + upperValue.value ); selectItem.value = ""; //清除本下拉菜单当前选中的值 emit("update:modelValue", ""); //通知父级 //清空下拉菜单,并设置第一项的值为placeholder clearAndInit1stOpt(); //下个下拉菜单名 let nextSelName = getNextSel(props.name, props.param.fieldOrd); if (nextSelName) { //清下个下拉菜单选中值和选项 event = new CustomEvent("cascader-cleanOpt-" + nextSelName, { detail: {}, }); window.dispatchEvent(event); } showPopup(); }); //设置mode2的下级下拉菜单的上级菜单当前值 function setNextSelectUpperValue() { //设置下级菜单的上级菜单当前值upperValue let paramArr = undefined; if (Array.isArray(props.param)) { paramArr = props.param; } else { paramArr = []; paramArr.push(props.param); } for (const oneParam of paramArr) { //下个下拉菜单名 const nextSelName = getNextSel(props.name, oneParam.fieldOrd); if (nextSelName) { setTimeout(() => { const openNextEvent = new CustomEvent( "cascade-setUpperVal-" + nextSelName, { detail: { upperVal: props.modelValue, }, } ); window.dispatchEvent(openNextEvent); }, 100); } } // end for } // 把上级 级联下拉菜单的值,设置进本组件的事件 window.addEventListener("cascade-setUpperVal-" + props.name, (event) => { // alert('props.name:'+props.name+',props.upperValue:'+event.detail.upperVal); const { upperVal } = event.detail; upperValue.value = upperVal; // console.log('props.name:'+props.name+',props.upperValue:'+upperValue.value); }); //清空下拉菜单,并设置第一项的值为空 function clearAndInit1stOpt() { if (props.opt) props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式 else { props.opt = []; } props.opt.push({ label: "", value: "" }); } //获取下一级下拉菜单,如果下一级下拉菜单不存在,则返回undefined function getNextSel(selName, selNameArr) { // 检查参数有效性 if (!Array.isArray(selNameArr) || selNameArr.length === 0) { return undefined; } // 查找当前元素的索引 const currentIndex = selNameArr.indexOf(selName); // 如果元素不存在或已经是最后一个元素,返回undefined if (currentIndex === -1 || currentIndex === selNameArr.length - 1) { return undefined; } // 返回下一个元素 return selNameArr[currentIndex + 1]; } // 处理选择事件 const doSelectItem = (item) => { selectItem.value = item; emit("update:modelValue", item.value); //修改本下拉菜单在vue中保存的值 // alert('item.value:'+item.value); if (props.mode === "1") { // mode 1 模式:修改隐藏字段值 let event = new CustomEvent( "cascader-setHiddenVal-" + props.param.combField, { detail: { value: item.value, }, } ); window.dispatchEvent(event); } emit("change", item.value); //触发配置的change方法 //设置下级菜单的上级菜单当前值upperValue let paramArr = undefined; if (Array.isArray(props.param)) { paramArr = props.param; } else { paramArr = []; paramArr.push(props.param); } for (const oneParam of paramArr) { //下个下拉菜单名 const nextSelName = getNextSel(props.name, oneParam.fieldOrd); if (nextSelName) { setTimeout(() => { const openNextEvent = new CustomEvent( "cascader-open-" + nextSelName, { detail: { upperVal: item.value, }, } ); window.dispatchEvent(openNextEvent); }, 100); } } // end for hidePopup(); //下个下拉菜单名 // let nextSelName = getNextSel(props.name, props.param.fieldOrd); // if(nextSelName){ // // //设置下一级下拉菜单中保存的本下拉菜单值(upperValue) // // event = new CustomEvent('cascade-setUpperVal-'+nextSelName, { // // detail: { // // value: item.value // // } // // }); // // window.dispatchEvent(event); // // //触发下一级下拉菜单,重新初始化下拉菜单项并弹出显示 // event = new CustomEvent('cascader-open-' +nextSelName, { // detail: { // upperVal: item.value // } // }); // window.dispatchEvent(event); // } // 只在手动选择时自动展开下一级 // if (!isAutoEcho.value) { // const nextLevel = props.level + 1; // setTimeout(() => { // const openNextEvent = new CustomEvent('open-next-cascader', { // detail: { // name: props.name, // level: nextLevel // } // }); // window.dispatchEvent(openNextEvent); // }, 100); // } }; // 监听下一级展开事件 (仅 mode 2) window.addEventListener("cascade-open", (event) => { if (props.mode === "2") { const { level } = event.detail; if (level === props.level) { popupWinVisible.value = true; } } }); if (props.mode === "1") { //如果是有隐藏字段的下拉菜单 // 监听回显事件 window.addEventListener( "cascader-echo-" + props.name, async (event) => { const { name, value, isAuto, upperVal } = event.detail; // level, if (upperVal) { upperValue.value = upperVal; console.log( "value:" + value + ",upperValue:" + upperValue + ",初始化级联组件时props.name:" + props.name ); } // if (name === props.name && level === props.level) { // 设置自动回显标记 isAutoEcho.value = true; // if (props.opt.length === 0) { // const loadDataEvent = new CustomEvent('cascader-load-data', { // detail: { // name: props.name, // level: props.level, // value: value // } // }); // window.dispatchEvent(loadDataEvent); //下面的代码只用于页面刚打开时,初始化级联菜单的回显值。 //Vue.watch用于监听数据的变化,并在数据变化时执行特定的回调函数。 //这段代码使用了 Vue.js 的 watch API 来监听 props.opt 的变化,如果props.opt有变化,则自动 // const unwatch = Vue.watch( // () => props.opt, // 监听的数据源(props.opt) // (newOptions) => { // 回调函数 // if (newOptions.length > 0) { // 条件判断 // matchAndSelect(value); // 执行逻辑 // unwatch(); // 停止监听 // } // }, // { immediate: true } // 配置:立即触发一次 // ); // } else { // matchAndSelect(value); // } // 初始化级联菜单在页面刚打开时的默认值 async function initDefaultValue(value) { try { // alert(1); if (props.url && props.param // && props.modelValue 对于有rule编码规则的级联菜单(即mode=1),modelValue一定是空的,所以注释掉,修复mode=1的级联菜单无法回显问题。Ben(20251124) ) { // alert(2); /** * let objectPickerParam= * {"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"hksheng,hkshi,hkxian\"," + * "\"name\":\"hksheng\"," + * "\"cascadingName\":\"dq\",\"cascadingInputsName\":\"hkdqm\"," + * "\"codebook\":\"sheng\"}", * "objectpickertype":2, * "objectpickervalue":"440000" * }; */ const objectPickerParam = { input: "false", cascadingLevel: props.param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian" name: props.name, //本下拉菜单名 cascadingName: props.param.name, //级联菜单名 cascadingInputsName: props.param.combField, //对象属性,即隐藏字段名,如:hkdqm codebook: props.param.codebook, }; const objectPickerParamStr = JSON.stringify(objectPickerParam); const params = new URLSearchParams(); params.append("objectpickerparam", objectPickerParamStr); params.append("objectpickertype", "2"); params.append("objectpickervalue", value); //需回显的值 axios .post(props.url, params, { headers: { "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置! }, }) .then((response) => { // alert(JSON.stringify(response.data)); if ("timeout" == response.data.statusText) { alert("网络超时!"); return; } if (response.data.result) { const keys = Object.keys(response.data.result); if (keys.length === 1) { let code = keys[0]; let desc = response.data.result[keys[0]]; clearAndInit1stOpt(); props.opt.push({ label: desc, value: code }); if (value) matchAndSelect(value); // updateSelectItem(); // alert('props.opt:'+JSON.stringify(props.opt)); } } }); } } catch (error) { alert(error); // callback(null, error.message); // 失败回调,传递错误 } } //下面的代码只用于页面刚打开时,初始化级联菜单的回显值。 initDefaultValue(value); // 延迟重置自动回显标记 setTimeout(() => { isAutoEcho.value = false; }, 500); } ); // 被上级下拉菜单触发的,清除选中值和下拉菜单选项 window.addEventListener( "cascader-cleanOpt-" + props.name, async (event) => { upperValue.value = ""; selectItem.value = ""; //清除本下拉菜单当前选中的值 emit("update:modelValue", ""); //通知父级 //清空所有下拉菜单项 if (props.opt) { props.opt.length = 0; } else { props.opt = []; } //下个下拉菜单名 let nextSelName = getNextSel(props.name, props.param.fieldOrd); // alert('nextSelName:'+nextSelName+'--,props.name:'+props.name); if (nextSelName) { //清下个下拉菜单选中值和选项 event = new CustomEvent("cascader-cleanOpt-" + nextSelName, { detail: {}, }); window.dispatchEvent(event); } } ); } else if (props.mode === "2") { //没隐藏字段的下拉菜单,在这初始化默认值 let needInitParam = undefined; if (Array.isArray(props.param)) { needInitParam = props.param[props.param.length - 1]; //只初始化数组最后一项 console.log("needInitParam最后一项:" + JSON.stringify(needInitParam)); } else { needInitParam = props.param; } // 初始化级联菜单在页面刚打开时的默认值 async function initDefaultValue(value, param) { try { // alert(1); if (props.url && param && props.modelValue) { // alert(2); /** * let param= * {"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"rylbm,gwid\"," + * "\"name\":\"gwid\",\"cascadingName\":\"rylb_gw\"," + * "\"codebook\":\"gwByRylb\"}", * "objectpickertype":2, * "objectpickervalue":"102121"}; */ // alert('props.name:'+props.name+',props.param.fieldOrd:'+props.param.fieldOrd); const objectPickerParam = { input: "false", cascadingLevel: param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian" name: props.name, //本下拉菜单名 cascadingName: param.name, //级联菜单名 codebook: param.codebook, }; const objectPickerParamStr = JSON.stringify(objectPickerParam); const sendParams = new URLSearchParams(); sendParams.append("objectpickerparam", objectPickerParamStr); sendParams.append("objectpickertype", "2"); sendParams.append("objectpickervalue", value); //需回显的值 axios .post(props.url, sendParams, { headers: { "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置! }, }) .then((response) => { // alert(JSON.stringify(response.data)); if ("timeout" == response.data.statusText) { alert("网络超时!"); return; } if (response.data.result) { const keys = Object.keys(response.data.result); console.log( "name:" + props.name + ",@@级联初始化默认值value:" + value + "--param:" + JSON.stringify(param) + "--objectPickerParamStr:" + objectPickerParamStr + "--response.data:" + JSON.stringify(response.data) ); if (keys.length === 1) { let code = keys[0]; let desc = response.data.result[keys[0]]; if (props.opt) props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式 else { props.opt = []; } props.opt.push({ label: desc, value: code }); if (value) matchAndSelect(value); console.log( "GOOD mode2回显的默认值:" + JSON.stringify({ label: desc, value: code }) + "--props.param:" + JSON.stringify(param) ); // updateSelectItem(); // alert('props.opt:'+JSON.stringify(props.opt)); } } }); } } catch (error) { alert(error); // callback(null, error.message); // 失败回调,传递错误 } // 重置自动回显标记 isAutoEcho.value = false; } // 初始化级联菜单在页面刚打开时的默认值 initDefaultValue(props.modelValue, needInitParam); //设置mode2的下级下拉菜单的上级菜单当前值 setNextSelectUpperValue(); } //选中要回显的默认值 const matchAndSelect = (value) => { const matchedOption = props.opt.find((opt) => opt.value === value); if (matchedOption) { selectItem.value = matchedOption; emit("update:modelValue", value); emit("change", value); } }; // 计算弹出方向和最大高度的方法 by xu 20251212 // 当空间不足时限制popup高度并显示滚动条 const calculatePopupDirection = () => { // 1. 获取select容器元素 const selectEl = document.querySelector( `[name="${props.name}"]` )?.nextElementSibling; console.log("selectEl:" + selectEl, props.name); if (!selectEl) return; // 2. 获取位置信息 const selectRect = selectEl.getBoundingClientRect(); const viewportHeight = window.innerHeight; // 3. 计算上下可用空间 by xu 20251212 const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距 const spaceAbove = selectRect.top - 10; // 减10px留边距 // 4. popup预估高度(假设每项36px,最多显示8项 + padding) const estimatedPopupHeight = 300; const minPopupHeight = 100; // 最小高度 console.log('[popup] 空间计算 - spaceAbove:', spaceAbove, 'spaceBelow:', spaceBelow, 'estimatedHeight:', estimatedPopupHeight); // 5. 判断方向和最大高度 by xu 20251212 if (spaceBelow >= estimatedPopupHeight) { // 下方空间足够,向下展开,不限制高度 popupDirection.value = "bottom"; popupMaxHeight.value = "none"; console.log('[popup] 向下展开,空间充足'); } else if (spaceAbove >= estimatedPopupHeight) { // 上方空间足够,向上展开,不限制高度 popupDirection.value = "top"; popupMaxHeight.value = "none"; console.log('[popup] 向上展开,空间充足'); } else { // 上下空间都不足,选择空间大的方向,并限制高度出滚动条 if (spaceBelow >= spaceAbove) { popupDirection.value = "bottom"; popupMaxHeight.value = Math.max(spaceBelow, minPopupHeight) + "px"; console.log('[popup] 向下展开,空间不足,限制高度:', popupMaxHeight.value); } else { popupDirection.value = "top"; popupMaxHeight.value = Math.max(spaceAbove, minPopupHeight) + "px"; console.log('[popup] 向上展开,空间不足,限制高度:', popupMaxHeight.value); } } }; //级联菜单点击事件 const togglePopup = () => { if (!popupWinVisible.value) { //如果当前下拉菜单是隐藏的,先ajax重新加载下拉菜单项,再显示。 showPopup(); } else { hidePopup(); } }; //显示下拉菜单,在此之前先清除下拉菜单项 const showPopup = () => { //清空下拉菜单,并设置第一项的值为空 clearAndInit1stOpt(); Vue.nextTick(() => { calculatePopupDirection(); }); let url = props.url; let filterObj = props.param.filter; if (filterObj) { for (let k in filterObj) { let v = filterObj[k]; url += "&" + k + "=" + v; } } if (props.mode === "1") { //如果是有隐藏字段的下拉菜单 console.log("666url:" + url); // alert('url:'+url); // 获取级联菜单所有下拉菜单项 async function getSelectItems(value) { try { // alert(1); if (props.url && props.param) { // alert(2); /** * param={"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"hksheng,hkshi,hkxian\"," + * "\"name\":\"hksheng\",\"cascadingName\":\"dq\"," + * "\"cascadingInputsName\":\"hkdqm\",\"codebook\":\"sheng\"}", * "objectpickertype":1,//2表示获取要回显的一项,1表示获取所有下拉菜单项 * "upperValue":"440000" * }; */ const objectPickerParam = { input: "false", cascadingLevel: props.param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian" name: props.name, //本下拉菜单名 cascadingName: props.param.name, //级联菜单名 cascadingInputsName: props.param.combField, //对象属性,即隐藏字段名,如:hkdqm codebook: props.param.codebook, }; console.log("mode1 upperValue.value:" + upperValue.value); const objectPickerParamStr = JSON.stringify(objectPickerParam); const params = new URLSearchParams(); params.append("objectpickerparam", objectPickerParamStr); params.append("objectpickertype", "1"); if (upperValue.value) { params.append("upperValue", upperValue.value); } // params.append('objectpickervalue', value); //需回显的值 axios .post(url, params, { headers: { "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置! }, }) .then((response) => { if ("timeout" == response.data.statusText) { alert("网络超时!"); return; } if (response.data.result) { const keys = Object.keys(response.data.result); console.log( "params:" + params + "@@response.data:" + JSON.stringify(response.data) ); if (keys.length > 0) { for (let k in response.data.result) { props.opt.push({ label: response.data.result[k], value: k, }); } console.log("props.opt11:" + JSON.stringify(props.opt)); } else { // 没有数据时打印日志 by xu 20251212 console.log("[ccp mode1] 接口返回空数据"); } } else { // result不存在时打印日志 by xu 20251212 console.log("[ccp mode1] 接口返回无result"); } // 无论是否有数据,都显示popup by xu 20251212 if (!popupWinVisible.value) { popupWinVisible.value = true; } }); } } catch (error) { alert(error); // callback(null, error.message); // 失败回调,传递错误 } } getSelectItems(props.modelValue); } else if (props.mode === "2") { //没隐藏字段的下拉菜单 let needInitParam = undefined; if (Array.isArray(props.param)) { needInitParam = props.param[props.param.length - 1]; //只初始化数组最后一项 console.log( "needInitParam最后一项:" + JSON.stringify(needInitParam) ); } else { needInitParam = props.param; } // 获取级联菜单所有下拉菜单项 async function getSelectItems(value, sendParam) { try { // alert(1); if (props.url && sendParam) { // alert(2); /** * param="{\"input\":\"false\",\"cascadingLevel\":\"dwid,sjryid\", * \"ryid\":\"111121\",\"name\":\"sjryid\", * \"cascadingName\":\"dw_sjry\",\"codebook\":\"sjryByDw\"}" */ const objectPickerParam = { input: "false", cascadingLevel: sendParam.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian" name: props.name, //本下拉菜单名 cascadingName: sendParam.name, //级联菜单名 codebook: sendParam.codebook, }; console.log("mode2 upperValue.value:" + upperValue.value); const objectPickerParamStr = JSON.stringify(objectPickerParam); const params = new URLSearchParams(); params.append("objectpickerparam", objectPickerParamStr); params.append("objectpickertype", "1"); if (upperValue.value) { params.append("upperValue", upperValue.value); } // params.append('objectpickervalue', value); //需回显的值 axios .post(url, params, { headers: { "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置! }, }) .then((response) => { if ("timeout" == response.data.statusText) { alert("网络超时!"); return; } if (response.data.result) { const keys = Object.keys(response.data.result); console.log( "params:" + params + "@@response.data:" + JSON.stringify(response.data) ); if (keys.length > 0) { for (let k in response.data.result) { props.opt.push({ label: response.data.result[k], value: k, }); } console.log("props.opt11:" + JSON.stringify(props.opt)); } else { // 没有数据时打印日志 by xu 20251212 console.log("[ccp mode2] 接口返回空数据"); } } else { // result不存在时打印日志 by xu 20251212 console.log("[ccp mode2] 接口返回无result"); } // 无论是否有数据,都显示popup by xu 20251212 if (!popupWinVisible.value) { popupWinVisible.value = true; } }); } } catch (error) { alert(error); // callback(null, error.message); // 失败回调,传递错误 } } getSelectItems(props.modelValue, needInitParam); // popupWinVisible.value = !popupWinVisible.value; } }; const hidePopup = () => { popupWinVisible.value = false; }; // 合并所有的 onMounted 逻辑 Vue.onMounted(() => { window.addEventListener("resize", calculatePopupDirection); // 1. 监听展开下一级事件 window.addEventListener("open-next-cascader", (event) => { const { name, level } = event.detail; if (name === props.name && level === props.level) { popupWinVisible.value = true; } }); // 2. 监听级联事件 window.addEventListener("cascader-change", (event) => { const { name, level, value } = event.detail; if (name === props.name && level < props.level) { selectItem.value = { label: "", value: "" }; emit("update:modelValue", ""); if (ssHidden) { ssHidden.updateValue(value); } } }); }); Vue.onUnmounted(() => { window.removeEventListener("resize", calculatePopupDirection); }); // 监听值变化,处理回显 (mode 2) Vue.watch( () => props.modelValue, (newVal) => { if (props.mode === "2" && newVal) { // 使用 watchEffect 替代嵌套的 watch Vue.watchEffect(() => { if (props.opt.length > 0) { const matchedOption = props.opt.find( (opt) => opt.value === newVal ); if (matchedOption) { selectItem.value = matchedOption; } } }); } else { // 原有的值变化处理 const item = props.opt.find((it) => it.value === newVal); if (item) { selectItem.value = item; } else { selectItem.value = { label: "", value: "" }; } } }, { immediate: true } ); // 监听选项变化,当数据加载完成时进行匹配 Vue.watch( () => props.opt, (newOptions) => { if (newOptions.length > 0) { const matchedOption = newOptions.find( (opt) => opt.value === selectItem.value.value ); if (matchedOption) { selectItem.value = matchedOption; emit("update:modelValue", matchedOption.value); emit("change", matchedOption.value); } } } ); return { selectItem, popupWinVisible, popupDirection, popupMaxHeight, // 添加popup最大高度 by xu 20251212 togglePopup, hidePopup, doSelectItem, }; }, template: `
`, }; // ss-date-picker 日期时间选择器组件 const SsDatePicker = { name: "SsDatePicker", props: { modelValue: { type: [String, Number, Date], default: "", }, name: { type: String, required: true, }, type: { type: String, default: "date", validator: (value) => ["date", "datetime", "time"].includes(value), }, fmt: { type: String, default: null, }, placeholder: { type: String, default: "", }, width: { type: String, default: "100%", }, }, emits: ["update:modelValue"], setup(props, { emit }) { const errMsg = ref(""); const validate = () => { if (window.ssVm) { const result = window.ssVm.validateField(props.name); console.log("validate", window.ssVm.validateField(props.name)); errMsg.value = result.valid ? "" : result.message; } }; // 根据type确定默认格式 const defaultFormat = computed(() => { switch (props.type) { case "datetime": return "YYYY-MM-DD HH:mm:ss"; case "date": return "YYYY-MM-DD"; case "time": return "HH:mm:ss"; } }); const convertJavaFormatToElement = (javaFormat) => { if (!javaFormat) return null; return javaFormat .replace("yyyy", "YYYY") .replace("MM", "MM") .replace("dd", "DD") .replace("HH", "HH") .replace("mm", "mm") .replace("ss", "ss"); }; const finalFormat = computed(() => { if (props.fmt) { return convertJavaFormatToElement(props.fmt); } return defaultFormat.value; }); // 使用 resolveComponent 获取组件 const ElDatePicker = resolveComponent("ElDatePicker"); const ElTimePicker = resolveComponent("ElTimePicker"); const SsFormIcon = resolveComponent("SsFormIcon"); const ElIcon = resolveComponent("ElIcon"); const handleValueUpdate = (val) => { emit("update:modelValue", val); emit("change", val); // 同时触发 change 事件 setTimeout(() => { validate(); }, 50); }; const dateType = computed(() => { const fmt = props.fmt || ""; if (fmt.includes("HH:mm:ss")) { return "datetime"; } else if (fmt.includes("HH:mm")) { return "datetime"; } else if (fmt.includes("mm:ss")) { return "time"; } return "date"; }); let useTimePicker = true; //"yyyy-MM-dd HH:mm:ss"; "日期字符串格式在java的写法",传到本组件fmt属性也是按这个格式 if (props.fmt) { //有fmt属性,则以fmt属性优先判断类型 if (/[dMy]/.test(props.fmt)) { //如果有传入日期格式,且含年月日 useTimePicker = false; } else { useTimePicker = true; } } else if (props.type !== "time") { useTimePicker = false; } return () => h("div", { class: "ss-date-picker", style: { width: props.width } }, [ h("input", { type: "hidden", name: props.name, value: props.modelValue, }), // 选择组件 h(useTimePicker ? ElTimePicker : ElDatePicker, { modelValue: props.modelValue, "onUpdate:modelValue": handleValueUpdate, type: dateType.value, format: finalFormat.value, "value-format": finalFormat.value, clearable: true, placeholder: props.placeholder, class: "custom-date-picker", // 用于自定义样式 "time-arrow-control": props.type === "datetime", // 修改这里 size: "large", // 添加这一行,改为 large 尺寸 style: { width: "100%" }, "prefix-icon": h(SsFormIcon, { class: "form-icon-time" }), }), ]); }, }; // ss-icon 图标 // v3.0 增加 class 属性分支:有 class 走新逻辑,否则走 v1.0 逻辑 by xu 20251212 // v3.0 用法: // v1.0 用法: const SsIcon = { name: "SsIcon", // v3.0 禁用 class 透传,手动处理 by xu 20251215 inheritAttrs: false, props: { // v1.0: 以下为旧属性 name: { type: String }, size: { type: [Number, String], default: 16 }, unit: { type: String, default: "px" }, color: String, type: { type: String, default: ssIcon.name, validator: function (value) { return [ssIcon, commonIcon].some((icon) => icon.name === value); }, }, }, emits: ["update:modelValue", "input", "blur", "change"], setup(props, { emit, attrs }) { // v3.0 分支:有 class 属性时直接渲染(从 attrs 获取) by xu 20251215 if (attrs.class) { return () => h("i", { class: attrs.class + ' icon-container' }); } // v1.0 分支:原有逻辑 const useIconType = computed(() => { return [ssIcon, commonIcon].find( (iconConfig) => iconConfig.name === props.type ); }); const iconName = computed(() => { const iconConfig = useIconType.value; // 注意:使用 .value 来访问响应式引用的值 if (!iconConfig) { console.error(`Icon type "${props.type}" not found.`); return ""; } const iconType = iconConfig.types[props.name]; if (!iconType) { console.error( `Icon name "${props.name}" not found in type "${props.type}".` ); return ""; } return `${iconConfig.prefix}${iconType}`; }); // 类似地,你可以计算 fontFamily 和 style const fontFamily = computed(() => { return useIconType.value ? useIconType.value.family : ""; }); // console.log(iconName.value,fontFamily.value) const style = computed(() => { const sizeStyle = isNum(props.size) ? `${props.size}${props.unit}` : props.size; const styleObj = { fontSize: sizeStyle, color: props.color || "", }; return toStyleStr(styleObj); }); // 使用渲染函数定义模板逻辑 return () => h("i", { class: ["icon-container", iconName.value, fontFamily.value], style: style.value, }); }, }; // 通用icon const SsCommonIcon = { name: "SsCommonIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("i", { class: props.class + " common-icon", }); }, }; // 登录页icon const SsLoginIcon = { name: "SsLoginIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("div", { class: props.class + " login-icon", }); }, }; // 弹窗icon const SsDialogIcon = { name: "SsDialogIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("i", { class: props.class + " dialog-icon", }); }, }; // 全局左侧导航图标组件 const SsNavIcon = { name: "SsNavIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("div", { class: props.class + " nav-icon", }); }, }; // 顶部工具栏图标组件 const SsHeaderIcon = { name: "SsHeaderIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("div", { class: props.class + " header-icon", }); }, }; // 全局菜单图标组件 const SsGolbalMenuIcon = { name: "SsGolbalMenuIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("div", { class: props.class + " global-menu-icon", }); }, }; // 全局查询列表卡片图标 const SsCartListIcon = { name: "SsCartListIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("div", { class: props.class + " cart-list-icon", }); }, }; // 全局底部工具栏图标组件 const SsQuickIcon = { name: "SsQuickIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("div", { class: props.class + " quick-icon", }); }, }; // 表单组件icon const SsFormIcon = { name: "SsFormIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("div", { class: props.class + " form-icon", }); }, }; // 弹窗底部按钮icon const SsBottomDivIcon = { name: "SsBottomDivIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("div", { class: props.class + " bottom-div-icon", }); }, }; // editor组件icon const SsEditorIcon = { name: "SsEditorIcon", props: { class: { type: String, required: true, }, }, setup(props) { return () => h("i", { class: props.class + " editor-icon", }); }, }; // ss-validate校验器 const SsValidate = { name: "SsValidate", props: { errMsg: { type: String }, textAlign: { type: String, default: "left" }, style: { type: Object, default: () => ({}) }, }, template: `
{{ errMsg }}
{{ errMsg }}
`, }; // ss-onoff-array 多选按钮 数组形式 const SsonoffArray = { name: "SsonoffArray", props: { name: { type: String, required: true, }, opt: { type: Array, default: () => [], }, defaultValue: [String, Number, Array], modelValue: [String, Number, Array], multiple: { // 新增多选模式属性 type: Boolean, default: false, }, // 是否允许一项都不选,默认true允许 by xu 20251212 null: { type: Boolean, default: true, }, }, emits: ["update:modelValue"], // 允许更新 v-model 绑定的值 setup(props, { emit }) { console.log("多选按钮", props.opt); // 使用数组来存储选中值 const checkedValue = ref( props.multiple ? Array.isArray(props.defaultValue) ? props.defaultValue : [] : props.defaultValue ); const errMsg = ref(props.errTip); // 生成icon名字 const genIconName = (itemValue) => { if (props.multiple) { return checkedValue.value.includes(itemValue) ? "form-icon-onoff-checked" : "form-icon-onoff-unchecked"; } return checkedValue.value === itemValue ? "form-icon-onoff-checked" : "form-icon-onoff-unchecked"; }; // 选中项 const selectItem = (value) => { if (props.multiple) { // 多选模式 const index = checkedValue.value.indexOf(value); if (index === -1) { checkedValue.value = [...checkedValue.value, value]; } else { // 取消选中当前项 const newValue = checkedValue.value.filter((v) => v !== value); // 如果不允许为空且取消后为空,则阻止取消操作 by xu 20251212 if (!props.null && newValue.length === 0) { return; // 阻止取消最后一项 } checkedValue.value = newValue; } } else { // 单选模式 // 如果点击的是当前已选中的项,判断是否允许取消 by xu 20251212 if (checkedValue.value === value) { if (!props.null) { return; // 不允许为空时,阻止取消 } checkedValue.value = ""; // 允许为空时,取消选中 } else { checkedValue.value = value; } } emit("update:modelValue", checkedValue.value); nextTick(() => { // 触发验证 if (window.ssVm) { window.ssVm.validateField(props.name); } }); }; return { checkedValue, genIconName, selectItem }; }, // 使用渲染函数定义模板逻辑 render() { const SsFormIcon = resolveComponent("ss-form-icon"); return h("div", { class: "radio-container" }, [ // 根据情况创建 input this.multiple ? this.checkedValue.length ? // 多选且有选中值:为选中项创建 input this.checkedValue.map((value) => h("input", { type: "checkbox", name: this.name, value: value, checked: true, style: { display: "none" }, }) ) : // 多选但没有选中值:创建一个空值 input h("input", { type: "hidden", name: this.name, value: "", }) : // 单选模式:创建一个 input h("input", { type: "hidden", name: this.name, value: this.checkedValue || "", }), this.opt.map((item, i) => h( "div", { key: i, class: { checked: this.multiple ? this.checkedValue.includes(item.value) : this.checkedValue === item.value, }, style: { width: item.width }, onClick: () => this.selectItem(item.value), }, [ h("span", null, item.label), h("div", { class: "mark" }, [ h(SsFormIcon, { class: this.genIconName(item.value), }), ]), ] ) ), ]); }, }; // ss-onoff 一个按钮 const Ssonoff = { name: "Ssonoff", props: { name: { type: String, required: true, }, label: { type: String, required: true, }, value: { type: [String, Number], required: true, }, width: { type: String, default: "", }, modelValue: [String, Number, Array], multiple: { type: Boolean, default: false, }, null: { type: Boolean, default: true, }, }, emits: ["update:modelValue"], setup(props, { emit }) { const parseModelValue = (val) => { if (!val) return []; // 如果以逗号开头,去掉开头的逗号 const cleanValue = val.toString().replace(/^,+/, ""); if (cleanValue.includes("|")) { return cleanValue.split("|"); } if (cleanValue.includes(",")) { return cleanValue.split(","); } return [cleanValue]; }; // 判断当前按钮是否选中 const isChecked = computed(() => { if (props.multiple) { const currentValue = parseModelValue(props.modelValue); return currentValue.includes(props.value.toString()); // return Array.isArray(props.modelValue) && props.modelValue.includes(props.value); } return (props.modelValue+'') === (props.value+'');//强转为字符串类型再比较(改之前是数字类型和字符串类型作比较,永远为false) Ben 20251206 }); // 切换选中状态 const toggleSelect = () => { if (props.multiple) { const currentValue = parseModelValue(props.modelValue); const index = currentValue.indexOf(props.value.toString()); let newValue; if (index === -1) { // 选中当前项 newValue = [...currentValue, props.value]; } else { // 取消选中当前项 const filteredValue = currentValue.filter((v) => v !== props.value.toString()); // 如果不允许为空且取消后为空,则阻止取消操作 if (!props.null && filteredValue.length === 0) { return; // 阻止取消最后一项 } newValue = filteredValue; } emit("update:modelValue", newValue.join(",")); } else { // 单选模式 const currentValue = parseModelValue(props.modelValue); const isCurrentlySelected = currentValue.includes(props.value.toString()); if (!isCurrentlySelected) { // 选中当前项 emit("update:modelValue", props.value); } else { // 取消选中当前项 // 如果不允许为空且当前只有这一项被选中,则阻止取消操作 if (!props.null && currentValue.length === 1) { return; // 阻止取消唯一选中项 } emit("update:modelValue", ""); } } nextTick(() => { // 触发验证 if (window.ssVm) { window.ssVm.validateField(props.name); } }); }; return { isChecked, toggleSelect }; }, render() { const SsFormIcon = resolveComponent("ss-form-icon"); return h("div", { class: "radio-container2" }, [ // 隐藏的表单元素 this.multiple ? h("input", { type: "hidden", name: `${this.name}`, // 多选模式下使用数组形式的 name value: this.isChecked ? this.value : "", }) : this.isChecked && h("input", { // 只有当前按钮被选中时才创建 input type: "hidden", name: this.name, value: this.value, }), // 按钮显示 h( "div", { class: { checked: this.isChecked }, style: { width: this.width }, onClick: this.toggleSelect, }, [ h("span", null, this.label), h("div", { class: "mark" }, [ h(SsFormIcon, { class: this.isChecked ? "form-icon-onoff-checked" : "form-icon-onoff-unchecked", }), ]), ] ), ]); }, }; // ss-textarea const SsTextarea = { name: "SsTextarea", props: { name: { type: String, required: true, }, placeholder: { type: String, default: "请输入", }, defaultValue: [String, Number], modelValue: [String, Number], }, emits: ["update:modelValue"], setup(props, { emit }) { const inputValue = ref(props.modelValue || props.defaultValue || ""); // 监听 modelValue 变化 watch( () => props.modelValue, (newVal) => { inputValue.value = newVal; } ); // 输入事件处理 const onInput = (event) => { const newValue = event.target.value; inputValue.value = newValue; emit("update:modelValue", newValue); // 触发验证 if (window.ssVm) { window.ssVm.validateField(props.name); } }; // 失焦时验证 const onBlur = () => { if (window.ssVm) { window.ssVm.validateField(props.name); } }; return { inputValue, onInput, onBlur }; }, render() { return h("div", { class: "textarea-container" }, [ h("div", { class: "textarea" }, [ // 添加隐藏的 input 用于验证 h("input", { type: "hidden", name: this.name, value: this.inputValue || "", }), h("textarea", { placeholder: this.placeholder, value: this.inputValue, onInput: this.onInput, onBlur: this.onBlur, }), ]), ]); }, }; // ss-editor 富文本编辑器 基于Jodit const SsEditor = { name: "SsEditor", props: { modelValue: { type: String, default: "", }, name: { type: String, default: "", }, url: { type: String, default: "", }, height: { type: [Number, String], default: 400, }, placeholder: { type: String, default: "请输入内容", }, readonly: { type: Boolean, default: false, }, uploadUrl: { type: String, default: "/ulByHttp",//原值为“upload” Ben(20251205) }, param: { type: Object, default: () => ({}), }, }, emits: ["update:modelValue", "ready", "change"], setup(props, { emit }) { const SsEditorIcon = resolveComponent("SsEditorIcon"); const editorRef = ref(null); let editorContent = '';//保存富文本编辑器里面的富文本内容 const uniqueId = "editor-" + Date.now(); const errMsg = Vue.ref(""); let fjid = ref(props.param.button.val); let fjName = props.param.button.desc; let mode = props.param.mode; const validate = () => { if (window.ssVm) { const result = window.ssVm.validateField(props.name); console.log("validate", window.ssVm.validateField(props.name)); errMsg.value = result.valid ? "" : result.message; } }; onMounted(() => { validate(); const editor = Jodit.make(`#${uniqueId}`, { height: props.height, placeholder: props.placeholder, readonly: props.readonly, language: "zh_cn", i18n: { zh_cn: { Link: "链接", URL: "链接", "No follow": "无跟踪", "Class name": "类名", Image: "图片", File: "文件", "Line height": "行高", Alternative: "描述", "Alternative text": "描述", "Lower Alpha": "小写字母", "Upper Alpha": "大写字母", "Upper Roman": "大写罗马数字", "Lower Roman": "小写罗马数字", "Lower Greek": "小写希腊字母", "Lower Letter": "小写字母", "Upper Letter": "大写字母", }, }, showXPathInStatusbar: false, showCharsCounter: false, showWordsCounter: false, allowResizeY: false, toolbarSticky: false, statusbar: false, uploader: { url: props.uploadUrl, format: "json", method: "POST", filesVariableName: function (i) { return "imgs[" + i + "]"; }, headers: { Accept: "application/json", }, prepareData: function (formData) { // 这里可以在发送前处理表单数据 return formData; }, isSuccess: function (resp) { console.log("isSuccess resp:", resp); return resp.code === 0; }, getMessage: function (resp) { console.log("getMessage resp:", resp); return resp.msg || "上传失败"; }, process: function (resp) { console.log("process resp:", resp); return resp.data.url; }, error: function (e) { console.error("上传失败:", e.message); }, defaultHandlerSuccess: function (resp) { console.log("上传成功:", resp); }, defaultHandlerError: function (err) { console.error("上传错误:", err); }, contentType: function (requestData) { // 可以根据需要修改 Content-Type return false; // 让浏览器自动设置 }, }, // 自定义字体列表 controls: { font: { list: { Arial: "Arial", SimSun: "宋体", SimHei: "黑体", "Microsoft YaHei": "微软雅黑", KaiTi: "楷体", FangSong: "仿宋", "Times New Roman": "Times New Roman", "Courier New": "Courier New", }, }, customLinkButton: { name: "link", tooltip: "附件", exec: function (editor) { // 按钮点击时的处理函数 console.log("附件点击了"); console.log("param", props.param); console.log("cmsAddUrl",props.param.button.cmsAddUrl); if (fjid.value == null || fjid.value == "") { $.ajax({ type: "post", url: props.param.button.cmsAddUrl, async: false, data: { name: "fjid", ssNrObjName: "sh", ssNrObjId: "", }, success: function (_fjid) { console.log("cmsAddUrl success", _fjid); fjid.value = _fjid; }, }); } var str = "&nrid=T-" + fjid.value + "&objectId=" + fjid.value + "&objectName=" + fjName + "&callback=" + (window["fjidCallbackName"] || ""); console.log("str", str); SS.openDialog({ src: props.param.button.cmsUpdUrl + str, headerTitle: "编辑", width: 900, high: 664, zIndex:51 }); // ss.display.showComponent({ // show: ["wdDialog"], // url: props.param.button.cmsUpdUrl + str, // title: "编辑", // width: 900, // high: 664, // }); }, }, }, toolbarAdaptive: true, buttons: [ "fullsize", "bold", "italic", "underline", "strikethrough", "eraser", "|", "font", "fontsize", "brush", "paragraph", "|", "left", "center", "right", "justify", "|", "ul", "ol", "indent", "outdent", "|", "image", "table", "customLinkButton", "print", "|", "undo", "redo", "find", ], // 中等宽度时显示的按钮 buttonsMD: [ "fullsize", "bold", "italic", "underline", "strikethrough", "eraser", "|", "font", "fontsize", "brush", "paragraph", "|", "font", "fontsize", "|", "left", "center", "right", "justify", "|", "image", "customLinkButton", "|", "dots", // 其余按钮会自动进入 dots 菜单 ], // 小屏幕时显示的按钮 buttonsSM: ["fullsize", "bold", "italic", "|", "image", "|", "dots"], // 超小屏幕时显示的按钮 buttonsXS: ["fullsize", "bold", "|", "dots"], // 设置响应式断点 sizeLG: 1024, // 大屏幕 sizeMD: 768, // 中等屏幕 sizeSM: 576, // 小屏幕 // 自定义图标 getIcon: function (name, clearName) { // 定义图标映射 const iconMap = { bold: "editor-icon-bold", italic: "editor-icon-italic", underline: "editor-icon-underline", strikethrough: "editor-icon-strikethrough", eraser: "editor-icon-eraser", copyformat: "editor-icon-copyformat", font: "editor-icon-font", fontsize: "editor-icon-fontsize", brush: "editor-icon-brush", paragraph: "editor-icon-paragraph", left: "editor-icon-align-left", center: "editor-icon-align-center", right: "editor-icon-align-right", justify: "editor-icon-align-justify", ul: "editor-icon-ul", ol: "editor-icon-ol", indent: "editor-icon-indent", outdent: "editor-icon-outdent", image: "editor-icon-image", file: "editor-icon-file", video: "editor-icon-video", table: "editor-icon-table", link: "editor-icon-link", source: "editor-icon-source", eye: "editor-icon-preview", fullsize: "editor-icon-fullsize", shrink: "editor-icon-fullsize-exit", // 添加退出全屏图标 print: "editor-icon-print", undo: "editor-icon-undo", redo: "editor-icon-redo", search: "editor-icon-find", selectall: "editor-icon-selectall", }; // 获取对应的图标类名 const iconClass = iconMap[clearName] || iconMap[name]; if (iconClass) { // 返回带有我们自定义图标类的 span 元素 return ``; } return null; }, }); // 设置初始值 editor.value = editorContent; // editor.value = props.modelValue; // 监听变化 editor.events.on("change", () => { // emit("update:modelValue", editor.value); editorContent = editor.value; // alert('editorContent:'+editorContent); let contentElements = document.getElementsByName(props.name.replace(/wj$/, "") + "Edit"); if(contentElements.length>0){ contentElements[0].value = editorContent; } emit("change", editor.value); setTimeout(() => { validate(); }, 50); }); // 保存编辑器实例 editorRef.value = editor; emit("ready", editor); //回显编辑器富文本文件 if (props.url ) { const params = new URLSearchParams(); if(mode) params.append("mode", mode); if(props.modelValue) params.append("path", props.modelValue); // alert('props.url:'+props.url+',props.modelValue:'+props.modelValue); axios .post(props.url, params, { headers: { "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置! }, }) .then((response) => { // alert(JSON.stringify(response.data)); if ("timeout" == response.data.statusText) { alert("网络超时!"); return; } let content = response.data.content; if (content) { // editor.value = content; editorRef.value.value = content; editorContent = content; // alert('editor.value:'+editor.value) } let filePath = response.data.path; // alert('response.data:'+JSON.stringify(response.data)); if(filePath){ props.modelValue = filePath; emit("update:modelValue", filePath); } }); } }); // 监听值变化 watch( // () => props.modelValue, () => editorContent, (newValue) => { if (editorRef.value && newValue !== editorRef.value.value) { editorRef.value.value = newValue || ""; } } ); // 监听只读状态变化 watch( () => props.readonly, (newValue) => { if (editorRef.value) { editorRef.value.setReadOnly(newValue); } } ); // 组件销毁时清理 onBeforeUnmount(() => { if (editorRef.value) { editorRef.value.destruct(); } }); return () => h("div", { class: "ss-editor-container" }, [ fjid.value && h("input", { type: "hidden", name: "fjid", value: fjid.value, }), h("input", { type: "hidden", name: props.name.replace(/wj$/, "") + "Edit", value: editorContent // value: props.modelValue, }), h("input", { type: "hidden", name: props.name.replace(/wj$/, "") + "wj", value: props.modelValue // value: props.url }), h("input", { type: "hidden", name: "ueditorpath", value: "mswj" }), h("textarea", { id: uniqueId }), ]); }, }; // 弹窗右边图标 const SsFullStyleHeader = { name: "SsFullStyleHeader", props: { title: { type: String, default: "标题", }, }, emits: ["close"], setup(props, { emit }) { // console.log(props.title) const onClose = () => { emit("close"); }; const SsIcon = resolveComponent("ss-icon"); return () => h("div", { class: "header-container" }, [ h("div", { class: "title" }, props.title), h("div", { class: "handle-bar" }, [ h("div", { class: "left-bar" }, [ h(SsDialogIcon, { class: "dialog-icon-download" }), h(SsDialogIcon, { class: "dialog-icon-print" }), h(SsDialogIcon, { class: "dialog-icon-setting" }), h(SsDialogIcon, { class: "dialog-icon-collect" }), h(SsDialogIcon, { class: "dialog-icon-help" }), h(SsDialogIcon, { class: "dialog-icon-full-screen" }), h(SsDialogIcon, { class: "dialog-icon-lock" }), ]), h("div", { class: "close-bar", onClick: onClose }, [ h(SsDialogIcon, { class: "dialog-icon-close" }), ]), ]), ]); }, }; // ss-dialog弹窗 const SsDialog = { name: "SsDialog", props: { src: { type: String, }, headerTitle: { type: String, // required: true, default: "弹窗", }, width: { type: String, default: "1400", }, height: { type: String, default: "600", }, params: { type: Object, default: () => ({}), }, zIndex: { type: Number, default: 1000, }, }, emits: ["close"], setup(props, { slots, emit }) { // 关闭窗口方法 const onClose = () => { emit("close"); }; const showHeader = ref(true); const headerVisible = ref(false); const popupHieght = ref(props.height); // 状态:存储位置信息 const position = reactive({ // 页面居中 x: (window.innerWidth - props.width) / 2, y: (window.innerHeight - popupHieght.value) / 2, isDragging: false, offsetX: 0, offsetY: 0, }); // 鼠标按下时设置起始坐标并开始拖拽 const startDrag = (event) => { position.isDragging = true; position.offsetX = event.clientX - position.x; position.offsetY = event.clientY - position.y; }; // 鼠标移动时更新位置 const onDrag = (event) => { if (position.isDragging) { position.x = event.clientX - position.offsetX; position.y = event.clientY - position.offsetY; } }; // 鼠标放开时结束拖拽 const endDrag = () => { position.isDragging = false; }; // 监听来自 iframe 的消息 const handleMessage = (event) => { // 顶天立地 if (event.data && typeof event.data.hasScrollBar !== "undefined") { if (event.data.hasScrollBar) { // console.log(event); position.y = 10; showHeader.value = false; headerVisible.value = true; popupHieght.value = window.innerHeight - 20; // console.log(popupHieght.value); document.querySelector(".body").style.height = "100%"; document.querySelector(".body").style.paddingTop = "30px"; document.querySelector(".header-container ").style.position = "absolute"; document.querySelector(".header-container ").style.zIndex = "10"; } } }; // 鼠标移入关闭按钮区域时显示头部 const onMouseEnterCloseButton = () => { headerVisible.value = false; }; // 鼠标移出关闭按钮区域时隐藏头部 const onMouseLeaveCloseButton = () => { headerVisible.value = true; }; // 在组件挂载时添加全局事件监听器 Vue.onMounted(() => { // 如果传过来的高度大于窗口高度,则设置为窗口高度减去20 否则保持传过来的高度 popupHieght.value = popupHieght.value > window.innerHeight ? window.innerHeight - 20 : popupHieght.value; const container = document.querySelector(".header-container"); if (container) { container.addEventListener("mousedown", startDrag); } document.addEventListener("mousemove", onDrag); document.addEventListener("mouseup", endDrag); window.addEventListener("message", handleMessage); }); // 在组件卸载时移除全局事件监听器 Vue.onUnmounted(() => { document.removeEventListener("mousemove", onDrag); document.removeEventListener("mouseup", endDrag); window.removeEventListener("message", handleMessage); }); const SsMark = resolveComponent("ss-mark"); const SsFullStyleHeader = resolveComponent("ss-full-style-header"); // render函数定义组件结构 return () => h( Teleport, { to: "body" }, // 使用 Teleport 将弹窗内容挂载到 body h(SsMark, { }, [ h( "div", { class: "popup-container", style: { position: "absolute", left: `${position.x}px`, top: `${position.y}px`, width: props.width + "px", height: popupHieght.value + "px", zIndex: props.zIndex, // 确保弹窗在最上层 }, }, [ h(SsFullStyleHeader, { class: "header", title: props.headerTitle, onClose: onClose, onMousedown: startDrag, // 绑定拖动事件 onMouseUp: endDrag, ...(!showHeader.value && { onMouseenter: onMouseEnterCloseButton, onMouseleave: onMouseLeaveCloseButton, }), style: { cursor: position.isDragging ? "grabbing" : "grab", visibility: headerVisible.value ? "hidden" : "visible", }, }), h( "div", { class: "body", style: {}, }, [ h("iframe", { src: props.src, frameborder: 0, style: { width: "100%", height: "100%" }, }), ] ), headerVisible.value && h("div", { class: "close-button", onMouseenter: onMouseEnterCloseButton, onMouseleave: onMouseLeaveCloseButton, style: { position: "absolute", top: "0", right: "0", // background: 'black', width: "60px", height: "60px", cursor: "pointer", }, }), ] ), ]) ); }, }; // ss-mark遮罩层 const SsMark = { name: "SsMark", setup(props, { slots, emit }) { return () => h("div", { class: "dialog-container" }, [ h("div", { class: "mark-content" }, [ h("div", { class: "dialog-contianer" }, [ slots.default ? slots.default() : "", ]), ]), ]); }, }; // ss-bottom-button 底部按钮 // 修改支持更多按钮 by xu 20251211 const SsBottomButton = { name: "SsBottomButton", props: { text: { type: String, required: false, }, type: { type: String, default: "button", }, iconClass: { type: String, }, class: { type: String, default: "", }, onclick: { type: [Function, String], default: null, }, // 修改支持更多按钮 by xu 20251211 more: { type: [Boolean, String], default: false, }, }, setup(props, { emit }) { const SsBottomDivIcon = Vue.resolveComponent("ss-bottom-div-icon"); const showDropdown = Vue.ref(false); // 修改支持更多按钮 by xu 20251211 const moreKey = Vue.computed(() => { const val = props.more; if (val === false || val === null || typeof val === "undefined") { return null; } if (val === true || val === "" || val === "true") { return "moreChg"; } return val; }); // 从配置中读取按钮信息和下拉选项 const config = Vue.computed(() => { if (moreKey.value && window.ss && window.ss.dom && window.ss.dom.btnElemConfig) { return window.ss.dom.btnElemConfig[moreKey.value] || {}; } return {}; }); const buttonText = Vue.computed(() => { return props.text || config.value.desc || ''; }); const dropOptions = Vue.computed(() => { return config.value.dropOptions || []; }); const hasDropdown = Vue.computed(() => { return dropOptions.value.length > 0; }); const handleMouseEnter = () => { if (hasDropdown.value) { showDropdown.value = true; } }; const handleMouseLeave = () => { showDropdown.value = false; }; const handleDropItemClick = (option) => { if (option.callback && typeof option.callback === 'function') { option.callback(); } showDropdown.value = false; }; return () => h( "div", { class: "ss-bottom-button-wrapper", onMouseenter: handleMouseEnter, onMouseleave: handleMouseLeave, }, [ h( "button", { class: props.class, onClick: (e) => { e.stopPropagation(); if (props.onclick) { // 如果是函数直接调用 if (typeof props.onclick === "function") { props.onclick(e); } else if (typeof props.onclick === "string") { // 如果是字符串,使用直接的方法执行 // 临时存储按钮元素到全局变量 window.__ss_current_button = e.currentTarget; // 直接执行代码,使用eval以保留原始上下文 try { eval(props.onclick); } finally { // 清理全局变量 delete window.__ss_current_button; } } } }, type: props.type, }, [ h("span", null, [ h(SsBottomDivIcon, { class: props.iconClass, }), ]), h("span", null, buttonText.value), ] ), // 渲染下拉菜单 hasDropdown.value && showDropdown.value ? h( "div", { class: "ss-bottom-button-dropdown", }, dropOptions.value.map((option) => h( "div", { class: "ss-bottom-button-dropdown-item", onClick: (e) => { e.stopPropagation(); handleDropItemClick(option); }, }, option.desc ) ) ) : null, ] ); }, }; // ss-search搜索框 const SsSearch = { name: "SsSearch", props: { theme: { type: String, default: "light", validator: function (value) { return ["dark", "light"].includes(value); }, }, placeholder: { type: String, default: "请输入搜索条件", }, }, setup(props, { emit }) { const onClick = () => { console.log("Search clicked"); emit("click"); }; const SsIcon = Vue.resolveComponent("ss-icon"); return () => Vue.h( "div", { class: ["search-container", props.theme], onClick: onClick, }, [ Vue.h("input", { placeholder: props.placeholder, disabled: true, }), Vue.h(SsIcon, { name: "search-result", size: "20px", }), ] ); }, }; // ss-cart-item 菜单页面的卡片 左右结构 const SsCartItem = { name: "SsCartItem", props: { active: Boolean, item: { type: Object, default: () => ({ thumb: "images/example/project-img.png", title: "广州(国际)科技成果转化天河基地专", description: "佳能中国广州分公司", all: 50, finish: 5, }), }, }, setup(props, { emit }) { const item = props.item; const itemWidth = Vue.computed(() => { const containerWidth = document.body.clientWidth || document.body.scrollWidth - 520; const halfWidth = containerWidth / 2; if (halfWidth < 480) { return Math.min(containerWidth, 702) + "px"; } else { return Math.min(halfWidth, 702) + "px"; } }); const onItemClick = (e) => { emit("click", e); }; return { item, itemWidth, onItemClick, }; }, render() { const SsIcon = Vue.resolveComponent("ss-icon"); return Vue.h( "div", { class: { "item-container": true, active: this.active }, onClick: this.onItemClick, style: { width: this.itemWidth }, }, [ Vue.h("div", { class: "header" }, [ Vue.h(SsIcon, { name: "setting", size: "20px" }), ]), Vue.h("div", { class: "body" }, [ Vue.h("div", { class: "left" }, [ Vue.h("img", { src: this.item.thumb, alt: "Thumbnail", class: "imgUnHandle", style: { "object-fit": "cover", width: "100%", height: "100%" }, }), ]), Vue.h("div", { class: "right" }, [ Vue.h("div", { class: "title" }, this.item.title), Vue.h("div", { class: "desc" }, this.item.description), Vue.h("div", { class: "progress" }, [ Vue.h( "div", { style: { width: `${(this.item.finish / this.item.all) * 100}%`, }, }, [Vue.h("div", `${this.item.finish}/${this.item.all}`)] ), ]), ]), ]), ] ); }, }; // ss-cart-item2 菜单页面的卡片 上下结构 const SsCartItem2 = { name: "SsCartItem2", props: { active: Boolean, item: { type: Object, default: () => ({ thumb: "images/example/project-img.png", title: "广州(国际)科技成果转化天河基地专", description: "佳能中国广州分公司", all: 50, finish: 5, }), }, }, setup(props, { emit }) { const item = props.item; const itemWidth = Vue.computed(() => { const containerWidth = document.body.clientWidth || document.body.scrollWidth - 520; const halfWidth = containerWidth / 2; if (halfWidth < 480) { return Math.min(containerWidth, 702) + "px"; } else { return Math.min(halfWidth, 702) + "px"; } }); const onItemClick = (e) => { emit("click", e); }; return { item, itemWidth, onItemClick, }; }, render() { const SsIcon = Vue.resolveComponent("ss-icon"); return Vue.h( "div", { class: { "item-container2": true, active: this.active }, onClick: this.onItemClick, style: { width: this.itemWidth }, }, [ Vue.h("div", { class: "action-bar" }, [ Vue.h(SsIcon, { name: "setting", size: "20px" }), ]), Vue.h("div", { class: "header" }, [ Vue.h("div", { class: "title" }, `${this.item.title}`), ]), Vue.h("div", { class: "body" }, [ Vue.h("div", { class: "left" }, [ Vue.h("img", { src: this.item.thumb, alt: "Thumbnail", class: "imgUnHandle", style: { "object-fit": "cover", width: "100%", height: "100%" }, }), ]), Vue.h("div", { class: "right" }, [ Vue.h("div", { class: "content" }, this.item.description), Vue.h("div", { class: "tip" }, [ Vue.h("div", { class: "progress" }, [ Vue.h( "div", { style: { width: `${(this.item.finish / this.item.all) * 100}%`, }, }, [Vue.h("div", `${this.item.finish}/${this.item.all}`)] ), ]), ]), ]), ]), ] ); }, }; // ss-list-card 通用列表卡片 const SsListCard = { name: "SsListCard", props: { item: { type: Object, required: true, }, }, emits: ["click", "change"], setup(props, { emit }) { const item = props.item; const itemWidth = Vue.computed(() => { const containerWidth = document.body.clientWidth || document.body.scrollWidth; if (containerWidth > 1200) { return "30%"; } return "45%"; }); const onItemClick = (e) => { // 清除所有类型卡片的 active 状态 const allListCards = document.querySelectorAll( ".knowledge-item-container" ); const allFolderCards = document.querySelectorAll(".ss-folder-list"); allListCards.forEach((card) => card.classList.remove("active")); allFolderCards.forEach((card) => card.classList.remove("active")); // 设置当前项的 active 状态 e.currentTarget.classList.add("active"); props.item.onclick(); }; const onItemChange = (e, icon, index) => { e.stopPropagation(); // 阻止事件冒泡到卡片 props.item.buttons[0].onclick(); // emit("change", { item: props.item, icon, index }); }; return { item, itemWidth, onItemClick, onItemChange, }; }, data() { return { showButtons: false, }; }, render() { const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon"); return Vue.h( "div", { class: { "knowledge-item-container": true, active: this.item.active }, onClick: this.onItemClick, style: { width: this.itemWidth }, }, [ this.item?.buttons?.length > 0 && Vue.h( "div", { class: "header", onMouseenter: () => (this.showButtons = true), onMouseleave: () => (this.showButtons = false), onClick: (e) => this.onItemChange(e, this.item.buttons[0], 0), }, [ // 只在有按钮时渲染设置图标 // this.item?.buttons?.length > 0 && Vue.h("div", { class: "cart-list-setting cart-list-icon", title: this.item?.buttons?.[0]?.title, }), // 鼠标移入时显示按钮列表,与图标同级 // this.item?.buttons?.length > 0 && this.showButtons && this.item?.buttons?.length > 1 && Vue.h( "div", { class: "cart-list-button-popup", }, this.item.buttons.map((btn) => Vue.h( "div", { onClick: (e) => { e.stopPropagation(); btn.onclick?.(); }, }, [ // 如果有 class,显示对应的图标 btn.class && Vue.h(SsCartListIcon, { class: [btn.class], }), // 显示按钮文本 Vue.h("span", null, btn.title), ] ) ) ), ] ), Vue.h("div", { class: "body" }, [ Vue.h("div", { class: "box-header" }, [ Vue.h("div", `${this.item.title}`), ]), Vue.h( "div", { class: !this.item.thumb ? "no-thumb box-body" : "box-body", }, [ this.item.thumb ? Vue.h("div", { class: "left" }, [ Vue.h("img", { src: this.item.thumb, alt: "Thumbnail", class: "imgUnHandle", style: { "object-fit": "cover", width: "100%", height: "100%", }, }), ]) : null, Vue.h("div", { class: "right" }, [ ...this.item.tags.map((tag) => { const [key, value] = Object.entries(tag)[0]; return Vue.h( "div", { class: "title", title: `${key}: ${value}`, }, `${key}: ${value}` ); }), ]), ] ), ]), ] ); }, }; // ss-folder-card 文件夹卡片 const SsFolderCard = { name: "SsFolderCard", props: { item: { type: Object, required: true, }, }, data() { return { showButtons: false, }; }, emits: ["click", "change"], setup(props, { emit }) { const item = props.item; const showChildren = ref(false); const eventBus = window.parent.sharedEventBus; const itemWidth = Vue.computed(() => { const containerWidth = document.body.clientWidth || document.body.scrollWidth; return containerWidth > 1200 ? "45%" : "90%"; }); onMounted(() => { eventBus.subscribe("folderPath", (path) => { const currentPath = path || []; // 如果当前文件夹不在路径中,则销毁视图 if ( !currentPath.some((item) => item.folder.title === props.item.title) ) { showChildren.value = false; } }); }); const onItemClick = (e) => { if (e && e.stopPropagation) { e.stopPropagation(); } // 单击只处理 active 状态 if (e && e.currentTarget) { const allListCards = document.querySelectorAll( ".knowledge-item-container" ); const allFolderCards = document.querySelectorAll(".ss-folder-list"); allListCards.forEach((card) => card.classList.remove("active")); allFolderCards.forEach((card) => card.classList.remove("active")); e.currentTarget.classList.add("active"); } else { // 如果是数据对象,需要找到对应的 DOM 元素 const allListCards = document.querySelectorAll( ".knowledge-item-container" ); const allFolderCards = document.querySelectorAll(".ss-folder-list"); allListCards.forEach((card) => card.classList.remove("active")); allFolderCards.forEach((card) => card.classList.remove("active")); // 找到标题匹配的文件夹元素 const targetFolder = Array.from(allFolderCards).find((card) => card.textContent.includes(e.title) ); if (targetFolder) { targetFolder.classList.add("active"); } } emit("click", item); }; // 修改双击处理函数 const handleFolderDblClick = (folder, e) => { if (e) e.stopPropagation(); if (folder.children?.length) { showChildren.value = true; const pathInfo = { title: folder.title, folder: folder, }; const currentPath = eventBus.getState("folderPath") || []; if (!currentPath.some((item) => item.title === folder.title)) { eventBus.publish("folderPath", [...currentPath, pathInfo]); } } }; const onItemChange = (e, icon, index) => { e.stopPropagation(); props.item.buttons[0].onclick(); // emit("change", { item: props.item, icon, index }); }; return { item, itemWidth, showChildren, onItemClick, onItemChange, handleFolderDblClick, }; }, render() { const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon"); if (this.showChildren) { return h(SsFolderCartView, { folder: this.item, }); } return Vue.h( "div", { class: { "ss-folder-list": true, active: this.item.active }, onClick: (e) => { e.stopPropagation(); this.onItemClick(e); }, onDblclick: (e) => this.handleFolderDblClick(this.item, e), style: { width: this.itemWidth }, }, [ // 文件夹特有的装饰元素 Vue.h("div", { class: "ss-folder-list-trapezoid" }), Vue.h("div", { class: "ss-folder-list-top-transparent" }), Vue.h("div", { class: "ss-folder-list-top" }), Vue.h("div", { class: "ss-folder-list-right" }), // header 部分(按钮) this.item?.buttons?.length > 0 && Vue.h( "div", { class: "header", onMouseenter: () => (this.showButtons = true), onMouseleave: () => (this.showButtons = false), onClick: (e) => this.onItemChange(e, this.item.buttons[0], 0), }, [ // this.item?.buttons?.length > 0 && Vue.h("div", { class: "cart-list-setting cart-list-icon", title: this.item?.buttons?.[0]?.title, }), // this.item?.buttons?.length > 0 && this.showButtons && this.item?.buttons?.length > 1 && Vue.h( "div", { class: "cart-list-button-popup", }, this.item.buttons.map((btn) => Vue.h( "div", { onClick: (e) => { e.stopPropagation(); btn.onclick?.(); }, }, [ btn.class && Vue.h(SsCartListIcon, { class: [btn.class], }), Vue.h("span", null, btn.title), ] ) ) ), ] ), // body 部分 Vue.h("div", { class: "body" }, [ Vue.h("div", { class: "box-header" }, [ Vue.h("div", null, this.item.title), ]), Vue.h( "div", { class: !this.item.thumb ? "no-thumb box-body" : "box-body", }, [ this.item.thumb ? Vue.h("div", { class: "left" }, [ Vue.h("img", { src: this.item.thumb, alt: "Thumbnail", class: "imgUnHandle", style: { "object-fit": "cover", width: "100%", height: "100%", }, }), ]) : null, Vue.h("div", { class: "right" }, [ ...this.item.tags.map((tag) => { const [key, value] = Object.entries(tag)[0]; return Vue.h( "div", { class: "title", title: `${key}: ${value}`, }, `${key}: ${value}` ); }), ]), ] ), ]), ] ); }, }; // SsFolderCartView 组件 - 用于显示文件夹内容 const SsFolderCartView = { name: "SsFolderCartView", props: { folder: { type: Object, required: true, }, }, emits: ["click"], setup(props, { emit }) { const eventBus = window.parent.sharedEventBus; const currentFolder = ref(props.folder); const showChildren = ref(false); const onItemClick = (e) => { if (e && e.stopPropagation) { e.stopPropagation(); } // 单击只处理 active 状态 if (e && e.currentTarget) { const allListCards = document.querySelectorAll( ".knowledge-item-container" ); const allFolderCards = document.querySelectorAll(".ss-folder-list"); allListCards.forEach((card) => card.classList.remove("active")); allFolderCards.forEach((card) => card.classList.remove("active")); e.currentTarget.classList.add("active"); } else { // 如果是数据对象,需要找到对应的 DOM 元素 const allListCards = document.querySelectorAll( ".knowledge-item-container" ); const allFolderCards = document.querySelectorAll(".ss-folder-list"); allListCards.forEach((card) => card.classList.remove("active")); allFolderCards.forEach((card) => card.classList.remove("active")); // 找到标题匹配的文件夹元素 const targetFolder = Array.from(allFolderCards).find((card) => card.textContent.includes(e.title) ); if (targetFolder) { targetFolder.classList.add("active"); } } emit("click", props.folder); }; const handleFolderDblClick = (folder, e) => { if (e) e.stopPropagation(); if (folder.children?.length) { showChildren.value = true; const pathInfo = { title: folder.title, folder: folder, }; const currentPath = eventBus.getState("folderPath") || []; if (!currentPath.some((item) => item.title === folder.title)) { eventBus.publish("folderPath", [...currentPath, pathInfo]); currentFolder.value = folder; } } }; const goBack = (targetFolder) => { if (targetFolder === null) { // 返回根目录 eventBus.publish("folderPath", []); } else { currentFolder.value = targetFolder; } }; return { currentFolder, showChildren, onItemClick, handleFolderDblClick, goBack, }; }, render() { return h( "div", { class: "page-container", style: { position: "fixed", top: 0, left: 0, width: "100%", height: "100%", background: "var(--lightgray)", padding: "20px 0", zIndex: 1000, }, }, [ // 搜索栏 h("div", { class: "search-bar" }, [ h("div", { class: "search-bar-contaienr" }, [ h(SsBreadcrumb, { level: { onBack: this.goBack, }, }), ]), ]), // 内容区域 h( "div", { class: "content-area item-content-area", style: { gap: "20px" }, }, [ ...(this.currentFolder.children || []).map((child, index) => h(child.children ? SsFolderCard : SsListCard, { key: index, item: child, onClick: (e) => this.onItemClick(e), onDblclick: (e) => this.handleFolderDblClick(child, e), }) ), ] ), ] ); }, }; // ss-page分页 const SsPage = { name: "SsPage", props: { total: { type: Number, required: true, }, size: { type: Number, default: 10, }, page: { type: Number, default: 1, }, onChange: { type: Function, default: () => {}, }, }, setup(props) { const totalItems = ref(props.total); // 总条目数 const totalPages = ref(Math.ceil(props.total / props.size)); const currentPage = ref(props.page); // 当前页码 // 计算显示的信息 const pageInfo = ref( `共${totalItems.value}条,第 ${currentPage.value}/${totalPages.value} 页` ); // 上一页的逻辑 const goToPreviousPage = (e) => { e.preventDefault(); // 阻止默认行为 if (currentPage.value > 1) { currentPage.value -= 1; updatePageInfo(); props.onChange?.({ pageNo: currentPage.value, // 当前页码 rowNumPer: props.size, // 每页条数 rowNum: props.total, // 总记录数 }); } }; // 下一页的逻辑 const goToNextPage = (e) => { e.preventDefault(); // 阻止默认行为 if (currentPage.value < totalPages.value) { currentPage.value += 1; updatePageInfo(); props.onChange?.({ pageNo: currentPage.value, // 当前页码 rowNumPer: props.size, // 每页条数 rowNum: props.total, // 总记录数 }); } }; // 更新页码信息的函数 const updatePageInfo = () => { pageInfo.value = `共${totalItems.value}条,第 ${currentPage.value}/${totalPages.value} 页`; }; return { pageInfo, totalPages, goToPreviousPage, goToNextPage, }; }, render(props, { slots, emit }) { return Vue.h("div", { class: "pager-container" }, [ Vue.h("input", { type: "hidden", name: "rowNum", value: props.total }), Vue.h("input", { type: "hidden", name: "rowNumPer", value: props.size, }), Vue.h("input", { type: "hidden", name: "pageCount", value: this.totalPages, }), Vue.h("input", { type: "hidden", name: "pageNo", value: props.page }), Vue.h("div", { class: "pager-content" }, [ Vue.h("div", { class: "info" }, this.pageInfo), Vue.h( "div", { class: "btn" }, Vue.h( "button", { onClick: (e) => this.goToPreviousPage(e) }, "上一页" ) ), Vue.h( "div", { class: "btn" }, Vue.h("button", { onClick: (e) => this.goToNextPage(e) }, "下一页") ), ]), ]); }, }; // ss-right-info 一级页面右边栏 const SSRightInfo = { name: "SSRightInfo", setup() { // 初始化响应式数据 const item = ref({ thumb: "images/example/project-img.png", // 更换为适合你项目的实际路径 title: "工业和信息化产业高质量发展资金", }); return { item, }; }, render() { return Vue.h("div", { class: "info-container" }, [ Vue.h("div", { class: "header" }, [ Vue.h("div", [ Vue.h("img", { src: this.item.thumb, class: "imgUnHandle", style: { "object-fit": "cover", width: "100%", height: "100%" }, }), // 将 ImageViewer 替换为 img 标签 ]), Vue.h("div", [Vue.h("div", this.item.title)]), ]), Vue.h("div", { class: "section-container" }, [ Vue.h("div", { class: "section" }, [ Vue.h("div", { class: "title" }, "合同"), Vue.h("div", { class: "text" }, "合同总金额:42,399,320"), Vue.h( "div", { class: "a" }, "《工业和信息化产业高质量发展资金补助合同》" ), ]), Vue.h("div", { class: "section" }, [ Vue.h("div", { class: "title" }, "发票"), Vue.h("div", { class: "text" }, "应开发票总额:42,399,320"), Vue.h("div", { class: "text" }, "已开发票金额:17,235,345"), Vue.h("div", { class: "text" }, "未开发票金额:25,163,975"), ]), Vue.h("div", { class: "section" }, [ Vue.h("div", { class: "title" }, "项目组成员"), Vue.h("div", { class: "text" }, "我司:3人"), Vue.h("div", { class: "text" }, "对方:2人"), Vue.h("div", { class: "text" }, "项目负责人:张三"), ]), Vue.h("div", { class: "section" }, [ Vue.h("div", { class: "title" }, "采购"), Vue.h("div", { class: "text" }, "总额:999,320"), Vue.h("div", { class: "text" }, "已付金额:335,345"), Vue.h("div", { class: "text" }, "未付金额:663,975"), ]), ]), ]); }, }; // const SsSuccessPopup = { name: "SsSuccessPopup", props: { right: { type: String, default: "20px", }, bottom: { type: String, default: "calc(100% + 5px)", }, }, setup(props, { expose }) { // 响应式状态:是否可见 const visible = ref(false); // 计算样式 const style = computed(() => { return { "--message-dialog-right": props.right, "--message-dialog-bottom": props.bottom, }; }); // 显示对话框的方法 const show = () => { visible.value = true; }; // 隐藏对话框的方法 const hide = () => { visible.value = false; }; // 将方法暴露给外部使用 expose({ show, hide }); // 返回渲染函数 return () => { if (!visible.value) return null; const SsIcon = resolveComponent("ss-icon"); return h( "div", { class: "success-popup", style: style.value, onClick: (e) => e.stopPropagation(), }, [ h("div", { class: "left" }, [ h("div", { class: "icon" }, [ h(SsIcon, { name: "check", size: "36px" }), ]), ]), h("div", { class: "right" }, [ h("div", { class: "title" }, "提交成功"), h("div", { class: "desc" }, "您的信息已成功提交。"), ]), ] ); }; }, }; const SsErrorDialog = { name: "SsErrorDialog", setup(props, { emit }) { const visible = ref(false); const style = computed(() => { return {}; }); const show = () => { visible.value = true; }; const hide = () => { visible.value = false; }; const onBack = () => { emit("back"); hide(); }; return { visible, style, show, hide, onBack, }; }, render() { const SsIcon = resolveComponent("ss-icon"); return this.visible ? h( "div", { class: "errorDialog", style: this.style, onClick: (event) => event.stopPropagation(), }, [ h("div", { class: "body" }, [ h("div", { class: "left" }, [ h("div", { class: "icon" }, [ h(SsIcon, { name: "close", size: "36px" }), ]), ]), h("div", { class: "right" }, [ h("div", { class: "title" }, "操作失败"), h("div", { class: "desc" }, "请点击返回以继续。"), ]), ]), h("div", { class: "footer" }, [ h("div", { class: "left" }), h("div", { class: "right" }, [ h( "div", { class: "btn", onClick: this.onBack, }, [h(SsIcon, { name: "arrow-left-line" }), h("div", "返回")] ), ]), ]), ] ) : null; }, }; /** * 审核链条 * @name ss-verify * @param { Array } verify-list 审核节点列表 * @property { Array } verify-list 审核节点列表 * @example * verify-list [ * { * groupName: "", // 群组名称 * open: true, // 默认是否展开 * children:[ //群组里的人员 * { * thumb: "images/example/user-4.png", // 头像 * name: "李丽思 ", // 姓名 * role: "人事处处长", // 角色 * description: "同意。", // 审核意见 * time: "09:38 08/11", // 审核时间 * video: false, // false不显示/true显示 视频icon * link: false, // false不显示/true显示 链接icon 后续应该是附件 * } * ] * } * ] */ const SsVerify = { name: "SsVerify", props: { verifyList: { type: Array, required: true, }, }, setup(props) { const toggleOpen = (item) => { item.open = !item.open; }; onMounted(() => { setTimeout(() => { const lastOpenGroup = document.querySelector(".group-item-last-open"); console.log("lastOpenGroup", lastOpenGroup); if (lastOpenGroup) { const nodes = $(lastOpenGroup).find(".verify-node-container"); if (nodes.length) { let totalHeight = 0; const gudingHeight = 100; if (nodes.length === 1) { totalHeight = gudingHeight; } else { // 累加除最后一个节点外的所有节点高度 for (let i = 0; i < nodes.length - 1; i++) { totalHeight += $(nodes[i]).outerHeight(); } totalHeight += gudingHeight; } console.log("节点信息:", { 节点总数: nodes.length, 计算后的高度: totalHeight, }); lastOpenGroup.style.setProperty( "--group-line-height", `${totalHeight}px` ); } } }, 0); }); return { toggleOpen, }; }, render() { const SsIcon = resolveComponent("ss-icon"); const SsCommonIcon = resolveComponent("ss-common-icon"); const SsVerifyNode = resolveComponent("ss-verify-node"); return h( "div", { class: "verify-nodes" }, this.verifyList.map((item, i) => h( "div", { key: i, class: { "group-item": true, "group-item-last-open": i === this.verifyList.length - 1 && item.open, }, }, [ h( "div", { class: "group-item-title", onClick: () => this.toggleOpen(item), }, [ h("div", { class: "icon" }, [ item.open ? h(SsCommonIcon, { class: "common-icon-folder-open" }) : h(SsCommonIcon, { class: "common-icon-folder-close" }), h( "div", { class: "num", style: { top: item.open ? "60%" : "55%" }, }, item.children?.length || 0 ), ]), h("div", { class: "name" }, item.groupName), ] ), item.open && item.children?.length > 0 ? h( "div", { class: "group-item-children" }, item.children.map((citem, j) => h(SsVerifyNode, { key: j, item: citem, // isGroup: i + 1 !== this.verifyList.length, isGroup: true, }) ) ) : null, ] ) ) ); }, }; /** * 审核页面的审核节点 * @name ss-verify-node * @param {Object} item 审核节点信息 * @param {Boolean} isGroup 是否为分组节点 */ const SsVerifyNode = { name: "SsVerifyNode", props: { item: { type: Object, required: true, }, isGroup: { type: Boolean, default: false, }, }, render() { const SsIcon = resolveComponent("ss-icon"); const SsCommonIcon = resolveComponent("ss-common-icon"); return Vue.h("div", { class: "verify-node-container" }, [ Vue.h("div", { class: "info" }, [ Vue.h("div", { class: "avatar" }, [ Vue.h("img", { src: this.item.thumb, style: { width: "50px", height: "50px", borderRadius: "50%", }, }), ]), Vue.h("div", { class: "desc" }, [ Vue.h("div", this.item.name), Vue.h("div", this.item.role), ]), Vue.h("div", { class: "link" }, [ Vue.h("div", [ this.item.video ? Vue.h(SsCommonIcon, { class: "common-icon-video" }) : null, this.item.link ? Vue.h(SsCommonIcon, { class: "common-icon-paper-clip", }) : null, ]), ]), ]), Vue.h( "div", { class: { description: true, link: this.isGroup, }, attrs: { "data-num": "3" }, }, [Vue.h("div", this.item.description)] ), Vue.h("div", { class: "time" }, this.item.time), ]); }, }; /** * 智能识别图片的左侧图片播放 可以放大缩小旋转图片 * @name ss-orc-img-box * @param { Object } image-obj 包含图片的url, 和图片的名称 * */ const SsOrcImgBox = { name: "SsOrcImgBox", props: { imageObj: { type: Object, required: true, }, }, setup(props) { const zoom = ref(1); const rotation = ref(0); const containerWidth = ref(0); const containerHeight = ref(0); const container = ref(null); const imgPosition = ref({ x: 0, y: 0 }); const isDragging = ref(false); const lastMousePosition = ref({ x: 0, y: 0 }); const imgStyle = computed(() => ({ width: `${zoom.value * 100}%`, height: `${zoom.value * 100}%`, transform: `rotate(${rotation.value}deg) translate(${imgPosition.value.x}px, ${imgPosition.value.y}px)`, transformOrigin: "center center", cursor: isDragging.value ? "grabbing" : "grab", })); const resetZoom = () => { zoom.value = 1; rotation.value = rotation.value + 90; imgPosition.value = { x: 0, y: 0 }; }; const handleRangeChange = (event) => { const value = event.target.value / 50; // 0 到 100 映射到 0 到 2 的缩放 zoom.value = Math.max(value, 0.1); // 设置最小缩放值为 0.1 }; const updateImgBoxDimensions = () => { if (container.value) { containerWidth.value = container.value.clientWidth; containerHeight.value = container.value.clientHeight; } }; const onMouseDown = (event) => { isDragging.value = true; lastMousePosition.value = { x: event.clientX, y: event.clientY }; }; const onMouseMove = (event) => { if (isDragging.value) { const dx = event.clientX - lastMousePosition.value.x; const dy = event.clientY - lastMousePosition.value.y; // 防止旋转后拖动的x,y反转 // 首先将当前旋转角度从度数转换为弧度,因为 JavaScript 的 Math 库使用弧度 const angle = rotation.value * (Math.PI / 180); // 使用基本的二维旋转矩阵将原始位移 dx 和 dy 转换为旋转后的位移 rotatedDx 和 rotatedDy。 const rotatedDx = dx * Math.cos(angle) + dy * Math.sin(angle); const rotatedDy = dy * Math.cos(angle) - dx * Math.sin(angle); imgPosition.value = { x: imgPosition.value.x + rotatedDx, y: imgPosition.value.y + rotatedDy, }; lastMousePosition.value = { x: event.clientX, y: event.clientY }; } }; const onMouseUp = () => { isDragging.value = false; }; onMounted(() => { nextTick(() => { updateImgBoxDimensions(); window.addEventListener("resize", updateImgBoxDimensions); window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); }); }); return { zoom, rotation, container, imgStyle, resetZoom, handleRangeChange, containerWidth, containerHeight, onMouseDown, imgPosition, }; }, render() { const SsIcon = resolveComponent("ss-icon"); return h("div", { class: "ocr-img-box" }, [ h("div", { class: "img-bar" }, [ h("div", this.imageObj.name), h("div", { class: "action-bar" }, [ h("div", { class: "ocr-img-range-box" }, [ h("input", { type: "range", min: 0, max: 100, value: this.zoom * 50, // 初始位置为50 onInput: this.handleRangeChange, }), h("span", { class: "line" }), ]), h(SsIcon, { name: "reset", size: "26px", onClick: this.resetZoom, }), ]), ]), h("div", { class: "img-viewer", ref: "container" }, [ h( "div", { class: "img-box", style: { width: `${this.containerWidth}px`, height: `${this.containerHeight}px`, overflow: "hidden", position: "relative", }, }, [ h("img", { src: this.imageObj.thumb, style: this.imgStyle, class: "zoomable-img", onMousedown: this.onMouseDown, }), ] ), ]), ]); }, }; // 搜索输入框组件 const SsSearchInput = { name: "SsSearchInput", props: { name: String, placeholder: String, width: { type: String, default: "100px", }, modelValue: String, }, emits: ["update:modelValue", "search"], setup(props, { emit }) { const handleInput = (e) => { emit("update:modelValue", e.target.value); }; const handleKeyup = (e) => { if (e.key === "Enter") { emit("search"); } }; return { handleInput, handleKeyup }; }, render() { return h( "div", { class: "input", style: this.width ? { width: this.width } : undefined, }, [ h("input", { name: this.name, placeholder: this.placeholder, value: this.modelValue, onInput: this.handleInput, onKeyup: this.handleKeyup, }), ] ); }, }; // ss-search-date-picker 日期时间选择器组件 const SsSearchDatePicker = { name: "SsSearchDatePicker", props: { modelValue: { type: [String, Number, Date], default: "", }, name: { type: String, required: true, }, type: { type: String, default: "date", validator: (value) => ["date", "datetime", "time"].includes(value), }, fmt: { type: String, default: null, }, placeholder: { type: String, default: "", }, width: { type: String, default: "100%", }, }, emits: ["update:modelValue"], setup(props, { emit }) { const errMsg = ref(""); const validate = () => { if (window.ssVm) { const result = window.ssVm.validateField(props.name); console.log("validate", window.ssVm.validateField(props.name)); errMsg.value = result.valid ? "" : result.message; } }; // 根据type确定默认格式 const defaultFormat = computed(() => { switch (props.type) { case "datetime": return "YYYY-MM-DD HH:mm:ss"; case "date": return "YYYY-MM-DD"; case "time": return "HH:mm:ss"; } }); const convertJavaFormatToElement = (javaFormat) => { if (!javaFormat) return null; return javaFormat .replace("yyyy", "YYYY") .replace("MM", "MM") .replace("dd", "DD") .replace("HH", "HH") .replace("mm", "mm") .replace("ss", "ss"); }; const finalFormat = computed(() => { if (props.fmt) { return convertJavaFormatToElement(props.fmt); } return defaultFormat.value; }); // 使用 resolveComponent 获取组件 const ElDatePicker = resolveComponent("ElDatePicker"); const ElTimePicker = resolveComponent("ElTimePicker"); const SsFormIcon = resolveComponent("SsFormIcon"); const ElIcon = resolveComponent("ElIcon"); let useTimePicker = true; //"yyyy-MM-dd HH:mm:ss"; "日期字符串格式在java的写法",传到本组件fmt属性也是按这个格式 if (props.fmt) { //有fmt属性,则以fmt属性优先判断类型 if (/[dMy]/.test(props.fmt)) { //如果有传入日期格式,且含年月日 useTimePicker = false; } else { useTimePicker = true; } } else if (props.type !== "time") { useTimePicker = false; } const dateType = computed(() => { const fmt = props.fmt || ""; if (fmt.includes("HH:mm:ss")) { return "datetime"; } else if (fmt.includes("HH:mm")) { return "datetime"; } else if (fmt.includes("mm:ss")) { return "time"; } return "date"; }); const handleValueUpdate = (val) => { emit("update:modelValue", val); emit("change", val); // 同时触发 change 事件 setTimeout(() => { validate(); }, 50); }; return () => h( "div", { class: "ss-search-date-picker", style: { width: props.width } }, [ h("input", { type: "hidden", name: props.name, value: props.modelValue, }), h(useTimePicker ? ElTimePicker : ElDatePicker, { modelValue: props.modelValue, "onUpdate:modelValue": handleValueUpdate, type: dateType.value, format: finalFormat.value, "value-format": finalFormat.value, clearable: true, placeholder: props.placeholder, class: "custom-date-picker", // 用于自定义样式 "time-arrow-control": props.type === "datetime", // 修改这里 size: "large", // 添加这一行,改为 large 尺寸 style: { width: "100%" }, "prefix-icon": h(SsFormIcon, { class: "form-icon-time" }), }), ] ); }, }; // 搜索按钮组件(包含下拉按钮) const SsSearchButton = { name: "SsSearchButton", props: { text: { type: String, required: true, }, iconClass: { type: String, required: false, }, opt: { type: Array, default: () => [], }, checkId: { type: String, default: "0", }, }, emits: ["click"], setup(props, { emit }) { const currentId = ref(props.checkId || "0"); const showPopup = ref(false); const handleMouseEnter = () => { showPopup.value = true; }; const handleMouseLeave = () => { showPopup.value = false; }; // 添加点击事件处理,阻止默认行为 const handleClick = (e) => { e.preventDefault(); if (props.opt?.length > 0) { const selectedOption = currentId.value === "0" ? props.opt[0] : props.opt.find((opt) => opt.id === currentId.value); if (selectedOption) { selectedOption.callback?.(); } } else { emit("click", e); } }; // 获取显示文本 const getDisplayText = () => { if (!props.opt?.length) return props.text; const selectedOption = currentId.value === "0" ? props.opt[0] : props.opt.find((opt) => opt.id === currentId.value); return selectedOption ? selectedOption.desc : props.opt[0].desc; }; return () => h( "button", { class: props.opt?.length > 0 ? "ss-drop-button ss-drop-button-more" : "ss-drop-button", type: "button", // 明确指定按钮类型为 button onMouseenter: handleMouseEnter, onMouseleave: handleMouseLeave, onClick: handleClick, // 添加点击事件处理 }, [ props.iconClass ? h("span", { class: props.iconClass, style: { fontFamily: "iconfont", marginRight: "5px" }, }) : null, h("span", getDisplayText()), props.opt.length > 0 && showPopup.value && h( "div", { class: "popup", }, props.opt.map((item) => h( "div", { onClick: (e) => { e.preventDefault(); // 选项点击也阻止默认行为 e.stopPropagation(); // 阻止事件冒泡 currentId.value = item.id; // 更新当前选中的ID item.callback(); showPopup.value = false; // 选择后关闭弹窗 }, }, item.desc ) ) ), ] ); }, }; // 下拉按钮组件 const SsDropButton = { name: "SsDropButton", props: { text: { type: String, required: true, }, iconClass: { type: String, required: true, }, opt: { type: Array, default: () => [], }, checkId: { type: String, default: "0", }, onclick: { type: Function, default: null, }, }, setup(props) { const currentId = ref(props.checkId || "0"); const showPopup = ref(false); const handleMouseEnter = () => { showPopup.value = true; }; const handleMouseLeave = () => { showPopup.value = false; }; // 添加点击事件处理,阻止默认行为 const handleClick = (e) => { e.preventDefault(); if (props.opt?.length > 0) { const selectedOption = currentId.value === "0" ? props.opt[0] : props.opt.find((opt) => opt.id === currentId.value); if (selectedOption) { selectedOption.callback?.(); } } else if (props.onclick) { props.onclick(); } }; // 获取显示文本 const getDisplayText = () => { if (!props.opt?.length) return props.text; const selectedOption = currentId.value === "0" ? props.opt[0] : props.opt.find((opt) => opt.id === currentId.value); return selectedOption ? selectedOption.desc : props.opt[0].desc; }; return () => h( "button", { class: props.opt?.length > 0 ? "ss-drop-button ss-drop-button-more" : "ss-drop-button", type: "button", // 明确指定按钮类型为 button onMouseenter: handleMouseEnter, onMouseleave: handleMouseLeave, onClick: handleClick, // 添加点击事件处理 }, [ h("span", { class: props.iconClass, style: { fontFamily: "iconfont" }, }), h("span", getDisplayText()), props.opt.length > 0 && showPopup.value && h( "div", { class: "popup", }, props.opt.map((item) => h( "div", { onClick: (e) => { e.preventDefault(); // 选项点击也阻止默认行为 e.stopPropagation(); // 阻止事件冒泡 currentId.value = item.id; // 更新当前选中的ID item.callback(); showPopup.value = false; // 选择后关闭弹窗 }, }, item.desc ) ) ), ] ); }, }; /** * 二级页面标签组件 * @name ss-sub-tab * @description 用于展示二级页面的布局组件,包含左侧垂直标签导航(支持分组)和右侧iframe内容区 * @property {String} headerImage - 左侧顶部图片地址 * @property {Array} menuList - 菜单配置列表 * @property {Object} [activeMenu] - 当前选中的菜单项,不传则自动选择第一个可选菜单 * @property {Array} footerButtons - 底部按钮配置列表 */ const SsSubTab = { name: "SsSubTab", props: { headerImage: { type: String, default: "", }, menuList: { type: Array, required: true, }, activeMenu: { // 只需要传入标题 type: String, default: "", }, footerButtons: { type: Array, default: () => [], }, leftDisplay: { type: Boolean, default: true, }, }, emits: ["menu-change", "footer-click"], setup(props, { emit }) { // 根据标题找到对应的菜单项 const findMenuByTitle = (title) => { for (const item of props.menuList) { if (item.children?.length > 0) { const child = item.children.find((c) => c.title === title); if (child) return child; } else if (item.title === title) { return item; } } return null; }; // 计算默认选中的菜单项 const defaultActiveMenu = computed(() => { if (props.activeMenu) { const menu = findMenuByTitle(props.activeMenu); if (menu) return menu; } const firstItem = props.menuList[0]; if (!firstItem) return null; return firstItem.children?.length > 0 ? firstItem.children[0] : firstItem; }); const currentMenu = ref(defaultActiveMenu.value); // 监听外部activeMenu变化 watch( () => props.activeMenu, (newTitle) => { if (newTitle) { const menu = findMenuByTitle(newTitle); if (menu) { currentMenu.value = menu; } } } ); // 选择菜单项时触发 menu-change 钩子 const selectItem = (item) => { currentMenu.value = item; emit("menu-change", item); }; // 处理底部按钮点击 const handleFooterClick = (button, index) => { emit("footer-click", { button, index }); }; return { currentMenu, selectItem, handleFooterClick, }; }, template: `