// H5版本的小程序组件库 // 参考 alf/ss-components.js 的组件形式 (function () { const { ref, createApp, watch, inject, onMounted, computed } = Vue; // ===== 公共上传函数 ===== /** * 统一文件上传函数 * @param {File|Blob} file - 文件或Blob对象 * @param {String} type - 'image' | 'file' * @param {String} fileName - 文件名(可选) * @returns {Promise} 服务器路径 */ async function uploadFile(file, type = 'image', fileName) { try { window.showToast?.('上传中...', 'loading'); const formData = new FormData(); const name = fileName || (file instanceof Blob ? 'file' : file.name); formData.append('fileEdit', file, name); formData.append('application', ''); // 根据类型选择接口参数 const apiType = type === 'image' ? 'img' : 'file'; const result = await window.request.post( `/service?ssServ=ulByHttp&type=${apiType}`, formData, { loading: false } ); if (result?.data?.fileList?.[0]?.path) { const serverPath = result.data.fileList[0].path; window.showToast?.('上传成功', 'success'); return serverPath; } else { throw new Error('上传返回数据格式错误'); } } catch (error) { console.error('上传失败:', error); window.showToast?.('上传失败,请重试', 'error'); throw error; } } const SsCommonIcon = { name: "SsCommonIcon", props: { class: { type: String, required: true, }, }, setup(props) { const { h } = Vue; return () => h("i", { class: props.class + " common-icon", }); }, }; // ss-input 智能输入组件 const SsInput = { name: 'SsInput', inheritAttrs: false, // 不直接继承属性到组件根元素 props: { // v-model 绑定的值 modelValue: { type: [String, Number], default: '', }, // 字段名称 name: { type: String, default: '', }, // 占位符 placeholder: { type: String, default: '请输入', }, // 错误提示 errTip: { type: String, default: '', }, }, emits: ['update:modelValue', 'input', 'blur', 'change', 'focus'], setup(props, { emit }) { const inputValue = ref(props.modelValue || ''); const validationState = ref({ hasError: false, errorMessage: '', isRequired: false, isEmpty: true, hasInteracted: false, // 是否已经交互过 isSubmitMode: false, // 是否处于提交模式 }); // 从ValidatedTd注入事件处理函数(兼容小程序方式) const onInputInject = inject('onInput', null); const onBlurInject = inject('onBlur', null); // 检查是否为必填字段 const checkRequired = () => { if (window.ssVm && props.name) { validationState.value.isRequired = window.ssVm.isRequired( props.name ); } }; // 更新父级td的class const updateTdClass = () => { // 找到父级td元素 const inputElement = document.querySelector( `input[name="${props.name}"]` ); if (inputElement) { const tdElement = inputElement.closest('td'); if (tdElement) { // 移除所有校验相关的class tdElement.classList.remove('td-required', 'td-error'); // 逻辑分离: // 1. 初始状态:必填且为空 → 只有左侧红线 (td-required) // 2. 实时校验失败 → 左侧红线 + 底部红线 + 错误文字 (td-error) if ( validationState.value.hasError && validationState.value.hasInteracted ) { // 用户交互后校验失败:左侧红线 + 底部红线 + 错误文字 tdElement.classList.add('td-error'); } else if ( validationState.value.isRequired && validationState.value.isEmpty ) { // 必填且为空:只有左侧红线 tdElement.classList.add('td-required'); } } } }; // 校验字段 const validateField = (value) => { if (window.ssVm && props.name) { const result = window.ssVm.validateField(props.name); validationState.value.hasError = !result.valid; validationState.value.errorMessage = result.message || ''; validationState.value.isEmpty = !value || value.trim() === ''; // 更新td的class setTimeout(updateTdClass, 0); return result; } return { valid: true, message: '' }; }; // 监听props变化 watch( () => props.modelValue, (newVal) => { inputValue.value = newVal; validateField(newVal); } ); // 挂载时初始化 onMounted(() => { checkRequired(); validateField(inputValue.value); // 监听ssVm规则更新事件 const inputElement = document.querySelector( `input[name="${props.name}"]` ); if (inputElement) { inputElement.addEventListener('ssvm-rules-updated', () => { console.log(`收到规则更新通知: ${props.name}`); checkRequired(); validateField(inputValue.value); updateTdClass(); }); } // 确保初始状态正确显示,延迟更长时间确保DOM完全渲染 setTimeout(() => { updateTdClass(); }, 200); }); // 事件处理函数 const handleInput = (event) => { const value = event.target.value; inputValue.value = value; // 标记已交互 validationState.value.hasInteracted = true; // 1. 支持v-model emit('update:modelValue', value); emit('input', event); // 2. 内部校验(实时校验) validateField(value); // 3. 兼容小程序inject方式 if (onInputInject) { onInputInject(event); } }; const handleBlur = (event) => { // 标记已交互 validationState.value.hasInteracted = true; emit('blur', event); // 失焦时再次校验 validateField(inputValue.value); // 兼容小程序inject方式 if (onBlurInject) { onBlurInject(event); } }; const handleFocus = (event) => { emit('focus', event); }; return { inputValue, validationState, handleInput, handleBlur, handleFocus, }; }, template: `
{{ validationState.errorMessage }}
`, }; // icon 图标组件 - 从小程序转换 const Icon = { name: 'Icon', props: { // 图标名称,对应iconfont中的类名,如'icon-home' name: { type: String, required: true, }, // 图标颜色 color: { type: String, default: '#000', }, // 图标大小,单位rpx (H5中转换为px) size: { type: [Number, String], default: 32, }, }, emits: ['click'], setup(props, { emit }) { // 计算完整的图标类名 const iconClass = computed(() => { return props.name; }); // 计算样式 const iconStyle = computed(() => ({ color: props.color, fontSize: parseInt(props.size) / 2 + 'px', // rpx转px,除以2 verticalAlign: 'middle', })); // 点击事件处理 const handleClick = (event) => { emit('click', event); }; return { iconClass, iconStyle, handleClick, }; }, template: ` `, }; // ss-card 卡片组件 - 从小程序转换 const SsCard = { name: 'SsCard', props: { item: { type: Object, default: () => ({}), }, }, emits: ['click', 'buttonClick'], setup(props, { emit }) { // 状态 const showButtonMenu = ref(false); // 计算属性 const hasButtons = computed(() => { return ( props.item?.buttons && Array.isArray(props.item.buttons) && props.item.buttons.length > 0 ); }); const isMultipleButtons = computed(() => { return props.item?.buttons && props.item.buttons.length > 1; }); const isSingleButton = computed(() => { return props.item?.buttons && props.item.buttons.length === 1; }); // 处理卡片点击 const handleCardClick = () => { // 如果菜单打开,先关闭菜单 if (showButtonMenu.value) { showButtonMenu.value = false; return; } emit('click'); }; // 处理设置按钮点击 const handleSettingClick = () => { if (isSingleButton.value) { // 只有一个按钮,直接执行 handleButtonClick(props.item.buttons[0], 0); } else if (isMultipleButtons.value) { // 先记录当前状态 const wasOpen = showButtonMenu.value; // 关闭其他卡片的菜单 document.dispatchEvent( new CustomEvent('closeAllCardMenus') ); // 切换当前菜单状态 showButtonMenu.value = !wasOpen; } }; // 关闭菜单 const closeMenu = () => { showButtonMenu.value = false; }; // 处理按钮点击 const handleButtonClick = (btn, index) => { showButtonMenu.value = false; // 执行按钮的回调 if (btn.onclick && typeof btn.onclick === 'function') { btn.onclick(); } // 触发组件事件 emit('buttonClick', { button: btn, index, item: props.item }); }; // 监听全局关闭事件 onMounted(() => { document.addEventListener('closeAllCardMenus', closeMenu); }); // H5环境下的清理逻辑 // 注意:Vue 3的beforeUnmount在某些H5环境下可能不可用 // 这里暂时省略,依赖页面刷新时的自动清理 return { showButtonMenu, hasButtons, isMultipleButtons, isSingleButton, handleCardClick, handleSettingClick, handleButtonClick, }; }, template: `
`, }; // ss-search-button 搜索按钮组件 - 从小程序转换 const SsSearchButton = { name: 'SsSearchButton', props: { // 按钮文本 text: { type: String, default: '增加', }, // 是否禁用 disabled: { type: Boolean, default: false, }, // 按钮高度 height: { type: [String, Number], default: '36px', }, // 前置图标名称 preIcon: { type: String, default: '', }, // 后置图标名称 suffixIcon: { type: String, default: '', }, // 图标大小 iconSize: { type: [String, Number], default: '32', }, // 图标颜色 iconColor: { type: String, default: '#585d6e', }, // 自定义按钮样式 customStyle: { type: Object, default: () => ({}), }, // 跳转链接(兼容原JSP用法) href: { type: String, default: '', }, // 选项列表 options: { type: Array, default: () => [], }, }, emits: ['click', 'optionClick'], setup(props, { emit }) { // 状态 const showOptionsMenu = ref(false); // 计算属性 const hasOptions = computed(() => { return ( props.options && Array.isArray(props.options) && props.options.length > 0 ); }); const hasMultipleOptions = computed(() => { return props.options && props.options.length > 1; }); const isSingleOption = computed(() => { return props.options && props.options.length === 1; }); // 按钮样式 const buttonStyle = computed(() => ({ height: typeof props.height === 'number' ? `${props.height}px` : props.height, ...props.customStyle, })); // 处理按钮点击 const handleClick = () => { if (!hasOptions.value) { // 没有选项,直接触发点击事件 emit('click'); } else if (isSingleOption.value) { // 单个选项,直接执行 handleOptionClick(props.options[0], 0); } else if (hasMultipleOptions.value) { // 先记录当前状态 const wasOpen = showOptionsMenu.value; // 关闭其他按钮的菜单 document.dispatchEvent( new CustomEvent('closeAllButtonMenus') ); // 切换当前菜单状态 showOptionsMenu.value = !wasOpen; } }; // 处理选项点击 const handleOptionClick = (option, index) => { showOptionsMenu.value = false; // 执行选项的回调 if (option.onclick && typeof option.onclick === 'function') { option.onclick(); } // 触发组件事件 emit('optionClick', { option, index }); }; // 关闭菜单 const closeMenu = () => { showOptionsMenu.value = false; }; // 监听全局关闭事件 onMounted(() => { document.addEventListener('closeAllButtonMenus', closeMenu); }); return { showOptionsMenu, hasOptions, hasMultipleOptions, isSingleOption, buttonStyle, handleClick, handleOptionClick, }; }, template: `
{{ option.text }}
`, }; // ss-select 下拉选择组件 - 从小程序转换 const SsSelect = { name: 'SsSelect', props: { // 选项数组 options: { type: Array, default: () => [], }, // 字段映射 mapping: { type: Object, default: () => ({ text: 'n', value: 'v' }), }, // 默认值 modelValue: { type: [String, Number], default: '', }, // 占位符 placeholder: { type: String, default: '请选择', }, // 校验配置 validation: { type: Object, default: () => ({ enable: false, message: '' }), }, // 是否禁用 disabled: { type: Boolean, default: false, }, // 是否支持搜索 searchable: { type: Boolean, default: false, }, // 是否支持清空 clearable: { type: Boolean, default: false, }, // 加载状态 loading: { type: Boolean, default: false, }, // 宽度设置 width: { type: String, default: '100%', }, minWidth: { type: String, default: 'unset', }, }, emits: ['update:modelValue', 'change', 'search', 'clear'], setup(props, { emit }) { // 响应式数据 const isOpen = ref(false); const selectedValue = ref(props.modelValue); const searchKeyword = ref(''); // 计算属性 const optionsList = computed(() => props.options || []); const displayText = computed(() => { if (!selectedValue.value) return props.placeholder; const selectedOption = optionsList.value.find( (option) => option[props.mapping.value] === selectedValue.value ); return selectedOption ? selectedOption[props.mapping.text] : props.placeholder; }); // 监听 modelValue 变化 watch( () => props.modelValue, (newValue) => { selectedValue.value = newValue; } ); // 切换下拉框 const toggleDropdown = () => { if (props.disabled) return; isOpen.value = !isOpen.value; }; // 选择选项 const selectOption = (option) => { const value = option[props.mapping.value]; selectedValue.value = value; isOpen.value = false; emit('update:modelValue', value); emit('change', value); }; // 点击外部关闭 const handleClickOutside = (event) => { if (!event.target.closest('.ss-select-container')) { isOpen.value = false; } }; onMounted(() => { document.addEventListener('click', handleClickOutside); }); return { isOpen, selectedValue, searchKeyword, optionsList, displayText, toggleDropdown, selectOption, }; }, template: `
{{ displayText }}
加载中...
无选项
{{ option[mapping.text] }}
`, }; // ss-bottom 底部按钮组件 const SsBottom = { name: 'SsBottom', props: { // 是否显示审核意见 showShyj: { type: Boolean, default: false, }, // 审核意见标题 shyjTitle: { type: String, default: '审核意见', }, // 审核意见占位符 shyjPlaceholder: { type: String, default: '请输入审核意见', }, divider: { type: Boolean, default: true, }, // 按钮配置 buttons: { type: Array, default: () => [ { text: '取消', action: 'cancel' }, { text: '保存并提交', action: 'submit' }, ], }, }, emits: ['button-click', 'update:shyjValue'], setup(props, { emit }) { const reason = ref(''); const activeButtonIndex = ref(-1); // 处理按钮点击 const handleButtonClick = (button, index) => { emit('button-click', { action: button.action, button: button, index: index, shyjValue: reason.value, // 传递审核意见 }); }; // 监听审核意见变化 const handleShyjInput = (event) => { const value = event.target.value; reason.value = value; emit('update:shyjValue', value); }; // 处理按钮按下 const handleButtonMouseDown = (index) => { activeButtonIndex.value = index; }; // 处理按钮释放 const handleButtonMouseUp = () => { activeButtonIndex.value = -1; }; // 处理鼠标离开 const handleMouseLeave = () => { activeButtonIndex.value = -1; }; // 计算按钮样式 const getButtonStyle = (button) => { const styles = {}; // 如果有背景颜色配置 if (button.backgroundColor) { styles.backgroundColor = button.backgroundColor; } // 如果有字体颜色配置 if (button.color) { styles.color = button.color; } return styles; }; // 计算按钮点击样式 const getButtonActiveStyle = (button) => { const styles = {}; // 如果有点击背景色配置,使用点击背景色 if (button.clickBgColor) { styles.backgroundColor = button.clickBgColor; } else if (button.backgroundColor) { // 如果没有点击背景色,但有背景色,点击时使用背景色 styles.backgroundColor = button.backgroundColor; } // 如果有点击字体色配置,使用点击字体色 if (button.clickColor) { styles.color = button.clickColor; } else if (button.color) { // 如果没有点击字体色,但有字体色,点击时使用字体色 styles.color = button.color; } return styles; }; return { reason, activeButtonIndex, handleButtonClick, handleShyjInput, handleButtonMouseDown, handleButtonMouseUp, handleMouseLeave, getButtonStyle, getButtonActiveStyle, }; }, template: `
{{ shyjTitle }}
`, }; // ===== SsVerify 审核节点链组件 ===== const SsVerify = { name: "SsVerify", props: { verifyList: { type: Array, required: true, }, }, setup(props) { const toggleOpen = (item) => { item.open = !item.open; // 切换后重新计算连线高度 setTimeout(() => { calculateLineHeight(); }, 50); }; // 计算连线高度的函数 const calculateLineHeight = () => { const lastOpenGroup = document.querySelector(".group-item-last-open"); console.log("lastOpenGroup", lastOpenGroup); if (lastOpenGroup) { // 使用原生JavaScript代替jQuery const nodes = lastOpenGroup.querySelectorAll(".verify-node-container"); if (nodes.length) { let totalHeight = 0; if (nodes.length === 1) { // 只有一个节点时,连线伸到节点的中间位置 const nodeHeight = nodes[0].offsetHeight; const nodeTop = nodes[0].offsetTop; totalHeight = nodeTop + (nodeHeight / 2) - 15; // 减去圆点半径5px } else { // 多个节点时,连线延伸到最后一个节点的中间位置 const lastNode = nodes[nodes.length - 1]; const lastNodeTop = lastNode.offsetTop; const lastNodeHeight = lastNode.offsetHeight; totalHeight = lastNodeTop + (lastNodeHeight / 2) - 15; // 减去圆点半径5px } console.log("节点信息:", { 节点总数: nodes.length, 计算后的高度: totalHeight, 最后节点top: nodes[nodes.length - 1]?.offsetTop, 最后节点高度: nodes[nodes.length - 1]?.offsetHeight, }); lastOpenGroup.style.setProperty( "--group-line-height", `${totalHeight}px` ); } } }; onMounted(() => { setTimeout(() => { calculateLineHeight(); }, 100); }); return { toggleOpen, }; }, render() { const { h } = Vue; 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" }, [ h("i", { class: item.open ? "common-icon-folder-open common-icon" : "common-icon-folder-close common-icon" }), 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, ] ) ) ); }, }; // ===== SsVerifyNode 审核节点组件 ===== const SsVerifyNode = { name: "SsVerifyNode", props: { item: { type: Object, required: true, }, isGroup: { type: Boolean, default: false, }, }, render() { const { h } = Vue; return h("div", { class: "verify-node-container" }, [ h("div", { class: "info" }, [ h("div", { class: "avatar" }, [ h("img", { src: this.item.thumb, style: { width: "50px", height: "50px", borderRadius: "50%", }, }), ]), h("div", { class: "desc" }, [ h("div", this.item.name), h("div", this.item.role), ]), h("div", { class: "link" }, [ h("div", [ this.item.video ? h("i", { class: "common-icon-video common-icon" }) : null, this.item.link ? h("i", { class: "common-icon-paper-clip common-icon" }) : null, ]), ]), ]), h( "div", { class: { description: true, link: this.isGroup, }, attrs: { "data-num": "3" }, }, [h("div", this.item.description)] ), h("div", { class: "time" }, this.item.time), ]); }, }; // ===== SsOnoffButton 开关按钮 ===== const SsOnoffButton = { name: 'SsOnoffButton', props: { // 字段名称,用于表单校验 name: { type: String, required: true }, // 显示标签 label: { type: String, required: true }, // 按钮的值 value: { type: [String, Number], required: true }, // 宽度设置 width: { type: String, default: '' }, // v-model 绑定的值 modelValue: { type: [String, Number, Array], default: '' }, // 是否多选模式 multiple: { type: Boolean, default: false }, // 是否禁用 disabled: { type: Boolean, default: false } }, emits: ['update:modelValue', 'change'], setup(props, { emit }) { // 解析 modelValue,支持逗号分隔的字符串和数组 const parseModelValue = (val) => { if (!val) return []; // 如果是数组,直接返回字符串数组 if (Array.isArray(val)) { return val.map(v => v.toString()); } // 如果是字符串,按逗号分割 const cleanValue = val.toString().replace(/^,+/, ''); // 去掉开头的逗号 if (cleanValue.includes('|')) { return cleanValue.split('|'); } if (cleanValue.includes(',')) { return cleanValue.split(','); } return cleanValue ? [cleanValue] : []; }; // 判断当前按钮是否选中 const isChecked = computed(() => { if (props.multiple) { const currentValue = parseModelValue(props.modelValue); return currentValue.includes(props.value.toString()); } return props.modelValue === props.value; }); // 切换选中状态 const toggleSelect = () => { // 如果禁用,不执行任何操作 if (props.disabled) return; if (props.multiple) { // 多选模式 const currentValue = parseModelValue(props.modelValue); const index = currentValue.indexOf(props.value.toString()); let newValue; if (index === -1) { // 添加选项 newValue = [...currentValue, props.value.toString()]; } else { // 移除选项 newValue = currentValue.filter(v => v !== props.value.toString()); } // 发送更新事件,使用逗号分隔的字符串格式 const emitValue = newValue.join(','); emit('update:modelValue', emitValue); emit('change', emitValue, newValue); } else { // 单选模式 emit('update:modelValue', props.value); emit('change', props.value); } }; return { isChecked, toggleSelect }; }, template: `
{{ label }}
`, }; // ===== SsDatetimePicker 日期时间选择(使用 Vant 4) ===== const SsDatetimePicker = { name: 'SsDatetimePicker', props: { mode: { type: String, default: 'date' }, // date | time | datetime placeholder: { type: String, default: '请选择日期' }, modelValue: { type: String, default: '' }, minDate: { type: String, default: '' }, maxDate: { type: String, default: '' }, // 字段名称 - 用于ssVm校验 name: { type: String, default: '' }, // 是否禁用 disabled: { type: Boolean, default: false }, }, emits: ['update:modelValue', 'change', 'confirm', 'cancel'], setup(props, { emit }) { const showPicker = ref(false); const showTimePicker = ref(false); const currentStep = ref('date'); // 'date' | 'time' // Vant DatePicker 需要数组格式 [year, month, day] const currentDateArray = ref([]); const currentTimeArray = ref(['12', '00']); // [hour, minute] const tempDateStr = ref(''); // 临时存储选择的日期 // 格式化显示文本 const displayText = computed(() => { if (!props.modelValue) return props.placeholder; try { const d = new Date(props.modelValue); if (isNaN(d.getTime())) return props.modelValue; const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const hours = String(d.getHours()).padStart(2, '0'); const minutes = String(d.getMinutes()).padStart(2, '0'); if (props.mode === 'time') { return `${hours}:${minutes}`; } else if (props.mode === 'datetime') { return `${year}-${month}-${day} ${hours}:${minutes}`; } return `${year}-${month}-${day}`; } catch (e) { return props.modelValue; } }); // 监听 modelValue 变化,转换为数组格式 watch(() => props.modelValue, (newVal) => { console.log('📅 modelValue 变化:', newVal); if (newVal) { try { const d = new Date(newVal); if (!isNaN(d.getTime())) { currentDateArray.value = [d.getFullYear(), d.getMonth() + 1, d.getDate()]; console.log('📅 转换为数组:', currentDateArray.value); } } catch (e) { console.warn('Invalid date:', newVal); } } else { const today = new Date(); currentDateArray.value = [today.getFullYear(), today.getMonth() + 1, today.getDate()]; } }, { immediate: true }); // 确认选择 - 根据模式处理不同的数据 const onConfirm = (value) => { console.log('📅 Vant Picker confirm 原始值:', value, 'mode:', props.mode); try { // Vant 返回的是对象,包含 selectedValues 数组 const selectedValues = value.selectedValues || value; console.log('📅 selectedValues:', selectedValues); if (props.mode === 'time') { // 时间模式:处理时分 if (Array.isArray(selectedValues) && selectedValues.length >= 2) { const [hour, minute] = selectedValues; const timeStr = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`; console.log('🕐 转换后的时间字符串:', timeStr); emit('update:modelValue', timeStr); emit('change', timeStr); emit('confirm', timeStr); showPicker.value = false; } } else if (Array.isArray(selectedValues) && selectedValues.length >= 3) { // 日期模式:处理年月日 const [year, month, day] = selectedValues; const dateStr = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; if (props.mode === 'datetime') { // datetime 模式:先存储日期,然后打开时间选择器 tempDateStr.value = dateStr; showPicker.value = false; showTimePicker.value = true; currentStep.value = 'time'; } else { // date 模式:直接完成 emit('update:modelValue', dateStr); emit('change', dateStr); emit('confirm', dateStr); showPicker.value = false; } } } catch (e) { console.error('Picker conversion error:', e); } }; // 时间选择确认 const onTimeConfirm = (value) => { try { const selectedValues = value.selectedValues || value; if (Array.isArray(selectedValues) && selectedValues.length >= 2) { const [hour, minute] = selectedValues; const datetimeStr = `${tempDateStr.value} ${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`; emit('update:modelValue', datetimeStr); emit('change', datetimeStr); emit('confirm', datetimeStr); } } catch (e) { console.error('Time conversion error:', e); } showTimePicker.value = false; currentStep.value = 'date'; }; // 取消选择 const onCancel = () => { emit('cancel'); showPicker.value = false; }; // 打开选择器 const openPicker = () => { // 如果禁用,不打开选择器 if (props.disabled) return; showPicker.value = true; }; // 计算最小最大日期 const minDateObj = computed(() => { return props.minDate ? new Date(props.minDate) : undefined; }); const maxDateObj = computed(() => { return props.maxDate ? new Date(props.maxDate) : undefined; }); return { showPicker, showTimePicker, currentDateArray, currentTimeArray, displayText, openPicker, onConfirm, onTimeConfirm, onCancel, minDateObj, maxDateObj }; }, template: `
{{ displayText }}
`, }; const SsConfirm = { name: 'SsConfirm', props: { modelValue: { type: Boolean, default: false }, title: { type: String, default: '确认' }, content: { type: String, default: '' }, maskClosable: { type: Boolean, default: true }, }, emits: ['update:modelValue', 'confirm', 'cancel', 'close'], setup(props, { emit }) { // 监听弹窗显示状态,控制 body 滚动 watch(() => props.modelValue, (newVal) => { console.log('🔔 SsConfirm modelValue 变化:', newVal); if (newVal) { document.body.classList.add('modal-open'); } else { document.body.classList.remove('modal-open'); } }, { immediate: true }); // 添加 immediate 选项 const close = () => { console.log('🚪 关闭确认弹窗'); emit('update:modelValue', false); emit('close'); }; const onMask = () => { console.log('👆 点击了遮罩层'); if (props.maskClosable) close(); }; const onCancel = () => { console.log('❌ 点击了取消按钮'); emit('cancel'); close(); }; const onConfirm = () => { console.log('✅ 点击了确认按钮,触发 confirm 事件'); emit('confirm'); close(); }; return { onMask, onCancel, onConfirm }; }, template: `
{{ title }}
`, }; // ===== SsImageCropper 纯裁剪组件 ===== const SsImageCropper = { name: 'SsImageCropper', props: { // 是否显示裁剪器 show: { type: Boolean, default: false }, // 图片源(base64 或 URL) src: { type: String, required: true }, // 图片形状:circle圆形 | square方形 shape: { type: String, required: true }, // 裁剪比例(宽/高) aspectRatio: { type: Number, default: 1 }, // 输出图片宽度 outputWidth: { type: Number, default: 300 }, // 输出图片高度 outputHeight: { type: Number, default: 300 }, }, emits: ['update:show', 'confirm', 'cancel'], setup(props, { emit }) { const cropperInstance = ref(null); // 监听 show 变化,初始化或销毁 Cropper watch(() => props.show, (newVal) => { if (newVal) { // 等待 DOM 更新后初始化 Vue.nextTick(() => { initCropper(); }); } else { destroyCropper(); } }); // 监听 src 变化,重新初始化 Cropper watch(() => props.src, () => { if (props.show) { Vue.nextTick(() => { initCropper(); }); } }); // 初始化 Cropper const initCropper = () => { const imageElement = document.getElementById('ss-image-cropper-img'); if (!imageElement || !window.Cropper) return; // 销毁旧实例 destroyCropper(); // 根据 shape 属性添加类名 const container = document.querySelector('.ss-image-cropper-container'); if (container) { if (props.shape === 'circle') { container.classList.add('crop-shape-circle'); container.classList.remove('crop-shape-square'); } else { container.classList.add('crop-shape-square'); container.classList.remove('crop-shape-circle'); } } cropperInstance.value = new window.Cropper(imageElement, { aspectRatio: props.aspectRatio, viewMode: 1, dragMode: 'move', autoCropArea: 0.8, restore: false, guides: false, // 关闭辅助线 center: false, // 关闭中心指示器 highlight: false, cropBoxMovable: true, cropBoxResizable: true, toggleDragModeOnDblclick: false, minContainerWidth: window.innerWidth, minContainerHeight: window.innerHeight - 50, }); }; // 销毁 Cropper const destroyCropper = () => { if (cropperInstance.value) { cropperInstance.value.destroy(); cropperInstance.value = null; } }; // 取消裁剪 const handleCancel = () => { emit('update:show', false); emit('cancel'); }; // 确认裁剪 const handleConfirm = () => { if (!cropperInstance.value) return; const canvas = cropperInstance.value.getCroppedCanvas({ width: props.outputWidth, height: props.outputHeight, imageSmoothingEnabled: true, imageSmoothingQuality: 'high', fillColor: '#fff' }); canvas.toBlob((blob) => { emit('update:show', false); emit('confirm', blob); }, 'image/jpeg', 0.9); }; // 处理底部按钮事件 const handleCropAction = (data) => { if (data.action === 'cancel') { handleCancel(); } else if (data.action === 'confirm') { handleConfirm(); } }; return { handleCropAction, }; }, template: `
长: {{ outputWidth }}px
宽: {{ outputHeight }}px
`, }; // ===== SsUploadImage 图片上传裁剪组件(支持单图/多图) ===== const SsUploadImage = { name: 'SsUploadImage', props: { // v-model 绑定的值(单图:String,多图:Array) modelValue: { type: [String, Array], default: '' }, // 最大上传数量(默认1张,多图时设置大于1) max: { type: Number, default: 1 }, // 是否禁用 disabled: { type: Boolean, default: false }, // 图片宽度(像素) - 必填 width: { type: [Number, String], required: true }, // 图片高度(像素) - 必填 height: { type: [Number, String], required: true }, // 图片形状:circle圆形 | square方形 - 必填 shape: { type: String, required: true }, // 裁剪比例(宽/高) aspectRatio: { type: Number, default: undefined }, // 输出图片宽度 outputWidth: { type: Number, default: 300 }, // 输出图片高度 outputHeight: { type: Number, default: 300 }, }, emits: ['update:modelValue', 'updated'], setup(props, { emit }) { const showCropper = ref(false); const tempImageSrc = ref(''); // 图片列表(统一用数组管理) const imageList = ref([]); // 监听 modelValue 变化,同步到 imageList watch(() => props.modelValue, (newVal) => { if (props.max === 1) { // 单图模式 imageList.value = newVal ? [newVal] : []; } else { // 多图模式 imageList.value = Array.isArray(newVal) ? [...newVal] : (newVal ? [newVal] : []); } }, { immediate: true }); // 是否可以继续添加图片 const canAddMore = computed(() => { return imageList.value.length < props.max; }); // 容器样式 const itemStyle = computed(() => ({ width: typeof props.width === 'number' ? `${props.width}px` : props.width, height: typeof props.height === 'number' ? `${props.height}px` : props.height, borderRadius: props.shape === 'circle' ? '50%' : '8px', })); // 获取图片 URL(用于显示) const getImageUrl = (path) => { if (!path) return '/static/images/yishuzhao_nv.svg'; if (path.startsWith('http') || path.startsWith('blob:')) { return path; } return window.SS.utils?.getImageUrl?.(path) || path; }; // 选择图片 const selectImage = () => { if (props.disabled || !canAddMore.value) return; const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { tempImageSrc.value = event.target.result; showCropper.value = true; }; reader.readAsDataURL(file); } }; input.click(); }; // 删除图片 const deleteImage = (index) => { const newList = imageList.value.filter((_, i) => i !== index); updateModelValue(newList); }; // 确认裁剪 const handleCropConfirm = async (blob) => { try { const serverPath = await uploadFile(blob, 'image', 'image.jpg'); const newList = [...imageList.value, serverPath]; updateModelValue(newList); emit('updated', serverPath); } catch (error) { console.error('上传失败:', error); } }; // 取消裁剪 const handleCropCancel = () => { tempImageSrc.value = ''; }; // 更新 modelValue const updateModelValue = (list) => { if (props.max === 1) { // 单图模式:返回 String emit('update:modelValue', list[0] || ''); } else { // 多图模式:返回 Array emit('update:modelValue', list); } }; return { imageList, canAddMore, itemStyle, showCropper, tempImageSrc, getImageUrl, selectImage, deleteImage, handleCropConfirm, handleCropCancel, }; }, template: `
{{ imageList.length }}/{{ max }}
`, }; // ===== SsCarCard 车辆卡片组件 ===== const SsCarCard = { name: 'SsCarCard', props: { // 车辆数据 carData: { type: Object, default: () => ({}) }, // 车辆状态:'available' | 'reserved' | 'disabled' status: { type: String, default: 'available', validator: (value) => ['available', 'reserved', 'disabled'].includes(value) } }, emits: ['click', 'select'], setup(props, { emit }) { // 计算状态样式类 const statusClass = computed(() => { return `status-${props.status}`; }); // 获取图片URL const getImageUrl = (path) => { if (!path) return '/static/images/default-car.png'; if (path.startsWith('http') || path.startsWith('blob:')) { return path; } return window.SS.utils?.getImageUrl?.(path) || path; }; // 处理卡片点击 const handleCardClick = () => { if (props.status === 'disabled') { return; // 禁用状态不响应点击 } emit('click', props.carData); emit('select', props.carData); }; return { statusClass, getImageUrl, handleCardClick, }; }, template: `
{{ carData.name || '别克GL8' }}
{{ carData.wph }}
{{ wp.mc }} : {{ wp.sz || wp.zf }}
{{ carData.type || '商务车' }}
`, }; // ===== SsSubTab 移动端Tab组件 ===== const SsSubTab = { name: 'SsSubTab', props: { // Tab列表数据 tabList: { type: Array, required: true, }, // 当前激活的Tab索引 activeIndex: { type: Number, default: 0, }, // 基础URL参数(会传递给每个iframe) baseParams: { type: Object, default: () => ({}), }, }, emits: ['tab-change'], setup(props, { emit }) { const currentTab = ref(props.activeIndex); const currentTabUrl = ref(''); // 加载Tab对应的URL const loadTabUrl = (index) => { const tab = props.tabList[index]; if (!tab || !tab.dest) return; // 构建iframe URL:mp_ + dest + .html const fileName = `mp_${tab.dest}.html`; // 构建完整URL,包含所有参数 const params = new URLSearchParams({ ...props.baseParams, service: tab.service || '', param: tab.param || '', }); currentTabUrl.value = `/page/${fileName}?${params.toString()}`; console.log('🔄 切换到Tab:', tab.desc || tab.title, '加载页面:', currentTabUrl.value); }; // 监听 activeIndex 变化 watch(() => props.activeIndex, (newIndex) => { currentTab.value = newIndex; loadTabUrl(newIndex); }, { immediate: true }); // 监听 tabList 变化 watch(() => props.tabList, () => { if (props.tabList.length > 0 && currentTab.value === 0) { loadTabUrl(0); } }, { immediate: true }); // 切换Tab const handleTabClick = (index) => { if (currentTab.value === index) return; currentTab.value = index; loadTabUrl(index); emit('tab-change', { index, tab: props.tabList[index] }); }; return { currentTab, currentTabUrl, handleTabClick, }; }, template: `
{{ tab.desc || tab.title }}
`, }; // ===== SsUploadFile 文件上传组件(支持单文件/多文件) ===== const SsUploadFile = { name: 'SsUploadFile', props: { // v-model 绑定的值(单文件:String,多文件:Array) modelValue: { type: [String, Array], default: '' }, // 最大上传数量(默认1个) max: { type: Number, default: 1 }, // 是否禁用 disabled: { type: Boolean, default: false }, // 允许的文件类型(默认常见文件类型) accept: { type: String, default: '.pdf,.doc,.docx,.xls,.xlsx,.txt,.zip,.rar' }, // 最大文件大小(MB,默认5MB) maxSize: { type: Number, default: 5 }, }, emits: ['update:modelValue', 'uploaded'], setup(props, { emit }) { // 文件列表(统一用数组管理) const fileList = ref([]); // 监听 modelValue 变化,同步到 fileList watch(() => props.modelValue, (newVal) => { if (props.max === 1) { // 单文件模式 fileList.value = newVal ? [{ path: newVal, name: getFileName(newVal) }] : []; } else { // 多文件模式 if (Array.isArray(newVal)) { fileList.value = newVal.map(path => ({ path, name: getFileName(path) })); } else { fileList.value = newVal ? [{ path: newVal, name: getFileName(newVal) }] : []; } } }, { immediate: true }); // 是否可以继续添加文件 const canAddMore = computed(() => { return fileList.value.length < props.max; }); // 从路径中提取文件名 const getFileName = (path) => { if (!path) return ''; const parts = path.split('/'); return parts[parts.length - 1]; }; // 获取文件图标 const getFileIcon = (fileName) => { const ext = fileName.split('.').pop().toLowerCase(); const iconMap = { pdf: 'icon-pdf', doc: 'icon-word', docx: 'icon-word', xls: 'icon-excel', xlsx: 'icon-excel', txt: 'icon-txt', zip: 'icon-zip', rar: 'icon-zip', }; return iconMap[ext] || 'icon-wenjian'; }; // 选择文件 const selectFile = () => { if (props.disabled || !canAddMore.value) return; const input = document.createElement('input'); input.type = 'file'; input.accept = props.accept; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; // 检查文件大小 const fileSizeMB = file.size / 1024 / 1024; if (fileSizeMB > props.maxSize) { window.showToast?.(`文件大小不能超过${props.maxSize}MB`, 'error'); return; } // 上传文件 try { const serverPath = await uploadFile(file, 'file', file.name); const newList = [...fileList.value, { path: serverPath, name: file.name }]; updateModelValue(newList); emit('uploaded', serverPath); } catch (error) { console.error('文件上传失败:', error); } }; input.click(); }; // 删除文件 const deleteFile = (index) => { const newList = fileList.value.filter((_, i) => i !== index); updateModelValue(newList); }; // 下载文件 const downloadFile = (file) => { const url = window.SS.utils?.getFileUrl?.(file.path) || window.getFileUrl?.(file.path) || file.path; window.open(url, '_blank'); }; // 更新 modelValue const updateModelValue = (list) => { const paths = list.map(f => f.path); if (props.max === 1) { // 单文件模式:返回 String emit('update:modelValue', paths[0] || ''); } else { // 多文件模式:返回 Array emit('update:modelValue', paths); } }; return { fileList, canAddMore, getFileIcon, selectFile, deleteFile, downloadFile, }; }, template: `
{{ file.name }}
上传文件 ({{ fileList.length }}/{{ max }}) 支持格式: {{ accept }},最大{{ maxSize }}MB
`, }; window.SS.dom.initializeFormApp = function (config) { const { el, ...vueOptions } = config; const app = createApp({ ...vueOptions, }); // 注册组件 // app.component("SsLoginIcon", SsLoginIcon); // app.component("SsMark", SsMark); // app.component("SsFullStyleHeader", SsFullStyleHeader); // app.component("SsDialog", SsDialog); app.component('SsInput', SsInput); app.component('SsBottom', SsBottom); app.component('SsCard', SsCard); app.component('SsSearchButton', SsSearchButton); app.component('SsSelect', SsSelect); app.component('Icon', Icon); // 注册 Vant 组件 const vantLib = window.vant || window.Vant; console.log('🔍 检查 Vant:', { hasVant: !!window.vant, hasVantCap: !!window.Vant, vantLib: !!vantLib, vantKeys: vantLib ? Object.keys(vantLib).slice(0, 20) : [], hasPopup: vantLib?.Popup, hasDatetimePicker: vantLib?.DatetimePicker, hasDatePicker: vantLib?.DatePicker, hasTimePicker: vantLib?.TimePicker, allKeys: vantLib ? Object.keys(vantLib) : [] }); if (vantLib) { try { // 使用 Vant 的 use 方法注册所有组件 app.use(vantLib); console.log('✅ Vant 全部组件注册成功'); } catch (error) { console.error('❌ Vant 组件注册失败:', error); // 降级方案:手动注册具体组件 try { app.component('van-popup', vantLib.Popup); app.component('van-datetime-picker', vantLib.DatetimePicker); console.log('✅ Vant 手动注册成功'); } catch (e) { console.error('❌ Vant 手动注册也失败:', e); } } } else { console.warn('⚠️ Vant 未加载'); } // 注册组件 - 统一使用 kebab-case app.component('ss-verify', SsVerify); app.component('ss-verify-node', SsVerifyNode); app.component('ss-common-icon', SsCommonIcon); app.component('ss-onoff-button', SsOnoffButton); app.component('ss-datetime-picker', SsDatetimePicker); app.component('ss-confirm', SsConfirm); app.component('ss-image-cropper', SsImageCropper); app.component('ss-upload-image', SsUploadImage); app.component('ss-upload-file', SsUploadFile); app.component('ss-car-card', SsCarCard); app.component('ss-sub-tab', SsSubTab); // app.component("SsObjp", SsObjp); // app.component("SsHidden", SsHidden); // app.component("SsCcp", SsCcp); // app.component("SsDatePicker", SsDatePicker); // app.component("SsIcon", SsIcon); // app.component("SsCommonIcon", SsCommonIcon); // app.component("SsBreadcrumb", SsBreadcrumb); // app.component("SsEditor", SsEditor); // app.component("SsDialogIcon", SsDialogIcon); // app.component("SsBottomButton", SsBottomButton); // app.component("SsNavIcon", SsNavIcon); // app.component("SsHeaderIcon", SsHeaderIcon); // app.component("SsGolbalMenuIcon", SsGolbalMenuIcon); // app.component("SsCartListIcon", SsCartListIcon); // app.component("SsQuickIcon", SsQuickIcon); // app.component("SsFormIcon", SsFormIcon); // app.component("SsBottomDivIcon", SsBottomDivIcon); // app.component("SsEditorIcon", SsEditorIcon); // app.component("SsValidate", SsValidate); // app.component("SsOnoffbutton", SsOnoffbutton); // app.component("SsOnoffbuttonArray", SsOnoffbuttonArray); // app.component("SsTextarea", SsTextarea); // app.component("SsLoginInput", SsLoginInput); // app.component("SsLoginButton", SsLoginButton); // app.component("SsSearch", SsSearch); // app.component("SsCartItem", SsCartItem); // app.component("SsCartItem2", SsCartItem2); // app.component("SsListCard", SsListCard); // app.component("SsFolderCard", SsFolderCard); // app.component("SsFolderCartView", SsFolderCartView); // app.component("SsPage", SsPage); // app.component("SsRightInfo", SSRightInfo); // app.component("SsSuccessPopup", SsSuccessPopup); // app.component("SsErrorDialog", SsErrorDialog); // app.component("SsVerify", SsVerify); // app.component("SsVerifyNode", SsVerifyNode); // app.component("SsOrcImgBox", SsOrcImgBox); // app.component("ss-search-input", SsSearchInput); // app.component("ss-search-date-picker", SsSearchDatePicker); // app.component("ss-search-button", SsSearchButton); // app.component("ss-drop-button", SsDropButton); // app.component("ss-sub-tab", SsSubTab); // app.component("ss-img", SsImgUpload); // 设置为中文 // app.use(ElementPlus, { // locale: ElementPlusLocaleZhCn, // }); // console.log(ElementPlus); // 确保 ElementPlusIconsVue // if (window.ElementPlusIconsVue) { // // 注册 Element Plus 图标组件 // for (const [key, component] of Object.entries( // window.ElementPlusIconsVue // )) { // console.log(key, component); // app.component(key, component); // } // } // 挂载首页的组件 // for (const componentName in IndexComponents) { // app.component(componentName, IndexComponents[componentName]); // } // 挂载echarts的组件 // for (const componentName in EchartComponents) { // app.component(componentName, EchartComponents[componentName]); // } // 挂载 Vue 应用 const vm = app.mount(el); vm.data = vueOptions.data(); return vm; }; })();